Init V4 community edition (#2265)

* Init V4 community edition

* Init V4 community edition
This commit is contained in:
AaronLiu
2025-04-20 17:31:25 +08:00
committed by GitHub
parent da4e44b77a
commit 21d158db07
597 changed files with 119415 additions and 41692 deletions

View File

@@ -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}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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, ", ")
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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()}

View File

@@ -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)
}

View File

@@ -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"`
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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{})
}

View File

@@ -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"))
}

View File

@@ -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{})
}

View File

@@ -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
}

View File

@@ -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)
}