This commit is contained in:
2024-02-25 08:30:34 +08:00
commit 4947f39e74
273 changed files with 45396 additions and 0 deletions

117
pkg/serializer/aria2.go Normal file
View File

@ -0,0 +1,117 @@
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}
}

21
pkg/serializer/auth.go Normal file
View File

@ -0,0 +1,21 @@
package serializer
import "encoding/json"
// RequestRawSign 待签名的HTTP请求
type RequestRawSign struct {
Path string
Header string
Body string
}
// NewRequestSignString 返回JSON格式的待签名字符串
func NewRequestSignString(path, header, body string) string {
req := RequestRawSign{
Path: path,
Header: header,
Body: body,
}
res, _ := json.Marshal(req)
return string(res)
}

262
pkg/serializer/error.go Normal file
View File

@ -0,0 +1,262 @@
package serializer
import (
"errors"
"github.com/gin-gonic/gin"
)
// AppError 应用错误实现了error接口
type AppError struct {
Code int
Msg string
RawError error
}
// NewError 返回新的错误对象
func NewError(code int, msg string, err error) AppError {
return AppError{
Code: code,
Msg: msg,
RawError: err,
}
}
// NewErrorFromResponse 从 serializer.Response 构建错误
func NewErrorFromResponse(resp *Response) AppError {
return AppError{
Code: resp.Code,
Msg: resp.Msg,
RawError: errors.New(resp.Error),
}
}
// WithError 将应用error携带标准库中的error
func (err *AppError) WithError(raw error) AppError {
err.RawError = raw
return *err
}
// Error 返回业务代码确定的可读错误信息
func (err AppError) Error() string {
return err.Msg
}
// 三位数错误编码为复用http原本含义
// 五位数错误编码为应用自定义错误
// 五开头的五位数错误编码为服务器端错误,比如数据库操作失败
// 四开头的五位数错误编码为客户端错误,有时候是客户端代码写错了,有时候是用户操作错误
const (
// CodeNotFullySuccess 未完全成功
CodeNotFullySuccess = 203
// CodeCheckLogin 未登录
CodeCheckLogin = 401
// CodeNoPermissionErr 未授权访问
CodeNoPermissionErr = 403
// CodeNotFound 资源未找到
CodeNotFound = 404
// CodeConflict 资源冲突
CodeConflict = 409
// CodeUploadFailed 上传出错
CodeUploadFailed = 40002
// CodeCreateFolderFailed 目录创建失败
CodeCreateFolderFailed = 40003
// CodeObjectExist 对象已存在
CodeObjectExist = 40004
// CodeSignExpired 签名过期
CodeSignExpired = 40005
// CodePolicyNotAllowed 当前存储策略不允许
CodePolicyNotAllowed = 40006
// CodeGroupNotAllowed 用户组无法进行此操作
CodeGroupNotAllowed = 40007
// CodeAdminRequired 非管理用户组
CodeAdminRequired = 40008
// CodeMasterNotFound 主机节点未注册
CodeMasterNotFound = 40009
// CodeUploadSessionExpired 上传会话已过期
CodeUploadSessionExpired = 40011
// CodeInvalidChunkIndex 无效的分片序号
CodeInvalidChunkIndex = 40012
// CodeInvalidContentLength 无效的正文长度
CodeInvalidContentLength = 40013
// CodePhoneRequired 未绑定手机
CodePhoneRequired = 40010
// CodeBatchSourceSize 超出批量获取外链限制
CodeBatchSourceSize = 40014
// CodeBatchAria2Size 超出最大 Aria2 任务数量限制
CodeBatchAria2Size = 40015
// CodeParentNotExist 父目录不存在
CodeParentNotExist = 40016
// CodeUserBaned 用户不活跃
CodeUserBaned = 40017
// CodeUserNotActivated 用户不活跃
CodeUserNotActivated = 40018
// CodeFeatureNotEnabled 此功能未开启
CodeFeatureNotEnabled = 40019
// CodeCredentialInvalid 凭证无效
CodeCredentialInvalid = 40020
// CodeUserNotFound 用户不存在
CodeUserNotFound = 40021
// Code2FACodeErr 二步验证代码错误
Code2FACodeErr = 40022
// CodeLoginSessionNotExist 登录会话不存在
CodeLoginSessionNotExist = 40023
// CodeInitializeAuthn 无法初始化 WebAuthn
CodeInitializeAuthn = 40024
// CodeWebAuthnCredentialError WebAuthn 凭证无效
CodeWebAuthnCredentialError = 40025
// CodeCaptchaError 验证码错误
CodeCaptchaError = 40026
// CodeCaptchaRefreshNeeded 验证码需要刷新
CodeCaptchaRefreshNeeded = 40027
// CodeFailedSendEmail 邮件发送失败
CodeFailedSendEmail = 40028
// CodeInvalidTempLink 临时链接无效
CodeInvalidTempLink = 40029
// CodeTempLinkExpired 临时链接过期
CodeTempLinkExpired = 40030
// CodeEmailProviderBaned 邮箱后缀被禁用
CodeEmailProviderBaned = 40031
// CodeEmailExisted 邮箱已被使用
CodeEmailExisted = 40032
// CodeEmailSent 邮箱已重新发送
CodeEmailSent = 40033
// CodeUserCannotActivate 用户无法激活
CodeUserCannotActivate = 40034
// 存储策略不存在
CodePolicyNotExist = 40035
// 无法删除默认存储策略
CodeDeleteDefaultPolicy = 40036
// 存储策略下还有文件
CodePolicyUsedByFiles = 40037
// 存储策略绑定了用户组
CodePolicyUsedByGroups = 40038
// 用户组不存在
CodeGroupNotFound = 40039
// 对系统用户组执行非法操作
CodeInvalidActionOnSystemGroup = 40040
// 用户组正在被使用
CodeGroupUsedByUser = 40041
// 为初始用户更改用户组
CodeChangeGroupForDefaultUser = 40042
// 对系统用户执行非法操作
CodeInvalidActionOnDefaultUser = 40043
// 文件不存在
CodeFileNotFound = 40044
// 列取文件失败
CodeListFilesError = 40045
// 对系统节点进行非法操作
CodeInvalidActionOnSystemNode = 40046
// 创建文件系统出错
CodeCreateFSError = 40047
// 创建任务出错
CodeCreateTaskError = 40048
// 文件尺寸太大
CodeFileTooLarge = 40049
// 文件类型不允许
CodeFileTypeNotAllowed = 40050
// 用户容量不足
CodeInsufficientCapacity = 40051
// 对象名非法
CodeIllegalObjectName = 40052
// 不支持对根目录执行此操作
CodeRootProtected = 40053
// 当前目录下已经有同名文件正在上传中
CodeConflictUploadOngoing = 40054
// 文件信息不一致
CodeMetaMismatch = 40055
// 不支持该格式的压缩文件
CodeUnsupportedArchiveType = 40056
// 可用存储策略发生变化
CodePolicyChanged = 40057
// 分享链接无效
CodeShareLinkNotFound = 40058
// 不能转存自己的分享
CodeSaveOwnShare = 40059
// 从机无法向主机发送回调请求
CodeSlavePingMaster = 40060
// Cloudreve 版本不一致
CodeVersionMismatch = 40061
// 积分不足
CodeInsufficientCredit = 40062
// 用户组冲突
CodeGroupConflict = 40063
// 当前已处于此用户组中
CodeGroupInvalid = 40064
// 兑换码无效
CodeInvalidGiftCode = 40065
// 已绑定了QQ账号
CodeQQBindConflict = 40066
// QQ账号已被绑定其他账号
CodeQQBindOtherAccount = 40067
// QQ 未绑定对应账号
CodeQQNotLinked = 40068
// 密码不正确
CodeIncorrectPassword = 40069
// 分享无法预览
CodeDisabledSharePreview = 40070
// 签名无效
CodeInvalidSign = 40071
// 管理员无法购买用户组
CodeFulfillAdminGroup = 40072
// CodeDBError 数据库操作失败
CodeDBError = 50001
// CodeEncryptError 加密失败
CodeEncryptError = 50002
// CodeIOFailed IO操作失败
CodeIOFailed = 50004
// CodeInternalSetting 内部设置参数错误
CodeInternalSetting = 50005
// CodeCacheOperation 缓存操作失败
CodeCacheOperation = 50006
// CodeCallbackError 回调失败
CodeCallbackError = 50007
// 后台设置更新失败
CodeUpdateSetting = 50008
// 跨域策略添加失败
CodeAddCORS = 50009
// 节点不可用
CodeNodeOffline = 50010
// 文件元信息查询失败
CodeQueryMetaFailed = 50011
//CodeParamErr 各种奇奇怪怪的参数错误
CodeParamErr = 40001
// CodeNotSet 未定错误后续尝试从error中获取
CodeNotSet = -1
)
// DBErr 数据库操作失败
func DBErr(msg string, err error) Response {
if msg == "" {
msg = "Database operation failed."
}
return Err(CodeDBError, msg, err)
}
// ParamErr 各种参数错误
func ParamErr(msg string, err error) Response {
if msg == "" {
msg = "Invalid parameters."
}
return Err(CodeParamErr, msg, err)
}
// Err 通用错误处理
func Err(errCode int, msg string, err error) Response {
// 底层错误是AppError则尝试从AppError中获取详细信息
var appError AppError
if errors.As(err, &appError) {
errCode = appError.Code
err = appError.RawError
msg = appError.Msg
}
res := Response{
Code: errCode,
Msg: msg,
}
// 生产环境隐藏底层报错
if err != nil && gin.Mode() != gin.ReleaseMode {
res.Error = err.Error()
}
return res
}

