Init V4 community edition (#2265)
* Init V4 community edition * Init V4 community edition
This commit is contained in:
@@ -1,117 +0,0 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/rpc"
|
||||
)
|
||||
|
||||
// DownloadListResponse 下载列表响应条目
|
||||
type DownloadListResponse struct {
|
||||
UpdateTime time.Time `json:"update"`
|
||||
UpdateInterval int `json:"interval"`
|
||||
Name string `json:"name"`
|
||||
Status int `json:"status"`
|
||||
Dst string `json:"dst"`
|
||||
Total uint64 `json:"total"`
|
||||
Downloaded uint64 `json:"downloaded"`
|
||||
Speed int `json:"speed"`
|
||||
Info rpc.StatusInfo `json:"info"`
|
||||
NodeName string `json:"node"`
|
||||
}
|
||||
|
||||
// FinishedListResponse 已完成任务条目
|
||||
type FinishedListResponse struct {
|
||||
Name string `json:"name"`
|
||||
GID string `json:"gid"`
|
||||
Status int `json:"status"`
|
||||
Dst string `json:"dst"`
|
||||
Error string `json:"error"`
|
||||
Total uint64 `json:"total"`
|
||||
Files []rpc.FileInfo `json:"files"`
|
||||
TaskStatus int `json:"task_status"`
|
||||
TaskError string `json:"task_error"`
|
||||
CreateTime time.Time `json:"create"`
|
||||
UpdateTime time.Time `json:"update"`
|
||||
NodeName string `json:"node"`
|
||||
}
|
||||
|
||||
// BuildFinishedListResponse 构建已完成任务条目
|
||||
func BuildFinishedListResponse(tasks []model.Download) Response {
|
||||
resp := make([]FinishedListResponse, 0, len(tasks))
|
||||
|
||||
for i := 0; i < len(tasks); i++ {
|
||||
fileName := tasks[i].StatusInfo.BitTorrent.Info.Name
|
||||
if len(tasks[i].StatusInfo.Files) == 1 {
|
||||
fileName = path.Base(tasks[i].StatusInfo.Files[0].Path)
|
||||
}
|
||||
|
||||
// 过滤敏感信息
|
||||
for i2 := 0; i2 < len(tasks[i].StatusInfo.Files); i2++ {
|
||||
tasks[i].StatusInfo.Files[i2].Path = path.Base(tasks[i].StatusInfo.Files[i2].Path)
|
||||
}
|
||||
|
||||
download := FinishedListResponse{
|
||||
Name: fileName,
|
||||
GID: tasks[i].GID,
|
||||
Status: tasks[i].Status,
|
||||
Error: tasks[i].Error,
|
||||
Dst: tasks[i].Dst,
|
||||
Total: tasks[i].TotalSize,
|
||||
Files: tasks[i].StatusInfo.Files,
|
||||
TaskStatus: -1,
|
||||
UpdateTime: tasks[i].UpdatedAt,
|
||||
CreateTime: tasks[i].CreatedAt,
|
||||
NodeName: tasks[i].NodeName,
|
||||
}
|
||||
|
||||
if tasks[i].Task != nil {
|
||||
download.TaskError = tasks[i].Task.Error
|
||||
download.TaskStatus = tasks[i].Task.Status
|
||||
}
|
||||
|
||||
resp = append(resp, download)
|
||||
}
|
||||
|
||||
return Response{Data: resp}
|
||||
}
|
||||
|
||||
// BuildDownloadingResponse 构建正在下载的列表响应
|
||||
func BuildDownloadingResponse(tasks []model.Download, intervals map[uint]int) Response {
|
||||
resp := make([]DownloadListResponse, 0, len(tasks))
|
||||
|
||||
for i := 0; i < len(tasks); i++ {
|
||||
fileName := ""
|
||||
if len(tasks[i].StatusInfo.Files) > 0 {
|
||||
fileName = path.Base(tasks[i].StatusInfo.Files[0].Path)
|
||||
}
|
||||
|
||||
// 过滤敏感信息
|
||||
tasks[i].StatusInfo.Dir = ""
|
||||
for i2 := 0; i2 < len(tasks[i].StatusInfo.Files); i2++ {
|
||||
tasks[i].StatusInfo.Files[i2].Path = path.Base(tasks[i].StatusInfo.Files[i2].Path)
|
||||
}
|
||||
|
||||
interval := 10
|
||||
if actualInterval, ok := intervals[tasks[i].ID]; ok {
|
||||
interval = actualInterval
|
||||
}
|
||||
|
||||
resp = append(resp, DownloadListResponse{
|
||||
UpdateTime: tasks[i].UpdatedAt,
|
||||
UpdateInterval: interval,
|
||||
Name: fileName,
|
||||
Status: tasks[i].Status,
|
||||
Dst: tasks[i].Dst,
|
||||
Total: tasks[i].TotalSize,
|
||||
Downloaded: tasks[i].DownloadedSize,
|
||||
Speed: tasks[i].Speed,
|
||||
Info: tasks[i].StatusInfo,
|
||||
NodeName: tasks[i].NodeName,
|
||||
})
|
||||
}
|
||||
|
||||
return Response{Data: resp}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/rpc"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuildFinishedListResponse(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
tasks := []model.Download{
|
||||
{
|
||||
StatusInfo: rpc.StatusInfo{
|
||||
Files: []rpc.FileInfo{
|
||||
{
|
||||
Path: "/file/name.txt",
|
||||
},
|
||||
},
|
||||
},
|
||||
Task: &model.Task{
|
||||
Model: gorm.Model{},
|
||||
Error: "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
StatusInfo: rpc.StatusInfo{
|
||||
Files: []rpc.FileInfo{
|
||||
{
|
||||
Path: "/file/name1.txt",
|
||||
},
|
||||
{
|
||||
Path: "/file/name2.txt",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
tasks[1].StatusInfo.BitTorrent.Info.Name = "name.txt"
|
||||
res := BuildFinishedListResponse(tasks).Data.([]FinishedListResponse)
|
||||
asserts.Len(res, 2)
|
||||
asserts.Equal("name.txt", res[1].Name)
|
||||
asserts.Equal("name.txt", res[0].Name)
|
||||
asserts.Equal("name.txt", res[0].Files[0].Path)
|
||||
asserts.Equal("name1.txt", res[1].Files[0].Path)
|
||||
asserts.Equal("name2.txt", res[1].Files[1].Path)
|
||||
asserts.EqualValues(0, res[0].TaskStatus)
|
||||
asserts.Equal("error", res[0].TaskError)
|
||||
}
|
||||
|
||||
func TestBuildDownloadingResponse(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
cache.Set("setting_aria2_interval", "10", 0)
|
||||
tasks := []model.Download{
|
||||
{
|
||||
StatusInfo: rpc.StatusInfo{
|
||||
Files: []rpc.FileInfo{
|
||||
{
|
||||
Path: "/file/name.txt",
|
||||
},
|
||||
},
|
||||
},
|
||||
Task: &model.Task{
|
||||
Model: gorm.Model{},
|
||||
Error: "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
StatusInfo: rpc.StatusInfo{
|
||||
Files: []rpc.FileInfo{
|
||||
{
|
||||
Path: "/file/name1.txt",
|
||||
},
|
||||
{
|
||||
Path: "/file/name2.txt",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
tasks[1].StatusInfo.BitTorrent.Info.Name = "name.txt"
|
||||
tasks[1].ID = 1
|
||||
|
||||
res := BuildDownloadingResponse(tasks, map[uint]int{1: 5}).Data.([]DownloadListResponse)
|
||||
asserts.Len(res, 2)
|
||||
asserts.Equal("name1.txt", res[1].Name)
|
||||
asserts.Equal(5, res[1].UpdateInterval)
|
||||
asserts.Equal("name.txt", res[0].Name)
|
||||
asserts.Equal("name.txt", res[0].Info.Files[0].Path)
|
||||
asserts.Equal("name1.txt", res[1].Info.Files[0].Path)
|
||||
asserts.Equal("name2.txt", res[1].Info.Files[1].Path)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewRequestSignString(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
|
||||
sign := NewRequestSignString("1", "2", "3")
|
||||
asserts.NotEmpty(sign)
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/lock"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// AppError 应用错误,实现了error接口
|
||||
@@ -32,15 +39,33 @@ func NewErrorFromResponse(resp *Response) AppError {
|
||||
|
||||
// WithError 将应用error携带标准库中的error
|
||||
func (err *AppError) WithError(raw error) AppError {
|
||||
err.RawError = raw
|
||||
return *err
|
||||
return AppError{
|
||||
Code: err.Code,
|
||||
Msg: err.Msg,
|
||||
RawError: raw,
|
||||
}
|
||||
}
|
||||
|
||||
// Error 返回业务代码确定的可读错误信息
|
||||
func (err AppError) Error() string {
|
||||
if err.RawError != nil {
|
||||
return fmt.Sprintf("%s: %s", err.Msg, err.RawError.Error())
|
||||
}
|
||||
return err.Msg
|
||||
}
|
||||
|
||||
func (err AppError) ErrCode() int {
|
||||
var inheritedErr AppError
|
||||
if errors.As(err.RawError, &inheritedErr) {
|
||||
return inheritedErr.ErrCode()
|
||||
}
|
||||
return err.Code
|
||||
}
|
||||
|
||||
func (err AppError) Unwrap() error {
|
||||
return err.RawError
|
||||
}
|
||||
|
||||
// 三位数错误编码为复用http原本含义
|
||||
// 五位数错误编码为应用自定义错误
|
||||
// 五开头的五位数错误编码为服务器端错误,比如数据库操作失败
|
||||
@@ -78,6 +103,8 @@ const (
|
||||
CodeInvalidChunkIndex = 40012
|
||||
// CodeInvalidContentLength 无效的正文长度
|
||||
CodeInvalidContentLength = 40013
|
||||
// CodePhoneRequired 未绑定手机
|
||||
CodePhoneRequired = 40010
|
||||
// CodeBatchSourceSize 超出批量获取外链限制
|
||||
CodeBatchSourceSize = 40014
|
||||
// CodeBatchAria2Size 超出最大 Aria2 任务数量限制
|
||||
@@ -112,6 +139,8 @@ const (
|
||||
CodeInvalidTempLink = 40029
|
||||
// CodeTempLinkExpired 临时链接过期
|
||||
CodeTempLinkExpired = 40030
|
||||
// CodeEmailProviderBaned 邮箱后缀被禁用
|
||||
CodeEmailProviderBaned = 40031
|
||||
// CodeEmailExisted 邮箱已被使用
|
||||
CodeEmailExisted = 40032
|
||||
// CodeEmailSent 邮箱已重新发送
|
||||
@@ -180,18 +209,50 @@ const (
|
||||
CodeGroupInvalid = 40064
|
||||
// 兑换码无效
|
||||
CodeInvalidGiftCode = 40065
|
||||
// 已绑定了QQ账号
|
||||
CodeQQBindConflict = 40066
|
||||
// QQ账号已被绑定其他账号
|
||||
CodeQQBindOtherAccount = 40067
|
||||
// QQ 未绑定对应账号
|
||||
CodeQQNotLinked = 40068
|
||||
// 已绑定了对应账号
|
||||
CodeOpenIDBindConflict = 40066
|
||||
// 对应账号已被绑定其他账号
|
||||
CodeOpenIDBindOtherAccount = 40067
|
||||
// 未绑定对应账号
|
||||
CodeOpenIDNotLinked = 40068
|
||||
// 密码不正确
|
||||
CodeIncorrectPassword = 40069
|
||||
// 分享无法预览
|
||||
CodeDisabledSharePreview = 40070
|
||||
// 签名无效
|
||||
CodeInvalidSign = 40071
|
||||
// 管理员无法购买用户组
|
||||
CodeFulfillAdminGroup = 40072
|
||||
// Lock confliced
|
||||
CodeLockConflict = 40073
|
||||
// Too many uris
|
||||
CodeTooManyUris = 40074
|
||||
// Lock token expired
|
||||
CodeLockExpired = 40075
|
||||
// Current updated version is stale
|
||||
CodeStaleVersion = 40076
|
||||
// CodeEntityNotExist Entity not exist
|
||||
CodeEntityNotExist = 40077
|
||||
// CodeFileDeleted File is deleted in recycle bin
|
||||
CodeFileDeleted = 40078
|
||||
// CodeFileCountLimitedReached file count limited reached
|
||||
CodeFileCountLimitedReached = 40079
|
||||
// CodeInvalidPassword invalid password
|
||||
CodeInvalidPassword = 40080
|
||||
// CodeBatchOperationNotFullyCompleted batch operation not fully completed
|
||||
CodeBatchOperationNotFullyCompleted = 40081
|
||||
// CodeOwnerOnly owner operation only
|
||||
CodeOwnerOnly = 40082
|
||||
// CodePurchaseRequired purchase required
|
||||
CodePurchaseRequired = 40083
|
||||
// CodeManagedAccountMinimumOpenID managed account minimum openid
|
||||
CodeManagedAccountMinimumOpenID = 40084
|
||||
// CodeAmountTooSmall amount too small
|
||||
CodeAmountTooSmall = 40085
|
||||
// CodeNodeUsedByStoragePolicy node used by storage policy
|
||||
CodeNodeUsedByStoragePolicy = 40086
|
||||
// CodeDomainNotLicensed domain not licensed
|
||||
CodeDomainNotLicensed = 40087
|
||||
// CodeDBError 数据库操作失败
|
||||
CodeDBError = 50001
|
||||
// CodeEncryptError 加密失败
|
||||
@@ -218,24 +279,41 @@ const (
|
||||
CodeNotSet = -1
|
||||
)
|
||||
|
||||
// DBErr 数据库操作失败
|
||||
func DBErr(msg string, err error) Response {
|
||||
// DBErrDeprecated 数据库操作失败
|
||||
func DBErr(c context.Context, msg string, err error) Response {
|
||||
if msg == "" {
|
||||
msg = "Database operation failed."
|
||||
}
|
||||
return Err(CodeDBError, msg, err)
|
||||
return ErrWithDetails(c, CodeDBError, msg, err)
|
||||
}
|
||||
|
||||
// DBErrDeprecated 数据库操作失败
|
||||
func DBErrDeprecated(msg string, err error) Response {
|
||||
if msg == "" {
|
||||
msg = "Database operation failed."
|
||||
}
|
||||
return ErrDeprecated(CodeDBError, msg, err)
|
||||
}
|
||||
|
||||
// ParamErr 各种参数错误
|
||||
func ParamErr(msg string, err error) Response {
|
||||
func ParamErr(c context.Context, msg string, err error) Response {
|
||||
if msg == "" {
|
||||
msg = "Invalid parameters."
|
||||
}
|
||||
return Err(CodeParamErr, msg, err)
|
||||
return ErrWithDetails(c, CodeParamErr, msg, err)
|
||||
}
|
||||
|
||||
// Err 通用错误处理
|
||||
func Err(errCode int, msg string, err error) Response {
|
||||
// ParamErrDeprecated 各种参数错误
|
||||
// Deprecated
|
||||
func ParamErrDeprecated(msg string, err error) Response {
|
||||
if msg == "" {
|
||||
msg = "Invalid parameters."
|
||||
}
|
||||
return ErrDeprecated(CodeParamErr, msg, err)
|
||||
}
|
||||
|
||||
// ErrDeprecated 通用错误处理
|
||||
func ErrDeprecated(errCode int, msg string, err error) Response {
|
||||
// 底层错误是AppError,则尝试从AppError中获取详细信息
|
||||
var appError AppError
|
||||
if errors.As(err, &appError) {
|
||||
@@ -254,3 +332,131 @@ func Err(errCode int, msg string, err error) Response {
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// ErrWithDetails 通用错误处理
|
||||
func ErrWithDetails(c context.Context, errCode int, msg string, err error) Response {
|
||||
res := Response{
|
||||
Code: errCode,
|
||||
Msg: msg,
|
||||
CorrelationID: logging.CorrelationID(c).String(),
|
||||
}
|
||||
|
||||
// 底层错误是AppError,则尝试从AppError中获取详细信息
|
||||
var appError AppError
|
||||
if errors.As(err, &appError) {
|
||||
res.Code = appError.ErrCode()
|
||||
err = appError.RawError
|
||||
res.Msg = appError.Msg
|
||||
|
||||
// Special case for error with detail data
|
||||
switch res.Code {
|
||||
case CodeLockConflict:
|
||||
var lockConflict lock.ConflictError
|
||||
if errors.As(err, &lockConflict) {
|
||||
res.Data = lockConflict
|
||||
}
|
||||
case CodeBatchOperationNotFullyCompleted:
|
||||
var errs *AggregateError
|
||||
if errors.As(err, &errs) {
|
||||
res.AggregatedError = errs.Expand(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生产环境隐藏底层报错
|
||||
if err != nil && gin.Mode() != gin.ReleaseMode {
|
||||
res.Error = err.Error()
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// Err Builds error response without addition details, code and message will
|
||||
// be retrieved from error if possible
|
||||
func Err(c context.Context, err error) Response {
|
||||
return ErrWithDetails(c, CodeNotSet, "", err)
|
||||
}
|
||||
|
||||
// AggregateError is a special error type that contains multiple errors
|
||||
type AggregateError struct {
|
||||
errs map[string]error
|
||||
}
|
||||
|
||||
// NewAggregateError creates a new AggregateError
|
||||
func NewAggregateError() *AggregateError {
|
||||
return &AggregateError{
|
||||
errs: make(map[string]error, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *AggregateError) Error() string {
|
||||
return fmt.Sprintf("aggregate error: one or more operation failed")
|
||||
}
|
||||
|
||||
// Add adds an error to the aggregate
|
||||
func (e *AggregateError) Add(id string, err error) {
|
||||
e.errs[id] = err
|
||||
}
|
||||
|
||||
// Merge merges another aggregate error into this one
|
||||
func (e *AggregateError) Merge(err error) bool {
|
||||
var errs *AggregateError
|
||||
if errors.As(err, &errs) {
|
||||
for id, err := range errs.errs {
|
||||
e.errs[id] = err
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Raw returns the raw error map
|
||||
func (e *AggregateError) Raw() map[string]error {
|
||||
return e.errs
|
||||
}
|
||||
|
||||
func (e *AggregateError) Remove(id string) {
|
||||
delete(e.errs, id)
|
||||
}
|
||||
|
||||
// Expand expands the aggregate error into a list of responses
|
||||
func (e *AggregateError) Expand(ctx context.Context) map[string]Response {
|
||||
return lo.MapEntries(e.errs, func(id string, err error) (string, Response) {
|
||||
return id, Err(ctx, err)
|
||||
})
|
||||
}
|
||||
|
||||
// Aggregate aggregates the error and returns nil if there is no error;
|
||||
// otherwise returns the error itself
|
||||
func (e *AggregateError) Aggregate() error {
|
||||
if len(e.errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
msg := "One or more operation failed"
|
||||
if len(e.errs) == 1 {
|
||||
for _, err := range e.errs {
|
||||
msg = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
return NewError(CodeBatchOperationNotFullyCompleted, msg, e)
|
||||
}
|
||||
|
||||
func (e *AggregateError) FormatFirstN(n int) string {
|
||||
if len(e.errs) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
res := make([]string, 0, n)
|
||||
for id, err := range e.errs {
|
||||
res = append(res, fmt.Sprintf("%s: %s", id, err.Error()))
|
||||
if len(res) >= n {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(res, ", ")
|
||||
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewError(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
err := NewError(400, "Bad Request", errors.New("error"))
|
||||
a.Error(err)
|
||||
a.EqualValues(400, err.Code)
|
||||
|
||||
err.WithError(errors.New("error2"))
|
||||
a.Equal("error2", err.RawError.Error())
|
||||
a.Equal("Bad Request", err.Error())
|
||||
|
||||
resp := &Response{
|
||||
Code: 400,
|
||||
Msg: "Bad Request",
|
||||
Error: "error",
|
||||
}
|
||||
err = NewErrorFromResponse(resp)
|
||||
a.Error(err)
|
||||
}
|
||||
|
||||
func TestDBErr(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
resp := DBErr("", nil)
|
||||
a.NotEmpty(resp.Msg)
|
||||
|
||||
resp = ParamErr("", nil)
|
||||
a.NotEmpty(resp.Msg)
|
||||
}
|
||||
|
||||
func TestErr(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
err := NewError(400, "Bad Request", errors.New("error"))
|
||||
resp := Err(400, "", err)
|
||||
a.Equal("Bad Request", resp.Msg)
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register(ObjectProps{})
|
||||
}
|
||||
|
||||
// ObjectProps 文件、目录对象的详细属性信息
|
||||
type ObjectProps struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Policy string `json:"policy"`
|
||||
Size uint64 `json:"size"`
|
||||
ChildFolderNum int `json:"child_folder_num"`
|
||||
ChildFileNum int `json:"child_file_num"`
|
||||
Path string `json:"path"`
|
||||
|
||||
QueryDate time.Time `json:"query_date"`
|
||||
}
|
||||
|
||||
// ObjectList 文件、目录列表
|
||||
type ObjectList struct {
|
||||
Parent string `json:"parent,omitempty"`
|
||||
Objects []Object `json:"objects"`
|
||||
Policy *PolicySummary `json:"policy,omitempty"`
|
||||
}
|
||||
|
||||
// Object 文件或者目录
|
||||
type Object struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Thumb bool `json:"thumb"`
|
||||
Size uint64 `json:"size"`
|
||||
Type string `json:"type"`
|
||||
Date time.Time `json:"date"`
|
||||
CreateDate time.Time `json:"create_date"`
|
||||
Key string `json:"key,omitempty"`
|
||||
SourceEnabled bool `json:"source_enabled"`
|
||||
}
|
||||
|
||||
// PolicySummary 用于前端组件使用的存储策略概况
|
||||
type PolicySummary struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
MaxSize uint64 `json:"max_size"`
|
||||
FileType []string `json:"file_type"`
|
||||
}
|
||||
|
||||
// BuildObjectList 构建列目录响应
|
||||
func BuildObjectList(parent uint, objects []Object, policy *model.Policy) ObjectList {
|
||||
res := ObjectList{
|
||||
Objects: objects,
|
||||
}
|
||||
|
||||
if parent > 0 {
|
||||
res.Parent = hashid.HashID(parent, hashid.FolderID)
|
||||
}
|
||||
|
||||
if policy != nil {
|
||||
res.Policy = &PolicySummary{
|
||||
ID: hashid.HashID(policy.ID, hashid.PolicyID),
|
||||
Name: policy.Name,
|
||||
Type: policy.Type,
|
||||
MaxSize: policy.MaxSize,
|
||||
FileType: policy.OptionsSerialized.FileType,
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// Sources 获取外链的结果响应
|
||||
type Sources struct {
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
Parent uint `json:"parent"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// DocPreviewSession 文档预览会话响应
|
||||
type DocPreviewSession struct {
|
||||
URL string `json:"url"`
|
||||
AccessToken string `json:"access_token,omitempty"`
|
||||
AccessTokenTTL int64 `json:"access_token_ttl,omitempty"`
|
||||
}
|
||||
|
||||
// WopiFileInfo Response for `CheckFileInfo`
|
||||
type WopiFileInfo struct {
|
||||
// Required
|
||||
BaseFileName string
|
||||
Version string
|
||||
Size int64
|
||||
|
||||
// Breadcrumb
|
||||
BreadcrumbBrandName string
|
||||
BreadcrumbBrandUrl string
|
||||
BreadcrumbFolderName string
|
||||
BreadcrumbFolderUrl string
|
||||
|
||||
// Post Message
|
||||
FileSharingPostMessage bool
|
||||
ClosePostMessage bool
|
||||
PostMessageOrigin string
|
||||
|
||||
// Other miscellaneous properties
|
||||
FileNameMaxLength int
|
||||
LastModifiedTime string
|
||||
|
||||
// User metadata
|
||||
IsAnonymousUser bool
|
||||
UserFriendlyName string
|
||||
UserId string
|
||||
OwnerId string
|
||||
|
||||
// Permission
|
||||
ReadOnly bool
|
||||
UserCanRename bool
|
||||
UserCanReview bool
|
||||
UserCanWrite bool
|
||||
|
||||
SupportsRename bool
|
||||
SupportsReviewing bool
|
||||
SupportsUpdate bool
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildObjectList(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
res := BuildObjectList(1, []Object{{}, {}}, &model.Policy{})
|
||||
a.NotEmpty(res.Parent)
|
||||
a.NotNil(res.Policy)
|
||||
a.Len(res.Objects, 2)
|
||||
}
|
||||
@@ -2,24 +2,27 @@ package serializer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
)
|
||||
|
||||
// Response 基础序列化器
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Msg string `json:"msg"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Code int `json:"code"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
AggregatedError interface{} `json:"aggregated_error,omitempty"`
|
||||
Msg string `json:"msg"`
|
||||
Error string `json:"error,omitempty"`
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
}
|
||||
|
||||
// NewResponseWithGobData 返回Data字段使用gob编码的Response
|
||||
func NewResponseWithGobData(data interface{}) Response {
|
||||
func NewResponseWithGobData(c context.Context, data interface{}) Response {
|
||||
var w bytes.Buffer
|
||||
encoder := gob.NewEncoder(&w)
|
||||
if err := encoder.Encode(data); err != nil {
|
||||
return Err(CodeInternalSetting, "Failed to encode response content", err)
|
||||
return ErrWithDetails(c, CodeInternalSetting, "Failed to encode response content", err)
|
||||
}
|
||||
|
||||
return Response{Data: w.Bytes()}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewResponseWithGobData(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
type args struct {
|
||||
data interface{}
|
||||
}
|
||||
|
||||
res := NewResponseWithGobData(args{})
|
||||
a.Equal(CodeInternalSetting, res.Code)
|
||||
|
||||
res = NewResponseWithGobData("TestNewResponseWithGobData")
|
||||
a.Equal(0, res.Code)
|
||||
a.NotEmpty(res.Data)
|
||||
}
|
||||
|
||||
func TestResponse_GobDecode(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
res := NewResponseWithGobData("TestResponse_GobDecode")
|
||||
jsonContent, err := json.Marshal(res)
|
||||
a.NoError(err)
|
||||
resDecoded := &Response{}
|
||||
a.NoError(json.Unmarshal(jsonContent, resDecoded))
|
||||
var target string
|
||||
resDecoded.GobDecode(&target)
|
||||
a.Equal("TestResponse_GobDecode", target)
|
||||
}
|
||||
@@ -1,92 +1,7 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SiteConfig 站点全局设置序列
|
||||
type SiteConfig struct {
|
||||
SiteName string `json:"title"`
|
||||
LoginCaptcha bool `json:"loginCaptcha"`
|
||||
RegCaptcha bool `json:"regCaptcha"`
|
||||
ForgetCaptcha bool `json:"forgetCaptcha"`
|
||||
EmailActive bool `json:"emailActive"`
|
||||
Themes string `json:"themes"`
|
||||
DefaultTheme string `json:"defaultTheme"`
|
||||
HomepageViewMethod string `json:"home_view_method"`
|
||||
ShareViewMethod string `json:"share_view_method"`
|
||||
Authn bool `json:"authn"`
|
||||
User User `json:"user"`
|
||||
ReCaptchaKey string `json:"captcha_ReCaptchaKey"`
|
||||
CaptchaType string `json:"captcha_type"`
|
||||
TCaptchaCaptchaAppId string `json:"tcaptcha_captcha_app_id"`
|
||||
RegisterEnabled bool `json:"registerEnabled"`
|
||||
AppPromotion bool `json:"app_promotion"`
|
||||
WopiExts []string `json:"wopi_exts"`
|
||||
}
|
||||
|
||||
type task struct {
|
||||
Status int `json:"status"`
|
||||
Type int `json:"type"`
|
||||
CreateDate time.Time `json:"create_date"`
|
||||
Progress int `json:"progress"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// BuildTaskList 构建任务列表响应
|
||||
func BuildTaskList(tasks []model.Task, total int) Response {
|
||||
res := make([]task, 0, len(tasks))
|
||||
for _, t := range tasks {
|
||||
res = append(res, task{
|
||||
Status: t.Status,
|
||||
Type: t.Type,
|
||||
CreateDate: t.CreatedAt,
|
||||
Progress: t.Progress,
|
||||
Error: t.Error,
|
||||
})
|
||||
}
|
||||
|
||||
return Response{Data: map[string]interface{}{
|
||||
"total": total,
|
||||
"tasks": res,
|
||||
}}
|
||||
}
|
||||
|
||||
func checkSettingValue(setting map[string]string, key string) string {
|
||||
if v, ok := setting[key]; ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// BuildSiteConfig 站点全局设置
|
||||
func BuildSiteConfig(settings map[string]string, user *model.User, wopiExts []string) Response {
|
||||
var userRes User
|
||||
if user != nil {
|
||||
userRes = BuildUser(*user)
|
||||
} else {
|
||||
userRes = BuildUser(*model.NewAnonymousUser())
|
||||
}
|
||||
res := Response{
|
||||
Data: SiteConfig{
|
||||
SiteName: checkSettingValue(settings, "siteName"),
|
||||
LoginCaptcha: model.IsTrueVal(checkSettingValue(settings, "login_captcha")),
|
||||
RegCaptcha: model.IsTrueVal(checkSettingValue(settings, "reg_captcha")),
|
||||
ForgetCaptcha: model.IsTrueVal(checkSettingValue(settings, "forget_captcha")),
|
||||
EmailActive: model.IsTrueVal(checkSettingValue(settings, "email_active")),
|
||||
Themes: checkSettingValue(settings, "themes"),
|
||||
DefaultTheme: checkSettingValue(settings, "defaultTheme"),
|
||||
HomepageViewMethod: checkSettingValue(settings, "home_view_method"),
|
||||
ShareViewMethod: checkSettingValue(settings, "share_view_method"),
|
||||
Authn: model.IsTrueVal(checkSettingValue(settings, "authn_enabled")),
|
||||
User: userRes,
|
||||
ReCaptchaKey: checkSettingValue(settings, "captcha_ReCaptchaKey"),
|
||||
CaptchaType: checkSettingValue(settings, "captcha_type"),
|
||||
TCaptchaCaptchaAppId: checkSettingValue(settings, "captcha_TCaptcha_CaptchaAppId"),
|
||||
RegisterEnabled: model.IsTrueVal(checkSettingValue(settings, "register_enabled")),
|
||||
AppPromotion: model.IsTrueVal(checkSettingValue(settings, "show_app_promotion")),
|
||||
WopiExts: wopiExts,
|
||||
}}
|
||||
return res
|
||||
// VolResponse VOL query response
|
||||
type VolResponse struct {
|
||||
Signature string `json:"signature"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCheckSettingValue(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
|
||||
asserts.Equal("", checkSettingValue(map[string]string{}, "key"))
|
||||
asserts.Equal("123", checkSettingValue(map[string]string{"key": "123"}, "key"))
|
||||
}
|
||||
|
||||
func TestBuildSiteConfig(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
|
||||
res := BuildSiteConfig(map[string]string{"not exist": ""}, &model.User{}, nil)
|
||||
asserts.Equal("", res.Data.(SiteConfig).SiteName)
|
||||
|
||||
res = BuildSiteConfig(map[string]string{"siteName": "123"}, &model.User{}, nil)
|
||||
asserts.Equal("123", res.Data.(SiteConfig).SiteName)
|
||||
|
||||
// 非空用户
|
||||
res = BuildSiteConfig(map[string]string{"qq_login": "1"}, &model.User{
|
||||
Model: gorm.Model{
|
||||
ID: 5,
|
||||
},
|
||||
}, nil)
|
||||
asserts.Len(res.Data.(SiteConfig).User.ID, 4)
|
||||
}
|
||||
|
||||
func TestBuildTaskList(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
tasks := []model.Task{{}}
|
||||
|
||||
res := BuildTaskList(tasks, 1)
|
||||
asserts.NotNil(res)
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
)
|
||||
|
||||
// Share 分享信息序列化
|
||||
type Share struct {
|
||||
Key string `json:"key"`
|
||||
Locked bool `json:"locked"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
CreateDate time.Time `json:"create_date,omitempty"`
|
||||
Downloads int `json:"downloads"`
|
||||
Views int `json:"views"`
|
||||
Expire int64 `json:"expire"`
|
||||
Preview bool `json:"preview"`
|
||||
Creator *shareCreator `json:"creator,omitempty"`
|
||||
Source *shareSource `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
type shareCreator struct {
|
||||
Key string `json:"key"`
|
||||
Nick string `json:"nick"`
|
||||
GroupName string `json:"group_name"`
|
||||
}
|
||||
|
||||
type shareSource struct {
|
||||
Name string `json:"name"`
|
||||
Size uint64 `json:"size"`
|
||||
}
|
||||
|
||||
// myShareItem 我的分享列表条目
|
||||
type myShareItem struct {
|
||||
Key string `json:"key"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
Password string `json:"password"`
|
||||
CreateDate time.Time `json:"create_date,omitempty"`
|
||||
Downloads int `json:"downloads"`
|
||||
RemainDownloads int `json:"remain_downloads"`
|
||||
Views int `json:"views"`
|
||||
Expire int64 `json:"expire"`
|
||||
Preview bool `json:"preview"`
|
||||
Source *shareSource `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
// BuildShareList 构建我的分享列表响应
|
||||
func BuildShareList(shares []model.Share, total int) Response {
|
||||
res := make([]myShareItem, 0, total)
|
||||
now := time.Now().Unix()
|
||||
for i := 0; i < len(shares); i++ {
|
||||
item := myShareItem{
|
||||
Key: hashid.HashID(shares[i].ID, hashid.ShareID),
|
||||
IsDir: shares[i].IsDir,
|
||||
Password: shares[i].Password,
|
||||
CreateDate: shares[i].CreatedAt,
|
||||
Downloads: shares[i].Downloads,
|
||||
Views: shares[i].Views,
|
||||
Preview: shares[i].PreviewEnabled,
|
||||
Expire: -1,
|
||||
RemainDownloads: shares[i].RemainDownloads,
|
||||
}
|
||||
if shares[i].Expires != nil {
|
||||
item.Expire = shares[i].Expires.Unix() - now
|
||||
if item.Expire == 0 {
|
||||
item.Expire = 0
|
||||
}
|
||||
}
|
||||
if shares[i].File.ID != 0 {
|
||||
item.Source = &shareSource{
|
||||
Name: shares[i].File.Name,
|
||||
Size: shares[i].File.Size,
|
||||
}
|
||||
} else if shares[i].Folder.ID != 0 {
|
||||
item.Source = &shareSource{
|
||||
Name: shares[i].Folder.Name,
|
||||
}
|
||||
}
|
||||
|
||||
res = append(res, item)
|
||||
}
|
||||
|
||||
return Response{Data: map[string]interface{}{
|
||||
"total": total,
|
||||
"items": res,
|
||||
}}
|
||||
}
|
||||
|
||||
// BuildShareResponse 构建获取分享信息响应
|
||||
func BuildShareResponse(share *model.Share, unlocked bool) Share {
|
||||
creator := share.Creator()
|
||||
resp := Share{
|
||||
Key: hashid.HashID(share.ID, hashid.ShareID),
|
||||
Locked: !unlocked,
|
||||
Creator: &shareCreator{
|
||||
Key: hashid.HashID(creator.ID, hashid.UserID),
|
||||
Nick: creator.Nick,
|
||||
GroupName: creator.Group.Name,
|
||||
},
|
||||
CreateDate: share.CreatedAt,
|
||||
}
|
||||
|
||||
// 未解锁时只返回基本信息
|
||||
if !unlocked {
|
||||
return resp
|
||||
}
|
||||
|
||||
resp.IsDir = share.IsDir
|
||||
resp.Downloads = share.Downloads
|
||||
resp.Views = share.Views
|
||||
resp.Preview = share.PreviewEnabled
|
||||
|
||||
if share.Expires != nil {
|
||||
resp.Expire = share.Expires.Unix() - time.Now().Unix()
|
||||
}
|
||||
|
||||
if share.IsDir {
|
||||
source := share.SourceFolder()
|
||||
resp.Source = &shareSource{
|
||||
Name: source.Name,
|
||||
Size: 0,
|
||||
}
|
||||
} else {
|
||||
source := share.SourceFile()
|
||||
resp.Source = &shareSource{
|
||||
Name: source.Name,
|
||||
Size: source.Size,
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuildShareList(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
timeNow := time.Now()
|
||||
|
||||
shares := []model.Share{
|
||||
{
|
||||
Expires: &timeNow,
|
||||
File: model.File{
|
||||
Model: gorm.Model{ID: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
Folder: model.Folder{
|
||||
Model: gorm.Model{ID: 1},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res := BuildShareList(shares, 2)
|
||||
asserts.Equal(0, res.Code)
|
||||
}
|
||||
|
||||
func TestBuildShareResponse(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
|
||||
// 未解锁
|
||||
{
|
||||
share := &model.Share{
|
||||
User: model.User{Model: gorm.Model{ID: 1}},
|
||||
Downloads: 1,
|
||||
}
|
||||
res := BuildShareResponse(share, false)
|
||||
asserts.EqualValues(0, res.Downloads)
|
||||
asserts.True(res.Locked)
|
||||
asserts.NotNil(res.Creator)
|
||||
}
|
||||
|
||||
// 已解锁,非目录
|
||||
{
|
||||
expires := time.Now().Add(time.Duration(10) * time.Second)
|
||||
share := &model.Share{
|
||||
User: model.User{Model: gorm.Model{ID: 1}},
|
||||
Downloads: 1,
|
||||
Expires: &expires,
|
||||
File: model.File{
|
||||
Model: gorm.Model{ID: 1},
|
||||
},
|
||||
}
|
||||
res := BuildShareResponse(share, true)
|
||||
asserts.EqualValues(1, res.Downloads)
|
||||
asserts.False(res.Locked)
|
||||
asserts.NotEmpty(res.Expire)
|
||||
asserts.NotNil(res.Creator)
|
||||
}
|
||||
|
||||
// 已解锁,是目录
|
||||
{
|
||||
expires := time.Now().Add(time.Duration(10) * time.Second)
|
||||
share := &model.Share{
|
||||
User: model.User{Model: gorm.Model{ID: 1}},
|
||||
Downloads: 1,
|
||||
Expires: &expires,
|
||||
Folder: model.Folder{
|
||||
Model: gorm.Model{ID: 1},
|
||||
},
|
||||
IsDir: true,
|
||||
}
|
||||
res := BuildShareResponse(share, true)
|
||||
asserts.EqualValues(1, res.Downloads)
|
||||
asserts.False(res.Locked)
|
||||
asserts.NotEmpty(res.Expire)
|
||||
asserts.NotNil(res.Creator)
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
)
|
||||
|
||||
// RemoteDeleteRequest 远程策略删除接口请求正文
|
||||
type RemoteDeleteRequest struct {
|
||||
Files []string `json:"files"`
|
||||
}
|
||||
|
||||
// ListRequest 远程策略列文件请求正文
|
||||
type ListRequest struct {
|
||||
Path string `json:"path"`
|
||||
Recursive bool `json:"recursive"`
|
||||
}
|
||||
|
||||
// NodePingReq 从机节点Ping请求
|
||||
type NodePingReq struct {
|
||||
SiteURL string `json:"site_url"`
|
||||
SiteID string `json:"site_id"`
|
||||
IsUpdate bool `json:"is_update"`
|
||||
CredentialTTL int `json:"credential_ttl"`
|
||||
Node *model.Node `json:"node"`
|
||||
}
|
||||
|
||||
// NodePingResp 从机节点Ping响应
|
||||
type NodePingResp struct {
|
||||
}
|
||||
|
||||
// SlaveAria2Call 从机有关Aria2的请求正文
|
||||
type SlaveAria2Call struct {
|
||||
Task *model.Download `json:"task"`
|
||||
GroupOptions map[string]interface{} `json:"group_options"`
|
||||
Files []int `json:"files"`
|
||||
}
|
||||
|
||||
// SlaveTransferReq 从机中转任务创建请求
|
||||
type SlaveTransferReq struct {
|
||||
Src string `json:"src"`
|
||||
Dst string `json:"dst"`
|
||||
Policy *model.Policy `json:"policy"`
|
||||
}
|
||||
|
||||
// Hash 返回创建请求的唯一标识,保持创建请求幂等
|
||||
func (s *SlaveTransferReq) Hash(id string) string {
|
||||
h := sha1.New()
|
||||
h.Write([]byte(fmt.Sprintf("transfer-%s-%s-%s-%d", id, s.Src, s.Dst, s.Policy.ID)))
|
||||
bs := h.Sum(nil)
|
||||
return fmt.Sprintf("%x", bs)
|
||||
}
|
||||
|
||||
const (
|
||||
SlaveTransferSuccess = "success"
|
||||
SlaveTransferFailed = "failed"
|
||||
)
|
||||
|
||||
type SlaveTransferResult struct {
|
||||
Error string
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register(SlaveTransferResult{})
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSlaveTransferReq_Hash(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
s1 := &SlaveTransferReq{
|
||||
Src: "1",
|
||||
Policy: &model.Policy{},
|
||||
}
|
||||
s2 := &SlaveTransferReq{
|
||||
Src: "2",
|
||||
Policy: &model.Policy{},
|
||||
}
|
||||
a.NotEqual(s1.Hash("1"), s2.Hash("1"))
|
||||
}
|
||||
@@ -1,64 +1,6 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UploadPolicy slave模式下传递的上传策略
|
||||
type UploadPolicy struct {
|
||||
SavePath string `json:"save_path"`
|
||||
FileName string `json:"file_name"`
|
||||
AutoRename bool `json:"auto_rename"`
|
||||
MaxSize uint64 `json:"max_size"`
|
||||
AllowedExtension []string `json:"allowed_extension"`
|
||||
CallbackURL string `json:"callback_url"`
|
||||
}
|
||||
|
||||
// UploadCredential 返回给客户端的上传凭证
|
||||
type UploadCredential struct {
|
||||
SessionID string `json:"sessionID"`
|
||||
ChunkSize uint64 `json:"chunkSize"` // 分块大小,0 为部分快
|
||||
Expires int64 `json:"expires"` // 上传凭证过期时间, Unix 时间戳
|
||||
UploadURLs []string `json:"uploadURLs,omitempty"`
|
||||
Credential string `json:"credential,omitempty"`
|
||||
UploadID string `json:"uploadID,omitempty"`
|
||||
Callback string `json:"callback,omitempty"` // 回调地址
|
||||
Path string `json:"path,omitempty"` // 存储路径
|
||||
AccessKey string `json:"ak,omitempty"`
|
||||
KeyTime string `json:"keyTime,omitempty"` // COS用有效期
|
||||
Policy string `json:"policy,omitempty"`
|
||||
CompleteURL string `json:"completeURL,omitempty"`
|
||||
}
|
||||
|
||||
// UploadSession 上传会话
|
||||
type UploadSession struct {
|
||||
Key string // 上传会话 GUID
|
||||
UID uint // 发起者
|
||||
VirtualPath string // 用户文件路径,不含文件名
|
||||
Name string // 文件名
|
||||
Size uint64 // 文件大小
|
||||
SavePath string // 物理存储路径,包含物理文件名
|
||||
LastModified *time.Time // 可选的文件最后修改日期
|
||||
Policy model.Policy
|
||||
Callback string // 回调 URL 地址
|
||||
CallbackSecret string // 回调 URL
|
||||
UploadURL string
|
||||
UploadID string
|
||||
Credential string
|
||||
}
|
||||
|
||||
// UploadCallback 上传回调正文
|
||||
type UploadCallback struct {
|
||||
PicInfo string `json:"pic_info"`
|
||||
}
|
||||
|
||||
// GeneralUploadCallbackFailed 存储策略上传回调失败响应
|
||||
type GeneralUploadCallbackFailed struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register(UploadSession{})
|
||||
}
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/duo-labs/webauthn/webauthn"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CheckLogin 检查登录
|
||||
func CheckLogin() Response {
|
||||
return Response{
|
||||
Code: CodeCheckLogin,
|
||||
Msg: "Login required",
|
||||
}
|
||||
}
|
||||
|
||||
// User 用户序列化器
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"user_name"`
|
||||
Nickname string `json:"nickname"`
|
||||
Status int `json:"status"`
|
||||
Avatar string `json:"avatar"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PreferredTheme string `json:"preferred_theme"`
|
||||
Anonymous bool `json:"anonymous"`
|
||||
Group group `json:"group"`
|
||||
Tags []tag `json:"tags"`
|
||||
}
|
||||
|
||||
type group struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
AllowShare bool `json:"allowShare"`
|
||||
AllowRemoteDownload bool `json:"allowRemoteDownload"`
|
||||
AllowArchiveDownload bool `json:"allowArchiveDownload"`
|
||||
ShareDownload bool `json:"shareDownload"`
|
||||
CompressEnabled bool `json:"compress"`
|
||||
WebDAVEnabled bool `json:"webdav"`
|
||||
SourceBatchSize int `json:"sourceBatch"`
|
||||
AdvanceDelete bool `json:"advanceDelete"`
|
||||
AllowWebDAVProxy bool `json:"allowWebDAVProxy"`
|
||||
}
|
||||
|
||||
type tag struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Color string `json:"color"`
|
||||
Type int `json:"type"`
|
||||
Expression string `json:"expression"`
|
||||
}
|
||||
|
||||
type storage struct {
|
||||
Used uint64 `json:"used"`
|
||||
Free uint64 `json:"free"`
|
||||
Total uint64 `json:"total"`
|
||||
}
|
||||
|
||||
// WebAuthnCredentials 外部验证器凭证
|
||||
type WebAuthnCredentials struct {
|
||||
ID []byte `json:"id"`
|
||||
FingerPrint string `json:"fingerprint"`
|
||||
}
|
||||
|
||||
// BuildWebAuthnList 构建设置页面凭证列表
|
||||
func BuildWebAuthnList(credentials []webauthn.Credential) []WebAuthnCredentials {
|
||||
res := make([]WebAuthnCredentials, 0, len(credentials))
|
||||
for _, v := range credentials {
|
||||
credential := WebAuthnCredentials{
|
||||
ID: v.ID,
|
||||
FingerPrint: fmt.Sprintf("% X", v.Authenticator.AAGUID),
|
||||
}
|
||||
res = append(res, credential)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// BuildUser 序列化用户
|
||||
func BuildUser(user model.User) User {
|
||||
tags, _ := model.GetTagsByUID(user.ID)
|
||||
return User{
|
||||
ID: hashid.HashID(user.ID, hashid.UserID),
|
||||
Email: user.Email,
|
||||
Nickname: user.Nick,
|
||||
Status: user.Status,
|
||||
Avatar: user.Avatar,
|
||||
CreatedAt: user.CreatedAt,
|
||||
PreferredTheme: user.OptionsSerialized.PreferredTheme,
|
||||
Anonymous: user.IsAnonymous(),
|
||||
Group: group{
|
||||
ID: user.GroupID,
|
||||
Name: user.Group.Name,
|
||||
AllowShare: user.Group.ShareEnabled,
|
||||
AllowRemoteDownload: user.Group.OptionsSerialized.Aria2,
|
||||
AllowArchiveDownload: user.Group.OptionsSerialized.ArchiveDownload,
|
||||
ShareDownload: user.Group.OptionsSerialized.ShareDownload,
|
||||
CompressEnabled: user.Group.OptionsSerialized.ArchiveTask,
|
||||
WebDAVEnabled: user.Group.WebDAVEnabled,
|
||||
AllowWebDAVProxy: user.Group.OptionsSerialized.WebDAVProxy,
|
||||
SourceBatchSize: user.Group.OptionsSerialized.SourceBatchSize,
|
||||
AdvanceDelete: user.Group.OptionsSerialized.AdvanceDelete,
|
||||
},
|
||||
Tags: buildTagRes(tags),
|
||||
}
|
||||
}
|
||||
|
||||
// BuildUserResponse 序列化用户响应
|
||||
func BuildUserResponse(user model.User) Response {
|
||||
return Response{
|
||||
Data: BuildUser(user),
|
||||
}
|
||||
}
|
||||
|
||||
// BuildUserStorageResponse 序列化用户存储概况响应
|
||||
func BuildUserStorageResponse(user model.User) Response {
|
||||
total := user.Group.MaxStorage
|
||||
storageResp := storage{
|
||||
Used: user.Storage,
|
||||
Free: total - user.Storage,
|
||||
Total: total,
|
||||
}
|
||||
|
||||
if total < user.Storage {
|
||||
storageResp.Free = 0
|
||||
}
|
||||
|
||||
return Response{
|
||||
Data: storageResp,
|
||||
}
|
||||
}
|
||||
|
||||
// buildTagRes 构建标签列表
|
||||
func buildTagRes(tags []model.Tag) []tag {
|
||||
res := make([]tag, 0, len(tags))
|
||||
for i := 0; i < len(tags); i++ {
|
||||
newTag := tag{
|
||||
ID: hashid.HashID(tags[i].ID, hashid.TagID),
|
||||
Name: tags[i].Name,
|
||||
Icon: tags[i].Icon,
|
||||
Color: tags[i].Color,
|
||||
Type: tags[i].Type,
|
||||
}
|
||||
if newTag.Type != 0 {
|
||||
newTag.Expression = tags[i].Expression
|
||||
|
||||
}
|
||||
res = append(res, newTag)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
package serializer
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/duo-labs/webauthn/webauthn"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var mock sqlmock.Sqlmock
|
||||
|
||||
// TestMain 初始化数据库Mock
|
||||
func TestMain(m *testing.M) {
|
||||
var db *sql.DB
|
||||
var err error
|
||||
db, mock, err = sqlmock.New()
|
||||
if err != nil {
|
||||
panic("An error was not expected when opening a stub database connection")
|
||||
}
|
||||
model.DB, _ = gorm.Open("mysql", db)
|
||||
defer db.Close()
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func TestBuildUser(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
user := model.User{}
|
||||
mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id"}))
|
||||
res := BuildUser(user)
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
asserts.NotNil(res)
|
||||
|
||||
}
|
||||
|
||||
func TestBuildUserResponse(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
user := model.User{}
|
||||
res := BuildUserResponse(user)
|
||||
asserts.NotNil(res)
|
||||
}
|
||||
|
||||
func TestBuildUserStorageResponse(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
cache.Set("pack_size_0", uint64(0), 0)
|
||||
|
||||
{
|
||||
user := model.User{
|
||||
Storage: 0,
|
||||
Group: model.Group{MaxStorage: 10},
|
||||
}
|
||||
res := BuildUserStorageResponse(user)
|
||||
asserts.Equal(uint64(0), res.Data.(storage).Used)
|
||||
asserts.Equal(uint64(10), res.Data.(storage).Total)
|
||||
asserts.Equal(uint64(10), res.Data.(storage).Free)
|
||||
}
|
||||
{
|
||||
user := model.User{
|
||||
Storage: 6,
|
||||
Group: model.Group{MaxStorage: 10},
|
||||
}
|
||||
res := BuildUserStorageResponse(user)
|
||||
asserts.Equal(uint64(6), res.Data.(storage).Used)
|
||||
asserts.Equal(uint64(10), res.Data.(storage).Total)
|
||||
asserts.Equal(uint64(4), res.Data.(storage).Free)
|
||||
}
|
||||
{
|
||||
user := model.User{
|
||||
Storage: 20,
|
||||
Group: model.Group{MaxStorage: 10},
|
||||
}
|
||||
res := BuildUserStorageResponse(user)
|
||||
asserts.Equal(uint64(20), res.Data.(storage).Used)
|
||||
asserts.Equal(uint64(10), res.Data.(storage).Total)
|
||||
asserts.Equal(uint64(0), res.Data.(storage).Free)
|
||||
}
|
||||
{
|
||||
user := model.User{
|
||||
Storage: 6,
|
||||
Group: model.Group{MaxStorage: 10},
|
||||
}
|
||||
res := BuildUserStorageResponse(user)
|
||||
asserts.Equal(uint64(6), res.Data.(storage).Used)
|
||||
asserts.Equal(uint64(10), res.Data.(storage).Total)
|
||||
asserts.Equal(uint64(4), res.Data.(storage).Free)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTagRes(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
tags := []model.Tag{
|
||||
{
|
||||
Type: 0,
|
||||
Expression: "exp",
|
||||
},
|
||||
{
|
||||
Type: 1,
|
||||
Expression: "exp",
|
||||
},
|
||||
}
|
||||
res := buildTagRes(tags)
|
||||
asserts.Len(res, 2)
|
||||
asserts.Equal("", res[0].Expression)
|
||||
asserts.Equal("exp", res[1].Expression)
|
||||
}
|
||||
|
||||
func TestBuildWebAuthnList(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
credentials := []webauthn.Credential{{}}
|
||||
res := BuildWebAuthnList(credentials)
|
||||
asserts.Len(res, 1)
|
||||
}
|
||||
Reference in New Issue
Block a user