132
pkg/serializer/explorer.go Normal file
View File

@ -0,0 +1,132 @@
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

@ -0,0 +1,35 @@
package serializer
import (
"bytes"
"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"`
}
// NewResponseWithGobData 返回Data字段使用gob编码的Response
func NewResponseWithGobData(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 Response{Data: w.Bytes()}
}
// GobDecode 将 Response 正文解码至目标指针
func (r *Response) GobDecode(target interface{}) {
src := r.Data.(string)
raw := make([]byte, len(src)*len(src)/base64.StdEncoding.DecodedLen(len(src)))
base64.StdEncoding.Decode(raw, []byte(src))
decoder := gob.NewDecoder(bytes.NewBuffer(raw))
decoder.Decode(target)
}

113
pkg/serializer/setting.go Normal file
View File

@ -0,0 +1,113 @@
package serializer
import (
"time"
model "github.com/cloudreve/Cloudreve/v3/models"
)
// SiteConfig 站点全局设置序列
type SiteConfig struct {
SiteName string `json:"title"`
LoginCaptcha bool `json:"loginCaptcha"`
RegCaptcha bool `json:"regCaptcha"`
ForgetCaptcha bool `json:"forgetCaptcha"`
EmailActive bool `json:"emailActive"`
QQLogin bool `json:"QQLogin"`
Themes string `json:"themes"`
DefaultTheme string `json:"defaultTheme"`
ScoreEnabled bool `json:"score_enabled"`
ShareScoreRate string `json:"share_score_rate"`
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"`
SiteNotice string `json:"site_notice"`
CaptchaType string `json:"captcha_type"`
TCaptchaCaptchaAppId string `json:"tcaptcha_captcha_app_id"`
RegisterEnabled bool `json:"registerEnabled"`
ReportEnabled bool `json:"report_enabled"`
AppPromotion bool `json:"app_promotion"`
WopiExts []string `json:"wopi_exts"`
AppFeedbackLink string `json:"app_feedback"`
AppForumLink string `json:"app_forum"`
}
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")),
QQLogin: model.IsTrueVal(checkSettingValue(settings, "qq_login")),
Themes: checkSettingValue(settings, "themes"),
DefaultTheme: checkSettingValue(settings, "defaultTheme"),
ScoreEnabled: model.IsTrueVal(checkSettingValue(settings, "score_enabled")),
ShareScoreRate: checkSettingValue(settings, "share_score_rate"),
HomepageViewMethod: checkSettingValue(settings, "home_view_method"),
ShareViewMethod: checkSettingValue(settings, "share_view_method"),
Authn: model.IsTrueVal(checkSettingValue(settings, "authn_enabled")),
User: userRes,
SiteNotice: checkSettingValue(settings, "siteNotice"),
ReCaptchaKey: checkSettingValue(settings, "captcha_ReCaptchaKey"),
CaptchaType: checkSettingValue(settings, "captcha_type"),
TCaptchaCaptchaAppId: checkSettingValue(settings, "captcha_TCaptcha_CaptchaAppId"),
RegisterEnabled: model.IsTrueVal(checkSettingValue(settings, "register_enabled")),
ReportEnabled: model.IsTrueVal(checkSettingValue(settings, "report_enabled")),
AppPromotion: model.IsTrueVal(checkSettingValue(settings, "show_app_promotion")),
AppFeedbackLink: checkSettingValue(settings, "app_feedback_link"),
AppForumLink: checkSettingValue(settings, "app_forum_link"),
WopiExts: wopiExts,
}}
return res
}
// VolResponse VOL query response
type VolResponse struct {
Signature string `json:"signature"`
Content string `json:"content"`
}

139
pkg/serializer/share.go Normal file
View File

@ -0,0 +1,139 @@
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"`
Score int `json:"score"`
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"`
Score int `json:"score"`
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,
Score: shares[i].Score,
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,
},
Score: share.Score,
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
}

68
pkg/serializer/slave.go Normal file
View File

@ -0,0 +1,68 @@
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{})
}

64
pkg/serializer/upload.go Normal file
View File

@ -0,0 +1,64 @@
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{})
}

172
pkg/serializer/user.go Normal file
View File

@ -0,0 +1,172 @@
package serializer
import (
"fmt"
"time"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
"github.com/duo-labs/webauthn/webauthn"
)
// CheckLogin 检查登录
func CheckLogin() Response {
return Response{
Code: CodeCheckLogin,
Msg: "Login required",
}
}
// PhoneRequired 需要绑定手机
func PhoneRequired() Response {
return Response{
Code: CodePhoneRequired,
Msg: "此功能需要绑定手机后使用",
}
}
// 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"`
Score int `json:"score"`
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"`
ShareFreeEnabled bool `json:"shareFree"`
ShareDownload bool `json:"shareDownload"`
CompressEnabled bool `json:"compress"`
WebDAVEnabled bool `json:"webdav"`
RelocateEnabled bool `json:"relocate"`
SourceBatchSize int `json:"sourceBatch"`
SelectNode bool `json:"selectNode"`
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,
Score: user.Score,
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,
ShareFreeEnabled: user.Group.OptionsSerialized.ShareFree,
ShareDownload: user.Group.OptionsSerialized.ShareDownload,
CompressEnabled: user.Group.OptionsSerialized.ArchiveTask,
WebDAVEnabled: user.Group.WebDAVEnabled,
AllowWebDAVProxy: user.Group.OptionsSerialized.WebDAVProxy,
RelocateEnabled: user.Group.OptionsSerialized.Relocate,
SourceBatchSize: user.Group.OptionsSerialized.SourceBatchSize,
SelectNode: user.Group.OptionsSerialized.SelectNode,
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 + user.GetAvailablePackSize()
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
}

158
pkg/serializer/vas.go Normal file
View File

@ -0,0 +1,158 @@
package serializer
import (
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
"time"
)
type quota struct {
Base uint64 `json:"base"`
Pack uint64 `json:"pack"`
Used uint64 `json:"used"`
Total uint64 `json:"total"`
Packs []storagePacks `json:"packs"`
}
type storagePacks struct {
Name string `json:"name"`
Size uint64 `json:"size"`
ActivateDate time.Time `json:"activate_date"`
Expiration int `json:"expiration"`
ExpirationDate time.Time `json:"expiration_date"`
}
// MountedFolders 已挂载的目录
type MountedFolders struct {
ID string `json:"id"`
Name string `json:"name"`
PolicyName string `json:"policy_name"`
}
type policyOptions struct {
Name string `json:"name"`
ID string `json:"id"`
}
type nodeOptions struct {
Name string `json:"name"`
ID uint `json:"id"`
}
// BuildPolicySettingRes 构建存储策略选项选择
func BuildPolicySettingRes(policies []model.Policy) Response {
options := make([]policyOptions, 0, len(policies))
for _, policy := range policies {
options = append(options, policyOptions{
Name: policy.Name,
ID: hashid.HashID(policy.ID, hashid.PolicyID),
})
}
return Response{
Data: options,
}
}
// BuildMountedFolderRes 构建已挂载目录响应list为当前用户可用存储策略ID
func BuildMountedFolderRes(folders []model.Folder, list []uint) []MountedFolders {
res := make([]MountedFolders, 0, len(folders))
for _, folder := range folders {
single := MountedFolders{
ID: hashid.HashID(folder.ID, hashid.FolderID),
Name: folder.Name,
PolicyName: "[Invalid Policy]",
}
if policy, err := model.GetPolicyByID(folder.PolicyID); err == nil && util.ContainsUint(list, policy.ID) {
single.PolicyName = policy.Name
}
res = append(res, single)
}
return res
}
// BuildUserQuotaResponse 序列化用户存储配额概况响应
func BuildUserQuotaResponse(user *model.User, packs []model.StoragePack) Response {
packSize := user.GetAvailablePackSize()
res := quota{
Base: user.Group.MaxStorage,
Pack: packSize,
Used: user.Storage,
Total: packSize + user.Group.MaxStorage,
Packs: make([]storagePacks, 0, len(packs)),
}
for _, pack := range packs {
res.Packs = append(res.Packs, storagePacks{
Name: pack.Name,
Size: pack.Size,
ActivateDate: *pack.ActiveTime,
Expiration: int(pack.ExpiredTime.Sub(*pack.ActiveTime).Seconds()),
ExpirationDate: *pack.ExpiredTime,
})
}
return Response{
Data: res,
}
}
// PackProduct 容量包商品
type PackProduct struct {
ID int64 `json:"id"`
Name string `json:"name"`
Size uint64 `json:"size"`
Time int64 `json:"time"`
Price int `json:"price"`
Score int `json:"score"`
}
// GroupProducts 用户组商品
type GroupProducts struct {
ID int64 `json:"id"`
Name string `json:"name"`
GroupID uint `json:"group_id"`
Time int64 `json:"time"`
Price int `json:"price"`
Score int `json:"score"`
Des []string `json:"des"`
Highlight bool `json:"highlight"`
}
// BuildProductResponse 构建增值服务商品响应
func BuildProductResponse(groups []GroupProducts, packs []PackProduct,
wechat, alipay, payjs, custom bool, customName string, scorePrice int) Response {
// 隐藏响应中的用户组ID
for i := 0; i < len(groups); i++ {
groups[i].GroupID = 0
}
return Response{
Data: map[string]interface{}{
"packs": packs,
"groups": groups,
"alipay": alipay,
"wechat": wechat,
"payjs": payjs,
"custom": custom,
"custom_name": customName,
"score_price": scorePrice,
},
}
}
// BuildNodeOptionRes 构建可用节点列表响应
func BuildNodeOptionRes(nodes []*model.Node) Response {
options := make([]nodeOptions, 0, len(nodes))
for _, node := range nodes {
options = append(options, nodeOptions{
Name: node.Name,
ID: node.ID,
})
}
return Response{
Data: options,
}
}