Init V4 community edition (#2265)
* Init V4 community edition * Init V4 community edition
This commit is contained in:
@@ -1,71 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
)
|
||||
|
||||
// Aria2TestService aria2连接测试服务
|
||||
type Aria2TestService struct {
|
||||
Server string `json:"server"`
|
||||
RPC string `json:"rpc" binding:"required"`
|
||||
Secret string `json:"secret"`
|
||||
Token string `json:"token"`
|
||||
Type model.ModelType `json:"type"`
|
||||
}
|
||||
|
||||
// Test 测试aria2连接
|
||||
func (service *Aria2TestService) TestMaster() serializer.Response {
|
||||
res, err := aria2.TestRPCConnection(service.RPC, service.Token, 5)
|
||||
if err != nil {
|
||||
return serializer.ParamErr("Failed to connect to RPC server: "+err.Error(), err)
|
||||
}
|
||||
|
||||
if res.Version == "" {
|
||||
return serializer.ParamErr("RPC server returns unexpected response", nil)
|
||||
}
|
||||
|
||||
return serializer.Response{Data: res.Version}
|
||||
}
|
||||
|
||||
func (service *Aria2TestService) TestSlave() serializer.Response {
|
||||
slave, err := url.Parse(service.Server)
|
||||
if err != nil {
|
||||
return serializer.ParamErr("Cannot parse slave server URL, "+err.Error(), nil)
|
||||
}
|
||||
|
||||
controller, _ := url.Parse("/api/v3/slave/ping/aria2")
|
||||
|
||||
// 请求正文
|
||||
service.Type = model.MasterNodeType
|
||||
bodyByte, _ := json.Marshal(service)
|
||||
|
||||
r := request.NewClient()
|
||||
res, err := r.Request(
|
||||
"POST",
|
||||
slave.ResolveReference(controller).String(),
|
||||
bytes.NewReader(bodyByte),
|
||||
request.WithTimeout(time.Duration(10)*time.Second),
|
||||
request.WithCredential(
|
||||
auth.HMACAuth{SecretKey: []byte(service.Secret)},
|
||||
int64(model.GetIntSetting("slave_api_timeout", 60)),
|
||||
),
|
||||
).DecodeResponse()
|
||||
if err != nil {
|
||||
return serializer.ParamErr("Failed to connect to slave node, "+err.Error(), nil)
|
||||
}
|
||||
|
||||
if res.Code != 0 {
|
||||
return serializer.ParamErr("Successfully connected to slave, but slave returns: "+res.Msg, nil)
|
||||
}
|
||||
|
||||
return serializer.Response{Data: res.Data.(string)}
|
||||
}
|
||||
@@ -2,14 +2,22 @@ package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/service/explorer"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// FileService 文件ID服务
|
||||
@@ -33,176 +41,495 @@ type ListFolderService struct {
|
||||
|
||||
// List 列出指定路径下的目录
|
||||
func (service *ListFolderService) List(c *gin.Context) serializer.Response {
|
||||
if service.Type == "policy" {
|
||||
// 列取存储策略中的目录
|
||||
policy, err := model.GetPolicyByID(service.ID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodePolicyNotExist, "", err)
|
||||
}
|
||||
//if service.Type == "policy" {
|
||||
// // 列取存储策略中的目录
|
||||
// policy, err := model.GetPolicyByID(service.ID)
|
||||
// if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodePolicyNotExist, "", err)
|
||||
// }
|
||||
//
|
||||
// // 创建文件系统
|
||||
// fs, err := filesystem.NewAnonymousFileSystem()
|
||||
// if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodeCreateFSError, "", err)
|
||||
// }
|
||||
// defer fs.Recycle()
|
||||
//
|
||||
// // 列取存储策略中的文件
|
||||
// fs.Policy = &policy
|
||||
// res, err := fs.ListPhysical(c.Request.Context(), service.Path)
|
||||
// if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodeListFilesError, "", err)
|
||||
// }
|
||||
//
|
||||
// return serializer.Response{
|
||||
// Data: serializer.BuildObjectList(0, res, nil),
|
||||
// }
|
||||
//
|
||||
//}
|
||||
//
|
||||
//// 列取用户空间目录
|
||||
//// 查找用户
|
||||
//user, err := model.GetUserByID(service.ID)
|
||||
//if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodeUserNotFound, "", err)
|
||||
//}
|
||||
//
|
||||
//// 创建文件系统
|
||||
//fs, err := filesystem.NewFileSystem(&user)
|
||||
//if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodeCreateFSError, "", err)
|
||||
//}
|
||||
//defer fs.Recycle()
|
||||
//
|
||||
//// 列取目录
|
||||
//res, err := fs.List(c.Request.Context(), service.Path, nil)
|
||||
//if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodeListFilesError, "", err)
|
||||
//}
|
||||
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewAnonymousFileSystem()
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
//return serializer.Response{
|
||||
// Data: serializer.BuildObjectList(0, res, nil),
|
||||
//}
|
||||
|
||||
// 列取存储策略中的文件
|
||||
fs.Policy = &policy
|
||||
res, err := fs.ListPhysical(c.Request.Context(), service.Path)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeListFilesError, "", err)
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Data: serializer.BuildObjectList(0, res, nil),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 列取用户空间目录
|
||||
// 查找用户
|
||||
user, err := model.GetUserByID(service.ID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeUserNotFound, "", err)
|
||||
}
|
||||
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystem(&user)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 列取目录
|
||||
res, err := fs.List(c.Request.Context(), service.Path, nil)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeListFilesError, "", err)
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Data: serializer.BuildObjectList(0, res, nil),
|
||||
}
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Delete 删除文件
|
||||
func (service *FileBatchService) Delete(c *gin.Context) serializer.Response {
|
||||
files, err := model.GetFilesByIDs(service.ID, 0)
|
||||
if err != nil {
|
||||
return serializer.DBErr("Failed to list files for deleting", err)
|
||||
}
|
||||
|
||||
// 根据用户分组
|
||||
userFile := make(map[uint][]model.File)
|
||||
for i := 0; i < len(files); i++ {
|
||||
if _, ok := userFile[files[i].UserID]; !ok {
|
||||
userFile[files[i].UserID] = []model.File{}
|
||||
}
|
||||
userFile[files[i].UserID] = append(userFile[files[i].UserID], files[i])
|
||||
}
|
||||
|
||||
// 异步执行删除
|
||||
go func(files map[uint][]model.File) {
|
||||
for uid, file := range files {
|
||||
var (
|
||||
fs *filesystem.FileSystem
|
||||
err error
|
||||
)
|
||||
user, err := model.GetUserByID(uid)
|
||||
if err != nil {
|
||||
fs, err = filesystem.NewAnonymousFileSystem()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
fs, err = filesystem.NewFileSystem(&user)
|
||||
if err != nil {
|
||||
fs.Recycle()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 汇总文件ID
|
||||
ids := make([]uint, 0, len(file))
|
||||
for i := 0; i < len(file); i++ {
|
||||
ids = append(ids, file[i].ID)
|
||||
}
|
||||
|
||||
// 执行删除
|
||||
fs.Delete(context.Background(), []uint{}, ids, service.Force, service.UnlinkOnly)
|
||||
fs.Recycle()
|
||||
}
|
||||
}(userFile)
|
||||
//files, err := model.GetFilesByIDs(service.ID, 0)
|
||||
//if err != nil {
|
||||
// return serializer.DBErrDeprecated("Failed to list files for deleting", err)
|
||||
//}
|
||||
//
|
||||
//// 根据用户分组
|
||||
//userFile := make(map[uint][]model.File)
|
||||
//for i := 0; i < len(files); i++ {
|
||||
// if _, ok := userFile[files[i].UserID]; !ok {
|
||||
// userFile[files[i].UserID] = []model.File{}
|
||||
// }
|
||||
// userFile[files[i].UserID] = append(userFile[files[i].UserID], files[i])
|
||||
//}
|
||||
//
|
||||
//// 异步执行删除
|
||||
//go func(files map[uint][]model.File) {
|
||||
// for uid, file := range files {
|
||||
// var (
|
||||
// fs *filesystem.FileSystem
|
||||
// err error
|
||||
// )
|
||||
// user, err := model.GetUserByID(uid)
|
||||
// if err != nil {
|
||||
// fs, err = filesystem.NewAnonymousFileSystem()
|
||||
// if err != nil {
|
||||
// continue
|
||||
// }
|
||||
// } else {
|
||||
// fs, err = filesystem.NewFileSystem(&user)
|
||||
// if err != nil {
|
||||
// fs.Recycle()
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 汇总文件ID
|
||||
// ids := make([]uint, 0, len(file))
|
||||
// for i := 0; i < len(file); i++ {
|
||||
// ids = append(ids, file[i].ID)
|
||||
// }
|
||||
//
|
||||
// // 执行删除
|
||||
// fs.Delete(context.Background(), []uint{}, ids, service.Force, service.UnlinkOnly)
|
||||
// fs.Recycle()
|
||||
// }
|
||||
//}(userFile)
|
||||
|
||||
// 分组执行删除
|
||||
return serializer.Response{}
|
||||
|
||||
}
|
||||
|
||||
// Get 预览文件
|
||||
func (service *FileService) Get(c *gin.Context) serializer.Response {
|
||||
file, err := model.GetFilesByIDs([]uint{service.ID}, 0)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeFileNotFound, "", err)
|
||||
}
|
||||
const (
|
||||
fileNameCondition = "file_name"
|
||||
fileUserCondition = "file_user"
|
||||
filePolicyCondition = "file_policy"
|
||||
)
|
||||
|
||||
ctx := context.WithValue(context.Background(), fsctx.FileModelCtx, &file[0])
|
||||
var subService explorer.FileIDService
|
||||
res := subService.PreviewContent(ctx, c, false)
|
||||
func (service *AdminListService) Files(c *gin.Context) (*ListFileResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
hasher := dep.HashIDEncoder()
|
||||
fileClient := dep.FileClient()
|
||||
|
||||
return res
|
||||
}
|
||||
ctx := context.WithValue(c, inventory.LoadFileEntity{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadFileMetadata{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadFileShare{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadFileUser{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadFileDirectLink{}, true)
|
||||
|
||||
// Files 列出文件
|
||||
func (service *AdminListService) Files() serializer.Response {
|
||||
var res []model.File
|
||||
total := 0
|
||||
var (
|
||||
err error
|
||||
userID int
|
||||
policyID int
|
||||
)
|
||||
|
||||
tx := model.DB.Model(&model.File{})
|
||||
if service.OrderBy != "" {
|
||||
tx = tx.Order(service.OrderBy)
|
||||
}
|
||||
|
||||
for k, v := range service.Conditions {
|
||||
tx = tx.Where(k+" = ?", v)
|
||||
}
|
||||
|
||||
if len(service.Searches) > 0 {
|
||||
search := ""
|
||||
for k, v := range service.Searches {
|
||||
search += k + " like '%" + v + "%' OR "
|
||||
if service.Conditions[fileUserCondition] != "" {
|
||||
userID, err = strconv.Atoi(service.Conditions[fileUserCondition])
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid user ID", err)
|
||||
}
|
||||
search = strings.TrimSuffix(search, " OR ")
|
||||
tx = tx.Where(search)
|
||||
}
|
||||
|
||||
// 计算总数用于分页
|
||||
tx.Count(&total)
|
||||
|
||||
// 查询记录
|
||||
tx.Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
|
||||
|
||||
// 查询对应用户
|
||||
users := make(map[uint]model.User)
|
||||
for _, file := range res {
|
||||
users[file.UserID] = model.User{}
|
||||
if service.Conditions[filePolicyCondition] != "" {
|
||||
policyID, err = strconv.Atoi(service.Conditions[filePolicyCondition])
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid policy ID", err)
|
||||
}
|
||||
}
|
||||
|
||||
userIDs := make([]uint, 0, len(users))
|
||||
for k := range users {
|
||||
userIDs = append(userIDs, k)
|
||||
res, err := fileClient.FlattenListFiles(ctx, &inventory.FlattenListFileParameters{
|
||||
PaginationArgs: &inventory.PaginationArgs{
|
||||
Page: service.Page - 1,
|
||||
PageSize: service.PageSize,
|
||||
OrderBy: service.OrderBy,
|
||||
Order: inventory.OrderDirection(service.OrderDirection),
|
||||
},
|
||||
UserID: userID,
|
||||
StoragePolicyID: policyID,
|
||||
Name: service.Conditions[fileNameCondition],
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list files", err)
|
||||
}
|
||||
|
||||
var userList []model.User
|
||||
model.DB.Where("id in (?)", userIDs).Find(&userList)
|
||||
|
||||
for _, v := range userList {
|
||||
users[v.ID] = v
|
||||
}
|
||||
|
||||
return serializer.Response{Data: map[string]interface{}{
|
||||
"total": total,
|
||||
"items": res,
|
||||
"users": users,
|
||||
}}
|
||||
return &ListFileResponse{
|
||||
Pagination: res.PaginationResults,
|
||||
Files: lo.Map(res.Files, func(file *ent.File, _ int) GetFileResponse {
|
||||
return GetFileResponse{
|
||||
File: file,
|
||||
UserHashID: hashid.EncodeUserID(hasher, file.OwnerID),
|
||||
}
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
SingleFileService struct {
|
||||
ID int `uri:"id" json:"id" binding:"required"`
|
||||
}
|
||||
SingleFileParamCtx struct{}
|
||||
)
|
||||
|
||||
func (service *SingleFileService) Get(c *gin.Context) (*GetFileResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
hasher := dep.HashIDEncoder()
|
||||
fileClient := dep.FileClient()
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadFileEntity{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadFileMetadata{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadFileShare{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadFileUser{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadEntityUser{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadEntityStoragePolicy{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadFileDirectLink{}, true)
|
||||
|
||||
file, err := fileClient.GetByID(ctx, service.ID)
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "File not found", nil)
|
||||
}
|
||||
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get file", err)
|
||||
}
|
||||
|
||||
directLinkMap := make(map[int]string)
|
||||
siteURL := dep.SettingProvider().SiteURL(c)
|
||||
for _, directLink := range file.Edges.DirectLinks {
|
||||
directLinkMap[directLink.ID] = routes.MasterDirectLink(siteURL, hashid.EncodeSourceLinkID(hasher, directLink.ID), directLink.Name).String()
|
||||
}
|
||||
|
||||
return &GetFileResponse{
|
||||
File: file,
|
||||
UserHashID: hashid.EncodeUserID(hasher, file.OwnerID),
|
||||
DirectLinkMap: directLinkMap,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
UpsertFileService struct {
|
||||
File *ent.File `json:"file" binding:"required"`
|
||||
}
|
||||
UpsertFileParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *UpsertFileService) Update(c *gin.Context) (*GetFileResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
fileClient := dep.FileClient()
|
||||
|
||||
fc, tx, ctx, err := inventory.WithTx(c, fileClient)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to start transaction", err)
|
||||
}
|
||||
|
||||
newFile, err := fc.Update(ctx, s.File)
|
||||
if err != nil {
|
||||
_ = inventory.Rollback(tx)
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to update file", err)
|
||||
}
|
||||
|
||||
if err := inventory.Commit(tx); err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to commit transaction", err)
|
||||
}
|
||||
|
||||
service := &SingleFileService{ID: newFile.ID}
|
||||
return service.Get(c)
|
||||
}
|
||||
|
||||
func (s *SingleFileService) Url(c *gin.Context) (string, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
fileClient := dep.FileClient()
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadFileEntity{}, true)
|
||||
file, err := fileClient.GetByID(ctx, s.ID)
|
||||
if err != nil {
|
||||
return "", serializer.NewError(serializer.CodeDBError, "Failed to get file", err)
|
||||
}
|
||||
|
||||
// find primary entity
|
||||
var primaryEntity *ent.Entity
|
||||
for _, entity := range file.Edges.Entities {
|
||||
if entity.Type == int(types.EntityTypeVersion) && entity.ID == file.PrimaryEntity {
|
||||
primaryEntity = entity
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if primaryEntity == nil {
|
||||
return "", serializer.NewError(serializer.CodeNotFound, "Primary entity not exist", nil)
|
||||
}
|
||||
|
||||
// find policy
|
||||
policy, err := dep.StoragePolicyClient().GetPolicyByID(ctx, primaryEntity.StoragePolicyEntities)
|
||||
if err != nil {
|
||||
return "", serializer.NewError(serializer.CodeDBError, "Failed to get policy", err)
|
||||
}
|
||||
|
||||
m := manager.NewFileManager(dep, inventory.UserFromContext(c))
|
||||
defer m.Recycle()
|
||||
|
||||
driver, err := m.GetStorageDriver(ctx, policy)
|
||||
if err != nil {
|
||||
return "", serializer.NewError(serializer.CodeInternalSetting, "Failed to get storage driver", err)
|
||||
}
|
||||
|
||||
es := entitysource.NewEntitySource(fs.NewEntity(primaryEntity), driver, policy, dep.GeneralAuth(),
|
||||
dep.SettingProvider(), dep.HashIDEncoder(), dep.RequestClient(), dep.Logger(), dep.ConfigProvider(), dep.MimeDetector(ctx))
|
||||
|
||||
expire := time.Now().Add(time.Hour * 1)
|
||||
url, err := es.Url(ctx, entitysource.WithExpire(&expire), entitysource.WithDisplayName(file.Name))
|
||||
if err != nil {
|
||||
return "", serializer.NewError(serializer.CodeInternalSetting, "Failed to get url", err)
|
||||
}
|
||||
|
||||
return url.Url, nil
|
||||
}
|
||||
|
||||
type (
|
||||
BatchFileService struct {
|
||||
IDs []int `json:"ids" binding:"min=1"`
|
||||
}
|
||||
BatchFileParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *BatchFileService) Delete(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
fileClient := dep.FileClient()
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadFileEntity{}, true)
|
||||
files, _, err := fileClient.GetByIDs(ctx, s.IDs, 0)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to get files", err)
|
||||
}
|
||||
|
||||
fc, tx, ctx, err := inventory.WithTx(c, fileClient)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to start transaction", err)
|
||||
}
|
||||
|
||||
_, diff, err := fc.Delete(ctx, files, nil)
|
||||
if err != nil {
|
||||
_ = inventory.Rollback(tx)
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to delete files", err)
|
||||
}
|
||||
|
||||
tx.AppendStorageDiff(diff)
|
||||
if err := inventory.CommitWithStorageDiff(ctx, tx, dep.Logger(), dep.UserClient()); err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to commit transaction", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
entityUserCondition = "entity_user"
|
||||
entityPolicyCondition = "entity_policy"
|
||||
entityTypeCondition = "entity_type"
|
||||
)
|
||||
|
||||
func (s *AdminListService) Entities(c *gin.Context) (*ListEntityResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
fileClient := dep.FileClient()
|
||||
hasher := dep.HashIDEncoder()
|
||||
ctx := context.WithValue(c, inventory.LoadEntityUser{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadEntityStoragePolicy{}, true)
|
||||
|
||||
var (
|
||||
userID int
|
||||
policyID int
|
||||
err error
|
||||
entityType *types.EntityType
|
||||
)
|
||||
|
||||
if s.Conditions[entityUserCondition] != "" {
|
||||
userID, err = strconv.Atoi(s.Conditions[entityUserCondition])
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid user ID", err)
|
||||
}
|
||||
}
|
||||
|
||||
if s.Conditions[entityPolicyCondition] != "" {
|
||||
policyID, err = strconv.Atoi(s.Conditions[entityPolicyCondition])
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid policy ID", err)
|
||||
}
|
||||
}
|
||||
|
||||
if s.Conditions[entityTypeCondition] != "" {
|
||||
typeId, err := strconv.Atoi(s.Conditions[entityTypeCondition])
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid entity type", err)
|
||||
}
|
||||
|
||||
t := types.EntityType(typeId)
|
||||
entityType = &t
|
||||
}
|
||||
|
||||
res, err := fileClient.ListEntities(ctx, &inventory.ListEntityParameters{
|
||||
PaginationArgs: &inventory.PaginationArgs{
|
||||
Page: s.Page - 1,
|
||||
PageSize: s.PageSize,
|
||||
OrderBy: s.OrderBy,
|
||||
Order: inventory.OrderDirection(s.OrderDirection),
|
||||
},
|
||||
UserID: userID,
|
||||
StoragePolicyID: policyID,
|
||||
EntityType: entityType,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list entities", err)
|
||||
}
|
||||
|
||||
return &ListEntityResponse{
|
||||
Pagination: res.PaginationResults,
|
||||
Entities: lo.Map(res.Entities, func(entity *ent.Entity, _ int) GetEntityResponse {
|
||||
return GetEntityResponse{
|
||||
Entity: entity,
|
||||
UserHashID: hashid.EncodeUserID(hasher, entity.CreatedBy),
|
||||
}
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
SingleEntityService struct {
|
||||
ID int `uri:"id" json:"id" binding:"required"`
|
||||
}
|
||||
SingleEntityParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *SingleEntityService) Get(c *gin.Context) (*GetEntityResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
fileClient := dep.FileClient()
|
||||
hasher := dep.HashIDEncoder()
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadEntityUser{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadEntityStoragePolicy{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadEntityFile{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadFileUser{}, true)
|
||||
|
||||
userHashIDMap := make(map[int]string)
|
||||
entity, err := fileClient.GetEntityByID(ctx, s.ID)
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "Entity not found", nil)
|
||||
}
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get entity", err)
|
||||
}
|
||||
|
||||
for _, file := range entity.Edges.File {
|
||||
userHashIDMap[file.OwnerID] = hashid.EncodeUserID(hasher, file.OwnerID)
|
||||
}
|
||||
|
||||
return &GetEntityResponse{
|
||||
Entity: entity,
|
||||
UserHashID: hashid.EncodeUserID(hasher, entity.CreatedBy),
|
||||
UserHashIDMap: userHashIDMap,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
BatchEntityService struct {
|
||||
IDs []int `json:"ids" binding:"min=1"`
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
BatchEntityParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *BatchEntityService) Delete(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
m := manager.NewFileManager(dep, inventory.UserFromContext(c))
|
||||
defer m.Recycle()
|
||||
|
||||
err := m.RecycleEntities(c.Request.Context(), s.Force, s.IDs...)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to recycle entities", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SingleEntityService) Url(c *gin.Context) (string, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
fileClient := dep.FileClient()
|
||||
|
||||
entity, err := fileClient.GetEntityByID(c, s.ID)
|
||||
if err != nil {
|
||||
return "", serializer.NewError(serializer.CodeDBError, "Failed to get file", err)
|
||||
}
|
||||
|
||||
// find policy
|
||||
policy, err := dep.StoragePolicyClient().GetPolicyByID(c, entity.StoragePolicyEntities)
|
||||
if err != nil {
|
||||
return "", serializer.NewError(serializer.CodeDBError, "Failed to get policy", err)
|
||||
}
|
||||
|
||||
m := manager.NewFileManager(dep, inventory.UserFromContext(c))
|
||||
defer m.Recycle()
|
||||
|
||||
driver, err := m.GetStorageDriver(c, policy)
|
||||
if err != nil {
|
||||
return "", serializer.NewError(serializer.CodeInternalSetting, "Failed to get storage driver", err)
|
||||
}
|
||||
|
||||
es := entitysource.NewEntitySource(fs.NewEntity(entity), driver, policy, dep.GeneralAuth(),
|
||||
dep.SettingProvider(), dep.HashIDEncoder(), dep.RequestClient(), dep.Logger(), dep.ConfigProvider(), dep.MimeDetector(c))
|
||||
|
||||
expire := time.Now().Add(time.Hour * 1)
|
||||
url, err := es.Url(c, entitysource.WithDownload(true), entitysource.WithExpire(&expire), entitysource.WithDisplayName(path.Base(entity.Source)))
|
||||
if err != nil {
|
||||
return "", serializer.NewError(serializer.CodeInternalSetting, "Failed to get url", err)
|
||||
}
|
||||
|
||||
return url.Url, nil
|
||||
}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AddGroupService 用户组添加服务
|
||||
type AddGroupService struct {
|
||||
Group model.Group `json:"group" binding:"required"`
|
||||
//Group model.Group `json:"group" binding:"required"`
|
||||
}
|
||||
|
||||
// GroupService 用户组ID服务
|
||||
@@ -18,100 +24,169 @@ type GroupService struct {
|
||||
|
||||
// Get 获取用户组详情
|
||||
func (service *GroupService) Get() serializer.Response {
|
||||
group, err := model.GetGroupByID(service.ID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeGroupNotFound, "", err)
|
||||
}
|
||||
|
||||
return serializer.Response{Data: group}
|
||||
}
|
||||
|
||||
// Delete 删除用户组
|
||||
func (service *GroupService) Delete() serializer.Response {
|
||||
// 查找用户组
|
||||
group, err := model.GetGroupByID(service.ID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeGroupNotFound, "", err)
|
||||
}
|
||||
|
||||
// 是否为系统用户组
|
||||
if group.ID <= 3 {
|
||||
return serializer.Err(serializer.CodeInvalidActionOnSystemGroup, "", err)
|
||||
}
|
||||
|
||||
// 检查是否有用户使用
|
||||
total := 0
|
||||
row := model.DB.Model(&model.User{}).Where("group_id = ?", service.ID).
|
||||
Select("count(id)").Row()
|
||||
row.Scan(&total)
|
||||
if total > 0 {
|
||||
return serializer.Err(serializer.CodeGroupUsedByUser, strconv.Itoa(total), nil)
|
||||
}
|
||||
|
||||
model.DB.Delete(&group)
|
||||
//group, err := model.GetGroupByID(service.ID)
|
||||
//if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodeGroupNotFound, "", err)
|
||||
//}
|
||||
//
|
||||
//return serializer.Response{Data: group}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Add 添加用户组
|
||||
func (service *AddGroupService) Add() serializer.Response {
|
||||
if service.Group.ID > 0 {
|
||||
if err := model.DB.Save(&service.Group).Error; err != nil {
|
||||
return serializer.DBErr("Failed to save group record", err)
|
||||
}
|
||||
} else {
|
||||
if err := model.DB.Create(&service.Group).Error; err != nil {
|
||||
return serializer.DBErr("Failed to create group record", err)
|
||||
}
|
||||
}
|
||||
// Delete 删除用户组
|
||||
func (service *GroupService) Delete() serializer.Response {
|
||||
//// 查找用户组
|
||||
//group, err := model.GetGroupByID(service.ID)
|
||||
//if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodeGroupNotFound, "", err)
|
||||
//}
|
||||
//
|
||||
//// 是否为系统用户组
|
||||
//if group.ID <= 3 {
|
||||
// return serializer.ErrDeprecated(serializer.CodeInvalidActionOnSystemGroup, "", err)
|
||||
//}
|
||||
//
|
||||
//// 检查是否有用户使用
|
||||
//total := 0
|
||||
//row := model.DB.Model(&model.User{}).Where("group_id = ?", service.ID).
|
||||
// Select("count(id)").Row()
|
||||
//row.Scan(&total)
|
||||
//if total > 0 {
|
||||
// return serializer.ErrDeprecated(serializer.CodeGroupUsedByUser, strconv.Itoa(total), nil)
|
||||
//}
|
||||
//
|
||||
//model.DB.Delete(&group)
|
||||
|
||||
return serializer.Response{Data: service.Group.ID}
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Groups 列出用户组
|
||||
func (service *AdminListService) Groups() serializer.Response {
|
||||
var res []model.Group
|
||||
total := 0
|
||||
|
||||
tx := model.DB.Model(&model.Group{})
|
||||
if service.OrderBy != "" {
|
||||
tx = tx.Order(service.OrderBy)
|
||||
func (service *SingleGroupService) Delete(c *gin.Context) error {
|
||||
if service.ID <= 3 {
|
||||
return serializer.NewError(serializer.CodeInvalidActionOnSystemGroup, "", nil)
|
||||
}
|
||||
|
||||
for k, v := range service.Conditions {
|
||||
tx = tx.Where(k+" = ?", v)
|
||||
dep := dependency.FromContext(c)
|
||||
groupClient := dep.GroupClient()
|
||||
|
||||
// Any user still under this group?
|
||||
users, err := groupClient.CountUsers(c, int(service.ID))
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to count users", err)
|
||||
}
|
||||
|
||||
// 计算总数用于分页
|
||||
tx.Count(&total)
|
||||
|
||||
// 查询记录
|
||||
tx.Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
|
||||
|
||||
// 统计每个用户组的用户总数
|
||||
statics := make(map[uint]int, len(res))
|
||||
for i := 0; i < len(res); i++ {
|
||||
total := 0
|
||||
row := model.DB.Model(&model.User{}).Where("group_id = ?", res[i].ID).
|
||||
Select("count(id)").Row()
|
||||
row.Scan(&total)
|
||||
statics[res[i].ID] = total
|
||||
if users > 0 {
|
||||
return serializer.NewError(serializer.CodeGroupUsedByUser, strconv.Itoa(users), nil)
|
||||
}
|
||||
|
||||
// 汇总用户组存储策略
|
||||
policies := make(map[uint]model.Policy)
|
||||
for i := 0; i < len(res); i++ {
|
||||
for _, p := range res[i].PolicyList {
|
||||
if _, ok := policies[p]; !ok {
|
||||
policies[p], _ = model.GetPolicyByID(p)
|
||||
}
|
||||
}
|
||||
err = groupClient.Delete(c, service.ID)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to delete group", err)
|
||||
}
|
||||
|
||||
return serializer.Response{Data: map[string]interface{}{
|
||||
"total": total,
|
||||
"items": res,
|
||||
"statics": statics,
|
||||
"policies": policies,
|
||||
}}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AdminListService) List(c *gin.Context) (*ListGroupResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
groupClient := dep.GroupClient()
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadGroupPolicy{}, true)
|
||||
res, err := groupClient.ListGroups(ctx, &inventory.ListGroupParameters{
|
||||
PaginationArgs: &inventory.PaginationArgs{
|
||||
Page: s.Page - 1,
|
||||
PageSize: s.PageSize,
|
||||
OrderBy: s.OrderBy,
|
||||
Order: inventory.OrderDirection(s.OrderDirection),
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list groups", err)
|
||||
}
|
||||
|
||||
return &ListGroupResponse{
|
||||
Pagination: res.PaginationResults,
|
||||
Groups: res.Groups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
SingleGroupService struct {
|
||||
ID int `uri:"id" json:"id" binding:"required"`
|
||||
}
|
||||
SingleGroupParamCtx struct{}
|
||||
)
|
||||
|
||||
const (
|
||||
countUserQuery = "countUser"
|
||||
)
|
||||
|
||||
func (s *SingleGroupService) Get(c *gin.Context) (*GetGroupResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
groupClient := dep.GroupClient()
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadGroupPolicy{}, true)
|
||||
group, err := groupClient.GetByID(ctx, s.ID)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get group", err)
|
||||
}
|
||||
|
||||
res := &GetGroupResponse{Group: group}
|
||||
|
||||
if c.Query(countUserQuery) != "" {
|
||||
totalUsers, err := groupClient.CountUsers(ctx, int(s.ID))
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to count users", err)
|
||||
}
|
||||
res.TotalUsers = totalUsers
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type (
|
||||
UpsertGroupService struct {
|
||||
Group *ent.Group `json:"group" binding:"required"`
|
||||
}
|
||||
UpsertGroupParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *UpsertGroupService) Update(c *gin.Context) (*GetGroupResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
groupClient := dep.GroupClient()
|
||||
|
||||
if s.Group.ID == 0 {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "ID is required", nil)
|
||||
}
|
||||
|
||||
// Initial admin group have to be admin
|
||||
if s.Group.ID == 1 && !s.Group.Permissions.Enabled(int(types.GroupPermissionIsAdmin)) {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Initial admin group have to be admin", nil)
|
||||
}
|
||||
|
||||
group, err := groupClient.Upsert(c, s.Group)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to update group", err)
|
||||
}
|
||||
|
||||
service := &SingleGroupService{ID: group.ID}
|
||||
return service.Get(c)
|
||||
}
|
||||
|
||||
func (s *UpsertGroupService) Create(c *gin.Context) (*GetGroupResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
groupClient := dep.GroupClient()
|
||||
|
||||
if s.Group.ID > 0 {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "ID must be 0", nil)
|
||||
}
|
||||
|
||||
group, err := groupClient.Upsert(c, s.Group)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to create group", err)
|
||||
}
|
||||
|
||||
service := &SingleGroupService{ID: group.ID}
|
||||
return service.Get(c)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
)
|
||||
|
||||
// AdminListService 仪表盘列条目服务
|
||||
type AdminListService struct {
|
||||
Page int `json:"page" binding:"min=1,required"`
|
||||
PageSize int `json:"page_size" binding:"min=1,required"`
|
||||
OrderBy string `json:"order_by"`
|
||||
Conditions map[string]string `form:"conditions"`
|
||||
Searches map[string]string `form:"searches"`
|
||||
}
|
||||
type (
|
||||
AdminListService struct {
|
||||
Page int `json:"page" binding:"min=1"`
|
||||
PageSize int `json:"page_size" binding:"min=1,required"`
|
||||
OrderBy string `json:"order_by"`
|
||||
OrderDirection string `json:"order_direction"`
|
||||
Conditions map[string]string `json:"conditions"`
|
||||
Searches map[string]string `json:"searches"`
|
||||
}
|
||||
AdminListServiceParamsCtx struct{}
|
||||
)
|
||||
|
||||
// GroupList 获取用户组列表
|
||||
func (service *NoParamService) GroupList() serializer.Response {
|
||||
var res []model.Group
|
||||
model.DB.Model(&model.Group{}).Find(&res)
|
||||
return serializer.Response{Data: res}
|
||||
//var res []model.Group
|
||||
//model.DB.Model(&model.Group{}).Find(&res)
|
||||
//return serializer.Response{Data: res}
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
@@ -1,142 +1,256 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/node"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/downloader"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/downloader/slave"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// AddNodeService 节点添加服务
|
||||
type AddNodeService struct {
|
||||
Node model.Node `json:"node" binding:"required"`
|
||||
const (
|
||||
nodeStatusCondition = "node_status"
|
||||
)
|
||||
|
||||
func (service *AdminListService) Nodes(c *gin.Context) (*ListNodeResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
nodeClient := dep.NodeClient()
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadNodeStoragePolicy{}, true)
|
||||
res, err := nodeClient.ListNodes(ctx, &inventory.ListNodeParameters{
|
||||
PaginationArgs: &inventory.PaginationArgs{
|
||||
Page: service.Page - 1,
|
||||
PageSize: service.PageSize,
|
||||
OrderBy: service.OrderBy,
|
||||
Order: inventory.OrderDirection(service.OrderDirection),
|
||||
},
|
||||
Status: node.Status(service.Conditions[nodeStatusCondition]),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list nodes", err)
|
||||
}
|
||||
|
||||
return &ListNodeResponse{Nodes: res.Nodes, Pagination: res.PaginationResults}, nil
|
||||
}
|
||||
|
||||
// Add 添加节点
|
||||
func (service *AddNodeService) Add() serializer.Response {
|
||||
if service.Node.ID > 0 {
|
||||
if err := model.DB.Save(&service.Node).Error; err != nil {
|
||||
return serializer.DBErr("Failed to save node record", err)
|
||||
}
|
||||
type (
|
||||
SingleNodeService struct {
|
||||
ID int `uri:"id" json:"id" binding:"required"`
|
||||
}
|
||||
SingleNodeParamCtx struct{}
|
||||
)
|
||||
|
||||
func (service *SingleNodeService) Get(c *gin.Context) (*GetNodeResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
nodeClient := dep.NodeClient()
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadNodeStoragePolicy{}, true)
|
||||
node, err := nodeClient.GetNodeById(ctx, service.ID)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get node", err)
|
||||
}
|
||||
|
||||
return &GetNodeResponse{Node: node}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
TestNodeService struct {
|
||||
Node *ent.Node `json:"node" binding:"required"`
|
||||
}
|
||||
TestNodeParamCtx struct{}
|
||||
)
|
||||
|
||||
func (service *TestNodeService) Test(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
settings := dep.SettingProvider()
|
||||
|
||||
slave, err := url.Parse(service.Node.Server)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeParamErr, "Failed to parse node URL", err)
|
||||
}
|
||||
|
||||
primaryURL := settings.SiteURL(setting.UseFirstSiteUrl(c)).String()
|
||||
body := map[string]string{
|
||||
"callback": primaryURL,
|
||||
}
|
||||
bodyByte, _ := json.Marshal(body)
|
||||
|
||||
r := dep.RequestClient()
|
||||
res, err := r.Request(
|
||||
"POST",
|
||||
routes.SlavePingRoute(slave),
|
||||
bytes.NewReader(bodyByte),
|
||||
request.WithTimeout(time.Duration(10)*time.Second),
|
||||
request.WithCredential(
|
||||
auth.HMACAuth{SecretKey: []byte(service.Node.SlaveKey)},
|
||||
int64(settings.SlaveRequestSignTTL(c)),
|
||||
),
|
||||
request.WithSlaveMeta(int(service.Node.ID)),
|
||||
request.WithMasterMeta(settings.SiteBasic(c).ID, primaryURL),
|
||||
request.WithCorrelationID(),
|
||||
).CheckHTTPResponse(http.StatusOK).DecodeResponse()
|
||||
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeParamErr, "Failed to connect to node: "+err.Error(), nil)
|
||||
}
|
||||
|
||||
if res.Code != 0 {
|
||||
return serializer.NewError(serializer.CodeParamErr, "Successfully connected to slave node, but slave returns: "+res.Msg, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
TestNodeDownloaderService struct {
|
||||
Node *ent.Node `json:"node" binding:"required"`
|
||||
}
|
||||
TestNodeDownloaderParamCtx struct{}
|
||||
)
|
||||
|
||||
func (service *TestNodeDownloaderService) Test(c *gin.Context) (string, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
settings := dep.SettingProvider()
|
||||
var (
|
||||
dl downloader.Downloader
|
||||
err error
|
||||
)
|
||||
if service.Node.Type == node.TypeMaster {
|
||||
dl, err = cluster.NewDownloader(c, dep.RequestClient(request.WithContext(c)), dep.SettingProvider(), service.Node.Settings)
|
||||
} else {
|
||||
if err := model.DB.Create(&service.Node).Error; err != nil {
|
||||
return serializer.DBErr("Failed to create node record", err)
|
||||
}
|
||||
dl = slave.NewSlaveDownloader(dep.RequestClient(
|
||||
request.WithContext(c),
|
||||
request.WithCorrelationID(),
|
||||
request.WithSlaveMeta(service.Node.ID),
|
||||
request.WithMasterMeta(settings.SiteBasic(c).ID, settings.SiteURL(setting.UseFirstSiteUrl(c)).String()),
|
||||
request.WithCredential(auth.HMACAuth{[]byte(service.Node.SlaveKey)}, int64(settings.SlaveRequestSignTTL(c))),
|
||||
request.WithEndpoint(service.Node.Server),
|
||||
), service.Node.Settings)
|
||||
}
|
||||
|
||||
if service.Node.Status == model.NodeActive {
|
||||
cluster.Default.Add(&service.Node)
|
||||
}
|
||||
|
||||
return serializer.Response{Data: service.Node.ID}
|
||||
}
|
||||
|
||||
// Nodes 列出从机节点
|
||||
func (service *AdminListService) Nodes() serializer.Response {
|
||||
var res []model.Node
|
||||
total := 0
|
||||
|
||||
tx := model.DB.Model(&model.Node{})
|
||||
if service.OrderBy != "" {
|
||||
tx = tx.Order(service.OrderBy)
|
||||
}
|
||||
|
||||
for k, v := range service.Conditions {
|
||||
tx = tx.Where(k+" = ?", v)
|
||||
}
|
||||
|
||||
if len(service.Searches) > 0 {
|
||||
search := ""
|
||||
for k, v := range service.Searches {
|
||||
search += k + " like '%" + v + "%' OR "
|
||||
}
|
||||
search = strings.TrimSuffix(search, " OR ")
|
||||
tx = tx.Where(search)
|
||||
}
|
||||
|
||||
// 计算总数用于分页
|
||||
tx.Count(&total)
|
||||
|
||||
// 查询记录
|
||||
tx.Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
|
||||
|
||||
isActive := make(map[uint]bool)
|
||||
for i := 0; i < len(res); i++ {
|
||||
if node := cluster.Default.GetNodeByID(res[i].ID); node != nil {
|
||||
isActive[res[i].ID] = node.IsActive()
|
||||
}
|
||||
}
|
||||
|
||||
return serializer.Response{Data: map[string]interface{}{
|
||||
"total": total,
|
||||
"items": res,
|
||||
"active": isActive,
|
||||
}}
|
||||
}
|
||||
|
||||
// ToggleNodeService 开关节点服务
|
||||
type ToggleNodeService struct {
|
||||
ID uint `uri:"id"`
|
||||
Desired model.NodeStatus `uri:"desired"`
|
||||
}
|
||||
|
||||
// Toggle 开关节点
|
||||
func (service *ToggleNodeService) Toggle() serializer.Response {
|
||||
node, err := model.GetNodeByID(service.ID)
|
||||
if err != nil {
|
||||
return serializer.DBErr("Node not found", err)
|
||||
return "", serializer.NewError(serializer.CodeParamErr, "Failed to create downloader", err)
|
||||
}
|
||||
|
||||
// 是否为系统节点
|
||||
if node.ID <= 1 {
|
||||
return serializer.Err(serializer.CodeInvalidActionOnSystemNode, "", err)
|
||||
}
|
||||
|
||||
if err = node.SetStatus(service.Desired); err != nil {
|
||||
return serializer.DBErr("Failed to change node status", err)
|
||||
}
|
||||
|
||||
if service.Desired == model.NodeActive {
|
||||
cluster.Default.Add(&node)
|
||||
} else {
|
||||
cluster.Default.Delete(node.ID)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// NodeService 节点ID服务
|
||||
type NodeService struct {
|
||||
ID uint `uri:"id" json:"id" binding:"required"`
|
||||
}
|
||||
|
||||
// Delete 删除节点
|
||||
func (service *NodeService) Delete() serializer.Response {
|
||||
// 查找用户组
|
||||
node, err := model.GetNodeByID(service.ID)
|
||||
version, err := dl.Test(c)
|
||||
if err != nil {
|
||||
return serializer.DBErr("Node record not found", err)
|
||||
return "", serializer.NewError(serializer.CodeParamErr, "Failed to test downloader: "+err.Error(), nil)
|
||||
}
|
||||
|
||||
// 是否为系统节点
|
||||
if node.ID <= 1 {
|
||||
return serializer.Err(serializer.CodeInvalidActionOnSystemNode, "", err)
|
||||
}
|
||||
|
||||
cluster.Default.Delete(node.ID)
|
||||
if err := model.DB.Delete(&node).Error; err != nil {
|
||||
return serializer.DBErr("Failed to delete node record", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
return version, nil
|
||||
}
|
||||
|
||||
// Get 获取节点详情
|
||||
func (service *NodeService) Get() serializer.Response {
|
||||
node, err := model.GetNodeByID(service.ID)
|
||||
type (
|
||||
UpsertNodeService struct {
|
||||
Node *ent.Node `json:"node" binding:"required"`
|
||||
}
|
||||
UpsertNodeParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *UpsertNodeService) Update(c *gin.Context) (*GetNodeResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
nodeClient := dep.NodeClient()
|
||||
|
||||
if s.Node.ID == 0 {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "ID is required", nil)
|
||||
}
|
||||
|
||||
node, err := nodeClient.Upsert(c, s.Node)
|
||||
if err != nil {
|
||||
return serializer.DBErr("Node not exist", err)
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to update node", err)
|
||||
}
|
||||
|
||||
return serializer.Response{Data: node}
|
||||
// reload node pool
|
||||
np, err := dep.NodePool(c)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to get node pool", err)
|
||||
}
|
||||
np.Upsert(c, node)
|
||||
|
||||
// Clear policy cache since some this node maybe cached by some storage policy
|
||||
kv := dep.KV()
|
||||
kv.Delete(inventory.StoragePolicyCacheKey)
|
||||
|
||||
service := &SingleNodeService{ID: node.ID}
|
||||
return service.Get(c)
|
||||
}
|
||||
|
||||
func (s *UpsertNodeService) Create(c *gin.Context) (*GetNodeResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
nodeClient := dep.NodeClient()
|
||||
|
||||
if s.Node.ID > 0 {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "ID must be 0", nil)
|
||||
}
|
||||
|
||||
node, err := nodeClient.Upsert(c, s.Node)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to create node", err)
|
||||
}
|
||||
|
||||
// reload node pool
|
||||
np, err := dep.NodePool(c)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to get node pool", err)
|
||||
}
|
||||
np.Upsert(c, node)
|
||||
|
||||
service := &SingleNodeService{ID: node.ID}
|
||||
return service.Get(c)
|
||||
}
|
||||
|
||||
func (s *SingleNodeService) Delete(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
nodeClient := dep.NodeClient()
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadNodeStoragePolicy{}, true)
|
||||
existing, err := nodeClient.GetNodeById(ctx, s.ID)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to get node", err)
|
||||
}
|
||||
|
||||
if existing.Type == node.TypeMaster {
|
||||
return serializer.NewError(serializer.CodeInvalidActionOnSystemNode, "", nil)
|
||||
}
|
||||
|
||||
if len(existing.Edges.StoragePolicy) > 0 {
|
||||
return serializer.NewError(
|
||||
serializer.CodeNodeUsedByStoragePolicy,
|
||||
strings.Join(lo.Map(existing.Edges.StoragePolicy, func(i *ent.StoragePolicy, _ int) string {
|
||||
return i.Name
|
||||
}), ", "),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
// insert dummpy disabled node in nodepool to evict it
|
||||
disabledNode := &ent.Node{
|
||||
ID: s.ID,
|
||||
Type: node.TypeSlave,
|
||||
Status: node.StatusSuspended,
|
||||
}
|
||||
np, err := dep.NodePool(c)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to get node pool", err)
|
||||
}
|
||||
np.Upsert(c, disabledNode)
|
||||
return nodeClient.Delete(c, s.ID)
|
||||
}
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/googledrive"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/cos"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/onedrive"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/oss"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/s3"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/constants"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/credmanager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/cos"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/obs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/onedrive"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/oss"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/s3"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
cossdk "github.com/tencentyun/cos-go-sdk-v5"
|
||||
)
|
||||
|
||||
// PathTestService 本地路径测试服务
|
||||
@@ -40,14 +41,17 @@ type SlaveTestService struct {
|
||||
Server string `json:"server" binding:"required"`
|
||||
}
|
||||
|
||||
// SlavePingService 从机相应ping
|
||||
type SlavePingService struct {
|
||||
Callback string `json:"callback" binding:"required"`
|
||||
}
|
||||
type (
|
||||
SlavePingParameterCtx struct{}
|
||||
// SlavePingService ping slave node
|
||||
SlavePingService struct {
|
||||
Callback string `json:"callback" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
// AddPolicyService 存储策略添加服务
|
||||
type AddPolicyService struct {
|
||||
Policy model.Policy `json:"policy" binding:"required"`
|
||||
//Policy model.Policy `json:"policy" binding:"required"`
|
||||
}
|
||||
|
||||
// PolicyService 存储策略ID服务
|
||||
@@ -57,301 +61,444 @@ type PolicyService struct {
|
||||
}
|
||||
|
||||
// Delete 删除存储策略
|
||||
func (service *PolicyService) Delete() serializer.Response {
|
||||
func (service *SingleStoragePolicyService) Delete(c *gin.Context) error {
|
||||
// 禁止删除默认策略
|
||||
if service.ID == 1 {
|
||||
return serializer.Err(serializer.CodeDeleteDefaultPolicy, "", nil)
|
||||
return serializer.NewError(serializer.CodeDeleteDefaultPolicy, "", nil)
|
||||
}
|
||||
|
||||
policy, err := model.GetPolicyByID(service.ID)
|
||||
dep := dependency.FromContext(c)
|
||||
storagePolicyClient := dep.StoragePolicyClient()
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadStoragePolicyGroup{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.SkipStoragePolicyCache{}, true)
|
||||
policy, err := storagePolicyClient.GetPolicyByID(ctx, service.ID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodePolicyNotExist, "", err)
|
||||
return serializer.NewError(serializer.CodePolicyNotExist, "", err)
|
||||
}
|
||||
|
||||
// 检查是否有文件使用
|
||||
total := 0
|
||||
row := model.DB.Model(&model.File{}).Where("policy_id = ?", service.ID).
|
||||
Select("count(id)").Row()
|
||||
row.Scan(&total)
|
||||
if total > 0 {
|
||||
return serializer.Err(serializer.CodePolicyUsedByFiles, strconv.Itoa(total), nil)
|
||||
// If policy is used by groups, return error
|
||||
if len(policy.Edges.Groups) > 0 {
|
||||
return serializer.NewError(serializer.CodePolicyUsedByGroups, strconv.Itoa(len(policy.Edges.Groups)), nil)
|
||||
}
|
||||
|
||||
// 检查用户组使用
|
||||
var groups []model.Group
|
||||
model.DB.Model(&model.Group{}).Where(
|
||||
"policies like ?",
|
||||
fmt.Sprintf("%%[%d]%%", service.ID),
|
||||
).Find(&groups)
|
||||
|
||||
if len(groups) > 0 {
|
||||
return serializer.Err(serializer.CodePolicyUsedByGroups, strconv.Itoa(len(groups)), nil)
|
||||
}
|
||||
|
||||
model.DB.Delete(&policy)
|
||||
policy.ClearCache()
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Get 获取存储策略详情
|
||||
func (service *PolicyService) Get() serializer.Response {
|
||||
policy, err := model.GetPolicyByID(service.ID)
|
||||
used, err := dep.FileClient().IsStoragePolicyUsedByEntities(ctx, service.ID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodePolicyNotExist, "", err)
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to check if policy is used by entities", err)
|
||||
}
|
||||
|
||||
return serializer.Response{Data: policy}
|
||||
}
|
||||
|
||||
// GetOAuth 获取 OneDrive OAuth 地址
|
||||
func (service *PolicyService) GetOAuth(c *gin.Context, policyType string) serializer.Response {
|
||||
policy, err := model.GetPolicyByID(service.ID)
|
||||
if err != nil || policy.Type != policyType {
|
||||
return serializer.Err(serializer.CodePolicyNotExist, "", nil)
|
||||
if used {
|
||||
return serializer.NewError(serializer.CodePolicyUsedByFiles, "", nil)
|
||||
}
|
||||
|
||||
util.SetSession(c, map[string]interface{}{
|
||||
policyType + "_oauth_policy": policy.ID,
|
||||
})
|
||||
|
||||
var redirect string
|
||||
switch policy.Type {
|
||||
case "onedrive":
|
||||
client, err := onedrive.NewClient(&policy)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to initialize OneDrive client", err)
|
||||
}
|
||||
|
||||
redirect = client.OAuthURL(context.Background(), []string{
|
||||
"offline_access",
|
||||
"files.readwrite.all",
|
||||
})
|
||||
case "googledrive":
|
||||
client, err := googledrive.NewClient(&policy)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to initialize Google Drive client", err)
|
||||
}
|
||||
|
||||
redirect = client.OAuthURL(context.Background(), googledrive.RequiredScope)
|
||||
}
|
||||
|
||||
// Delete token cache
|
||||
cache.Deletes([]string{policy.BucketName}, policyType+"_")
|
||||
|
||||
return serializer.Response{Data: redirect}
|
||||
}
|
||||
|
||||
// AddSCF 创建回调云函数
|
||||
func (service *PolicyService) AddSCF() serializer.Response {
|
||||
policy, err := model.GetPolicyByID(service.ID)
|
||||
err = storagePolicyClient.Delete(ctx, policy)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodePolicyNotExist, "", nil)
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to delete policy", err)
|
||||
}
|
||||
|
||||
if err := cos.CreateSCF(&policy, service.Region); err != nil {
|
||||
return serializer.ParamErr("Failed to create SCF function", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// AddCORS 创建跨域策略
|
||||
func (service *PolicyService) AddCORS() serializer.Response {
|
||||
policy, err := model.GetPolicyByID(service.ID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodePolicyNotExist, "", nil)
|
||||
}
|
||||
|
||||
switch policy.Type {
|
||||
case "oss":
|
||||
handler, err := oss.NewDriver(&policy)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeAddCORS, "", err)
|
||||
}
|
||||
if err := handler.CORS(); err != nil {
|
||||
return serializer.Err(serializer.CodeAddCORS, "", err)
|
||||
}
|
||||
case "cos":
|
||||
u, _ := url.Parse(policy.Server)
|
||||
b := &cossdk.BaseURL{BucketURL: u}
|
||||
handler := cos.Driver{
|
||||
Policy: &policy,
|
||||
HTTPClient: request.NewClient(),
|
||||
Client: cossdk.NewClient(b, &http.Client{
|
||||
Transport: &cossdk.AuthorizationTransport{
|
||||
SecretID: policy.AccessKey,
|
||||
SecretKey: policy.SecretKey,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
if err := handler.CORS(); err != nil {
|
||||
return serializer.Err(serializer.CodeAddCORS, "", err)
|
||||
}
|
||||
case "s3":
|
||||
handler, err := s3.NewDriver(&policy)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeAddCORS, "", err)
|
||||
}
|
||||
|
||||
if err := handler.CORS(); err != nil {
|
||||
return serializer.Err(serializer.CodeAddCORS, "", err)
|
||||
}
|
||||
default:
|
||||
return serializer.Err(serializer.CodePolicyNotAllowed, "", nil)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Test 从机响应ping
|
||||
func (service *SlavePingService) Test() serializer.Response {
|
||||
func (service *SlavePingService) Test(c *gin.Context) error {
|
||||
master, err := url.Parse(service.Callback)
|
||||
if err != nil {
|
||||
return serializer.ParamErr("Failed to parse Master site url: "+err.Error(), nil)
|
||||
return serializer.NewError(serializer.CodeParamErr, "Failed to parse callback url", err)
|
||||
}
|
||||
|
||||
controller, _ := url.Parse("/api/v3/site/ping")
|
||||
|
||||
r := request.NewClient()
|
||||
dep := dependency.FromContext(c)
|
||||
r := dep.RequestClient()
|
||||
res, err := r.Request(
|
||||
"GET",
|
||||
master.ResolveReference(controller).String(),
|
||||
routes.MasterPingUrl(master).String(),
|
||||
nil,
|
||||
request.WithContext(c),
|
||||
request.WithLogger(logging.FromContext(c)),
|
||||
request.WithCorrelationID(),
|
||||
request.WithTimeout(time.Duration(10)*time.Second),
|
||||
).DecodeResponse()
|
||||
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeSlavePingMaster, err.Error(), nil)
|
||||
return serializer.NewError(serializer.CodeSlavePingMaster, err.Error(), nil)
|
||||
}
|
||||
|
||||
version := conf.BackendVersion
|
||||
if conf.IsPro == "true" {
|
||||
version += "-pro"
|
||||
}
|
||||
if res.Data.(string) != version {
|
||||
return serializer.Err(serializer.CodeVersionMismatch, "Master: "+res.Data.(string)+", Slave: "+version, nil)
|
||||
version := constants.BackendVersion
|
||||
|
||||
if strings.TrimSuffix(res.Data.(string), "-pro") != version {
|
||||
return serializer.NewError(serializer.CodeVersionMismatch, "Master: "+res.Data.(string)+", Slave: "+version, nil)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Test 测试从机通信
|
||||
func (service *SlaveTestService) Test() serializer.Response {
|
||||
slave, err := url.Parse(service.Server)
|
||||
if err != nil {
|
||||
return serializer.ParamErr("Failed to parse slave node server URL: "+err.Error(), nil)
|
||||
}
|
||||
|
||||
controller, _ := url.Parse("/api/v3/slave/ping")
|
||||
|
||||
// 请求正文
|
||||
body := map[string]string{
|
||||
"callback": model.GetSiteURL().String(),
|
||||
}
|
||||
bodyByte, _ := json.Marshal(body)
|
||||
|
||||
r := request.NewClient()
|
||||
res, err := r.Request(
|
||||
"POST",
|
||||
slave.ResolveReference(controller).String(),
|
||||
bytes.NewReader(bodyByte),
|
||||
request.WithTimeout(time.Duration(10)*time.Second),
|
||||
request.WithCredential(
|
||||
auth.HMACAuth{SecretKey: []byte(service.Secret)},
|
||||
int64(model.GetIntSetting("slave_api_timeout", 60)),
|
||||
),
|
||||
).DecodeResponse()
|
||||
if err != nil {
|
||||
return serializer.ParamErr("Failed to connect to slave node: "+err.Error(), nil)
|
||||
}
|
||||
|
||||
if res.Code != 0 {
|
||||
return serializer.ParamErr("Successfully connected to slave node, but slave returns: "+res.Msg, nil)
|
||||
}
|
||||
//slave, err := url.Parse(service.Server)
|
||||
//if err != nil {
|
||||
// return serializer.ParamErrDeprecated("Failed to parse slave node server URL: "+err.Error(), nil)
|
||||
//}
|
||||
//
|
||||
//controller, _ := url.Parse("/api/v3/slave/ping")
|
||||
//
|
||||
//// 请求正文
|
||||
//body := map[string]string{
|
||||
// "callback": model.GetSiteURL().String(),
|
||||
//}
|
||||
//bodyByte, _ := json.Marshal(body)
|
||||
//
|
||||
//r := request.NewClientDeprecated()
|
||||
//res, err := r.Request(
|
||||
// "POST",
|
||||
// slave.ResolveReference(controller).String(),
|
||||
// bytes.NewReader(bodyByte),
|
||||
// request.WithTimeout(time.Duration(10)*time.Second),
|
||||
// request.WithCredential(
|
||||
// auth.HMACAuth{SecretKey: []byte(service.Secret)},
|
||||
// int64(model.GetIntSetting("slave_api_timeout", 60)),
|
||||
// ),
|
||||
//).DecodeResponse()
|
||||
//if err != nil {
|
||||
// return serializer.ParamErrDeprecated("Failed to connect to slave node: "+err.Error(), nil)
|
||||
//}
|
||||
//
|
||||
//if res.Code != 0 {
|
||||
// return serializer.ParamErrDeprecated("Successfully connected to slave node, but slave returns: "+res.Msg, nil)
|
||||
//}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Add 添加存储策略
|
||||
func (service *AddPolicyService) Add() serializer.Response {
|
||||
if service.Policy.Type != "local" && service.Policy.Type != "remote" {
|
||||
service.Policy.DirNameRule = strings.TrimPrefix(service.Policy.DirNameRule, "/")
|
||||
}
|
||||
|
||||
if service.Policy.ID > 0 {
|
||||
if err := model.DB.Save(&service.Policy).Error; err != nil {
|
||||
return serializer.DBErr("Failed to save policy", err)
|
||||
}
|
||||
} else {
|
||||
if err := model.DB.Create(&service.Policy).Error; err != nil {
|
||||
return serializer.DBErr("Failed to create policy", err)
|
||||
}
|
||||
}
|
||||
|
||||
service.Policy.ClearCache()
|
||||
|
||||
return serializer.Response{Data: service.Policy.ID}
|
||||
}
|
||||
|
||||
// Test 测试本地路径
|
||||
func (service *PathTestService) Test() serializer.Response {
|
||||
policy := model.Policy{DirNameRule: service.Path}
|
||||
path := policy.GeneratePath(1, "/My File")
|
||||
path = filepath.Join(path, "test.txt")
|
||||
file, err := util.CreatNestedFile(util.RelativePath(path))
|
||||
if err != nil {
|
||||
return serializer.ParamErr(fmt.Sprintf("Failed to create \"%s\": %s", path, err.Error()), nil)
|
||||
}
|
||||
|
||||
file.Close()
|
||||
os.Remove(path)
|
||||
//policy := model.Policy{DirNameRule: service.Path}
|
||||
//path := policy.GeneratePath(1, "/My File")
|
||||
//path = filepath.Join(path, "test.txt")
|
||||
//file, err := util.CreatNestedFile(util.RelativePath(path))
|
||||
//if err != nil {
|
||||
// return serializer.ParamErrDeprecated(fmt.Sprintf("Failed to create \"%s\": %s", path, err.Error()), nil)
|
||||
//}
|
||||
//
|
||||
//file.Close()
|
||||
//os.Remove(path)
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
const (
|
||||
policyTypeCondition = "policy_type"
|
||||
)
|
||||
|
||||
// Policies 列出存储策略
|
||||
func (service *AdminListService) Policies() serializer.Response {
|
||||
var res []model.Policy
|
||||
total := 0
|
||||
func (service *AdminListService) Policies(c *gin.Context) (*ListPolicyResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
storagePolicyClient := dep.StoragePolicyClient()
|
||||
|
||||
tx := model.DB.Model(&model.Policy{})
|
||||
if service.OrderBy != "" {
|
||||
tx = tx.Order(service.OrderBy)
|
||||
ctx := context.WithValue(c, inventory.LoadStoragePolicyGroup{}, true)
|
||||
res, err := storagePolicyClient.ListPolicies(ctx, &inventory.ListPolicyParameters{
|
||||
PaginationArgs: &inventory.PaginationArgs{
|
||||
Page: service.Page - 1,
|
||||
PageSize: service.PageSize,
|
||||
OrderBy: service.OrderBy,
|
||||
Order: inventory.OrderDirection(service.OrderDirection),
|
||||
},
|
||||
Type: types.PolicyType(service.Conditions[policyTypeCondition]),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list policies", err)
|
||||
}
|
||||
|
||||
for k, v := range service.Conditions {
|
||||
tx = tx.Where(k+" = ?", v)
|
||||
}
|
||||
|
||||
// 计算总数用于分页
|
||||
tx.Count(&total)
|
||||
|
||||
// 查询记录
|
||||
tx.Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
|
||||
|
||||
// 统计每个策略的文件使用
|
||||
statics := make(map[uint][2]int, len(res))
|
||||
policyIds := make([]uint, 0, len(res))
|
||||
for i := 0; i < len(res); i++ {
|
||||
policyIds = append(policyIds, res[i].ID)
|
||||
}
|
||||
|
||||
rows, _ := model.DB.Model(&model.File{}).Where("policy_id in (?)", policyIds).
|
||||
Select("policy_id,count(id),sum(size)").Group("policy_id").Rows()
|
||||
|
||||
for rows.Next() {
|
||||
policyId := uint(0)
|
||||
total := [2]int{}
|
||||
rows.Scan(&policyId, &total[0], &total[1])
|
||||
|
||||
statics[policyId] = total
|
||||
}
|
||||
|
||||
return serializer.Response{Data: map[string]interface{}{
|
||||
"total": total,
|
||||
"items": res,
|
||||
"statics": statics,
|
||||
}}
|
||||
return &ListPolicyResponse{
|
||||
Pagination: res.PaginationResults,
|
||||
Policies: res.Policies,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
SingleStoragePolicyService struct {
|
||||
ID int `uri:"id" json:"id" binding:"required"`
|
||||
}
|
||||
GetStoragePolicyParamCtx struct{}
|
||||
)
|
||||
|
||||
const (
|
||||
countEntityQuery = "countEntity"
|
||||
)
|
||||
|
||||
func (service *SingleStoragePolicyService) Get(c *gin.Context) (*GetStoragePolicyResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
storagePolicyClient := dep.StoragePolicyClient()
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadStoragePolicyGroup{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.SkipStoragePolicyCache{}, true)
|
||||
policy, err := storagePolicyClient.GetPolicyByID(ctx, service.ID)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get policy", err)
|
||||
}
|
||||
|
||||
res := &GetStoragePolicyResponse{StoragePolicy: policy}
|
||||
if c.Query(countEntityQuery) != "" {
|
||||
count, size, err := dep.FileClient().CountEntityByStoragePolicyID(ctx, service.ID)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to count entities", err)
|
||||
}
|
||||
res.EntitiesCount = count
|
||||
res.EntitiesSize = size
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type (
|
||||
CreateStoragePolicyService struct {
|
||||
Policy *ent.StoragePolicy `json:"policy" binding:"required"`
|
||||
}
|
||||
CreateStoragePolicyParamCtx struct{}
|
||||
)
|
||||
|
||||
func (service *CreateStoragePolicyService) Create(c *gin.Context) (*GetStoragePolicyResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
storagePolicyClient := dep.StoragePolicyClient()
|
||||
|
||||
if service.Policy.Type == types.PolicyTypeLocal {
|
||||
service.Policy.DirNameRule = util.DataPath("uploads/{uid}/{path}")
|
||||
}
|
||||
|
||||
service.Policy.ID = 0
|
||||
policy, err := storagePolicyClient.Upsert(c, service.Policy)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to create policy", err)
|
||||
}
|
||||
|
||||
return &GetStoragePolicyResponse{StoragePolicy: policy}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
UpdateStoragePolicyService struct {
|
||||
Policy *ent.StoragePolicy `json:"policy" binding:"required"`
|
||||
}
|
||||
UpdateStoragePolicyParamCtx struct{}
|
||||
)
|
||||
|
||||
func (service *UpdateStoragePolicyService) Update(c *gin.Context) (*GetStoragePolicyResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
storagePolicyClient := dep.StoragePolicyClient()
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "ID is required", nil)
|
||||
}
|
||||
idInt, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid ID", err)
|
||||
}
|
||||
|
||||
service.Policy.ID = idInt
|
||||
_, err = storagePolicyClient.Upsert(c, service.Policy)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to update policy", err)
|
||||
}
|
||||
|
||||
_ = dep.KV().Delete(manager.EntityUrlCacheKeyPrefix)
|
||||
|
||||
s := SingleStoragePolicyService{ID: idInt}
|
||||
return s.Get(c)
|
||||
}
|
||||
|
||||
type (
|
||||
CreateStoragePolicyCorsService struct {
|
||||
Policy *ent.StoragePolicy `json:"policy" binding:"required"`
|
||||
}
|
||||
CreateStoragePolicyCorsParamCtx struct{}
|
||||
)
|
||||
|
||||
func (service *CreateStoragePolicyCorsService) Create(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
|
||||
switch service.Policy.Type {
|
||||
case types.PolicyTypeOss:
|
||||
handler, err := oss.New(c, service.Policy, dep.SettingProvider(), dep.ConfigProvider(), dep.Logger(), dep.MimeDetector(c))
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to create oss driver", err)
|
||||
}
|
||||
if err := handler.CORS(); err != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to create cors: "+err.Error(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case types.PolicyTypeCos:
|
||||
handler, err := cos.New(c, service.Policy, dep.SettingProvider(), dep.ConfigProvider(), dep.Logger(), dep.MimeDetector(c))
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to create cos driver", err)
|
||||
}
|
||||
|
||||
if err := handler.CORS(); err != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to create cors: "+err.Error(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case types.PolicyTypeS3:
|
||||
handler, err := s3.New(c, service.Policy, dep.SettingProvider(), dep.ConfigProvider(), dep.Logger(), dep.MimeDetector(c))
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to create s3 driver", err)
|
||||
}
|
||||
|
||||
if err := handler.CORS(); err != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to create cors: "+err.Error(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case types.PolicyTypeObs:
|
||||
handler, err := obs.New(c, service.Policy, dep.SettingProvider(), dep.ConfigProvider(), dep.Logger(), dep.MimeDetector(c))
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to create obs driver", err)
|
||||
}
|
||||
|
||||
if err := handler.CORS(); err != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to create cors: "+err.Error(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
default:
|
||||
return serializer.NewError(serializer.CodeParamErr, "Unsupported policy type", nil)
|
||||
}
|
||||
}
|
||||
|
||||
type (
|
||||
GetOauthRedirectService struct {
|
||||
ID int `json:"id" binding:"required"`
|
||||
Secret string `json:"secret" binding:"required"`
|
||||
AppID string `json:"app_id" binding:"required"`
|
||||
}
|
||||
GetOauthRedirectParamCtx struct{}
|
||||
)
|
||||
|
||||
// GetOAuth 获取 OneDrive OAuth 地址
|
||||
func (service *GetOauthRedirectService) GetOAuth(c *gin.Context) (string, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
storagePolicyClient := dep.StoragePolicyClient()
|
||||
|
||||
policy, err := storagePolicyClient.GetPolicyByID(c, service.ID)
|
||||
if err != nil || policy.Type != types.PolicyTypeOd {
|
||||
return "", serializer.NewError(serializer.CodePolicyNotExist, "", nil)
|
||||
}
|
||||
|
||||
// Update to latest redirect url
|
||||
policy.Settings.OauthRedirect = routes.MasterPolicyOAuthCallback(dep.SettingProvider().SiteURL(c)).String()
|
||||
policy.SecretKey = service.Secret
|
||||
policy.BucketName = service.AppID
|
||||
policy, err = storagePolicyClient.Upsert(c, policy)
|
||||
if err != nil {
|
||||
return "", serializer.NewError(serializer.CodeDBError, "Failed to update policy", err)
|
||||
}
|
||||
|
||||
client := onedrive.NewClient(policy, dep.RequestClient(), dep.CredManager(), dep.Logger(), dep.SettingProvider(), 0)
|
||||
redirect := client.OAuthURL(context.Background(), []string{
|
||||
"offline_access",
|
||||
"files.readwrite.all",
|
||||
})
|
||||
|
||||
return redirect, nil
|
||||
}
|
||||
|
||||
func GetPolicyOAuthURL(c *gin.Context) string {
|
||||
dep := dependency.FromContext(c)
|
||||
return routes.MasterPolicyOAuthCallback(dep.SettingProvider().SiteURL(c)).String()
|
||||
}
|
||||
|
||||
// GetOauthCredentialStatus returns last refresh time of oauth credential
|
||||
func (service *SingleStoragePolicyService) GetOauthCredentialStatus(c *gin.Context) (*OauthCredentialStatus, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
storagePolicyClient := dep.StoragePolicyClient()
|
||||
|
||||
policy, err := storagePolicyClient.GetPolicyByID(c, service.ID)
|
||||
if err != nil || policy.Type != types.PolicyTypeOd {
|
||||
return nil, serializer.NewError(serializer.CodePolicyNotExist, "", nil)
|
||||
}
|
||||
|
||||
if policy.AccessKey == "" {
|
||||
return &OauthCredentialStatus{Valid: false}, nil
|
||||
}
|
||||
|
||||
token, err := dep.CredManager().Obtain(c, onedrive.CredentialKey(policy.ID))
|
||||
if err != nil {
|
||||
if errors.Is(err, credmanager.ErrNotFound) {
|
||||
return &OauthCredentialStatus{Valid: false}, nil
|
||||
}
|
||||
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get credential", err)
|
||||
}
|
||||
|
||||
return &OauthCredentialStatus{Valid: true, LastRefreshTime: token.RefreshedAt()}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
FinishOauthCallbackService struct {
|
||||
Code string `json:"code" binding:"required"`
|
||||
State string `json:"state" binding:"required"`
|
||||
}
|
||||
FinishOauthCallbackParamCtx struct{}
|
||||
)
|
||||
|
||||
func (service *FinishOauthCallbackService) Finish(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
storagePolicyClient := dep.StoragePolicyClient()
|
||||
|
||||
policyId, err := strconv.Atoi(service.State)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeParamErr, "Invalid state", err)
|
||||
}
|
||||
|
||||
policy, err := storagePolicyClient.GetPolicyByID(c, policyId)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodePolicyNotExist, "", nil)
|
||||
}
|
||||
|
||||
if policy.Type != types.PolicyTypeOd {
|
||||
return serializer.NewError(serializer.CodeParamErr, "Invalid policy type", nil)
|
||||
}
|
||||
|
||||
client := onedrive.NewClient(policy, dep.RequestClient(), dep.CredManager(), dep.Logger(), dep.SettingProvider(), 0)
|
||||
credential, err := client.ObtainToken(c, onedrive.WithCode(service.Code))
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeIncorrectPassword, "Failed to obtain token", err)
|
||||
}
|
||||
|
||||
credManager := dep.CredManager()
|
||||
err = credManager.Upsert(c, credential)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to upsert credential", err)
|
||||
}
|
||||
|
||||
_, err = credManager.Obtain(c, onedrive.CredentialKey(policy.ID))
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to obtain credential", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *SingleStoragePolicyService) GetSharePointDriverRoot(c *gin.Context) (string, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
storagePolicyClient := dep.StoragePolicyClient()
|
||||
|
||||
policy, err := storagePolicyClient.GetPolicyByID(c, service.ID)
|
||||
if err != nil {
|
||||
return "", serializer.NewError(serializer.CodePolicyNotExist, "", nil)
|
||||
}
|
||||
|
||||
if policy.Type != types.PolicyTypeOd {
|
||||
return "", serializer.NewError(serializer.CodeParamErr, "Invalid policy type", nil)
|
||||
}
|
||||
|
||||
client := onedrive.NewClient(policy, dep.RequestClient(), dep.CredManager(), dep.Logger(), dep.SettingProvider(), 0)
|
||||
root, err := client.GetSiteIDByURL(c, c.Query("url"))
|
||||
if err != nil {
|
||||
return "", serializer.NewError(serializer.CodeInternalSetting, "Failed to get site id", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("sites/%s/drive", root), nil
|
||||
}
|
||||
|
||||
141
service/admin/response.go
Normal file
141
service/admin/response.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/queue"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
)
|
||||
|
||||
type ListShareResponse struct {
|
||||
Pagination *inventory.PaginationResults `json:"pagination"`
|
||||
Shares []GetShareResponse `json:"shares"`
|
||||
}
|
||||
|
||||
type GetShareResponse struct {
|
||||
*ent.Share
|
||||
UserHashID string `json:"user_hash_id,omitempty"`
|
||||
ShareLink string `json:"share_link,omitempty"`
|
||||
}
|
||||
|
||||
type ListTaskResponse struct {
|
||||
Pagination *inventory.PaginationResults `json:"pagination"`
|
||||
Tasks []GetTaskResponse `json:"tasks"`
|
||||
}
|
||||
|
||||
type GetTaskResponse struct {
|
||||
*ent.Task
|
||||
UserHashID string `json:"user_hash_id,omitempty"`
|
||||
Summary *queue.Summary `json:"summary,omitempty"`
|
||||
Node *ent.Node `json:"node,omitempty"`
|
||||
}
|
||||
|
||||
type ListEntityResponse struct {
|
||||
Pagination *inventory.PaginationResults `json:"pagination"`
|
||||
Entities []GetEntityResponse `json:"entities"`
|
||||
}
|
||||
|
||||
type GetEntityResponse struct {
|
||||
*ent.Entity
|
||||
UserHashID string `json:"user_hash_id,omitempty"`
|
||||
UserHashIDMap map[int]string `json:"user_hash_id_map,omitempty"`
|
||||
}
|
||||
|
||||
type ListFileResponse struct {
|
||||
Pagination *inventory.PaginationResults `json:"pagination"`
|
||||
Files []GetFileResponse `json:"files"`
|
||||
}
|
||||
|
||||
type GetFileResponse struct {
|
||||
*ent.File
|
||||
UserHashID string `json:"user_hash_id,omitempty"`
|
||||
DirectLinkMap map[int]string `json:"direct_link_map,omitempty"`
|
||||
}
|
||||
|
||||
type ListUserResponse struct {
|
||||
Pagination *inventory.PaginationResults `json:"pagination"`
|
||||
Users []GetUserResponse `json:"users"`
|
||||
}
|
||||
|
||||
type GetUserResponse struct {
|
||||
*ent.User
|
||||
HashID string `json:"hash_id,omitempty"`
|
||||
TwoFAEnabled bool `json:"two_fa_enabled,omitempty"`
|
||||
Capacity *fs.Capacity `json:"capacity,omitempty"`
|
||||
}
|
||||
|
||||
type GetNodeResponse struct {
|
||||
*ent.Node
|
||||
}
|
||||
|
||||
type GetGroupResponse struct {
|
||||
*ent.Group
|
||||
TotalUsers int `json:"total_users"`
|
||||
}
|
||||
|
||||
type OauthCredentialStatus struct {
|
||||
Valid bool `json:"valid"`
|
||||
LastRefreshTime *time.Time `json:"last_refresh_time"`
|
||||
}
|
||||
|
||||
type GetStoragePolicyResponse struct {
|
||||
*ent.StoragePolicy
|
||||
EntitiesCount int `json:"entities_count,omitempty"`
|
||||
EntitiesSize int `json:"entities_size,omitempty"`
|
||||
}
|
||||
|
||||
type ListNodeResponse struct {
|
||||
Pagination *inventory.PaginationResults `json:"pagination"`
|
||||
Nodes []*ent.Node `json:"nodes"`
|
||||
}
|
||||
|
||||
type ListPolicyResponse struct {
|
||||
Pagination *inventory.PaginationResults `json:"pagination"`
|
||||
Policies []*ent.StoragePolicy `json:"policies"`
|
||||
}
|
||||
|
||||
type QueueMetric struct {
|
||||
Name setting.QueueType `json:"name"`
|
||||
BusyWorkers int `json:"busy_workers"`
|
||||
SuccessTasks int `json:"success_tasks"`
|
||||
FailureTasks int `json:"failure_tasks"`
|
||||
SubmittedTasks int `json:"submitted_tasks"`
|
||||
SuspendingTasks int `json:"suspending_tasks"`
|
||||
}
|
||||
|
||||
type ListGroupResponse struct {
|
||||
Groups []*ent.Group `json:"groups"`
|
||||
Pagination *inventory.PaginationResults `json:"pagination"`
|
||||
}
|
||||
|
||||
type HomepageSummary struct {
|
||||
MetricsSummary *MetricsSummary `json:"metrics_summary"`
|
||||
SiteURls []string `json:"site_urls"`
|
||||
Version *Version `json:"version"`
|
||||
}
|
||||
|
||||
type MetricsSummary struct {
|
||||
Dates []time.Time `json:"dates"`
|
||||
Files []int `json:"files"`
|
||||
Users []int `json:"users"`
|
||||
Shares []int `json:"shares"`
|
||||
FileTotal int `json:"file_total"`
|
||||
UserTotal int `json:"user_total"`
|
||||
ShareTotal int `json:"share_total"`
|
||||
EntitiesTotal int `json:"entities_total"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
}
|
||||
|
||||
type Version struct {
|
||||
Version string `json:"version"`
|
||||
Pro bool `json:"pro"`
|
||||
Commit string `json:"commit"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register(MetricsSummary{})
|
||||
}
|
||||
@@ -1,80 +1,145 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// ShareBatchService 分享批量操作服务
|
||||
type ShareBatchService struct {
|
||||
ID []uint `json:"id" binding:"min=1"`
|
||||
}
|
||||
const (
|
||||
shareUserIDCondition = "share_user_id"
|
||||
shareFileIDCondition = "share_file_id"
|
||||
)
|
||||
|
||||
// Delete 删除文件
|
||||
func (service *ShareBatchService) Delete(c *gin.Context) serializer.Response {
|
||||
if err := model.DB.Where("id in (?)", service.ID).Delete(&model.Share{}).Error; err != nil {
|
||||
return serializer.DBErr("Failed to delete share record", err)
|
||||
}
|
||||
return serializer.Response{}
|
||||
}
|
||||
func (s *AdminListService) Shares(c *gin.Context) (*ListShareResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
shareClient := dep.ShareClient()
|
||||
hasher := dep.HashIDEncoder()
|
||||
|
||||
// Shares 列出分享
|
||||
func (service *AdminListService) Shares() serializer.Response {
|
||||
var res []model.Share
|
||||
total := 0
|
||||
var (
|
||||
err error
|
||||
userID int
|
||||
fileID int
|
||||
)
|
||||
|
||||
tx := model.DB.Model(&model.Share{})
|
||||
if service.OrderBy != "" {
|
||||
tx = tx.Order(service.OrderBy)
|
||||
}
|
||||
|
||||
for k, v := range service.Conditions {
|
||||
tx = tx.Where(k+" = ?", v)
|
||||
}
|
||||
|
||||
if len(service.Searches) > 0 {
|
||||
search := ""
|
||||
for k, v := range service.Searches {
|
||||
search += k + " like '%" + v + "%' OR "
|
||||
if s.Conditions[shareUserIDCondition] != "" {
|
||||
userID, err = strconv.Atoi(s.Conditions[shareUserIDCondition])
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid share user ID", err)
|
||||
}
|
||||
search = strings.TrimSuffix(search, " OR ")
|
||||
tx = tx.Where(search)
|
||||
}
|
||||
|
||||
// 计算总数用于分页
|
||||
tx.Count(&total)
|
||||
|
||||
// 查询记录
|
||||
tx.Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
|
||||
|
||||
// 查询对应用户,同时计算HashID
|
||||
users := make(map[uint]model.User)
|
||||
hashIDs := make(map[uint]string, len(res))
|
||||
for _, file := range res {
|
||||
users[file.UserID] = model.User{}
|
||||
hashIDs[file.ID] = hashid.HashID(file.ID, hashid.ShareID)
|
||||
if s.Conditions[shareFileIDCondition] != "" {
|
||||
fileID, err = strconv.Atoi(s.Conditions[shareFileIDCondition])
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid share file ID", err)
|
||||
}
|
||||
}
|
||||
|
||||
userIDs := make([]uint, 0, len(users))
|
||||
for k := range users {
|
||||
userIDs = append(userIDs, k)
|
||||
ctx := context.WithValue(c, inventory.LoadShareFile{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadShareUser{}, true)
|
||||
|
||||
res, err := shareClient.List(ctx, &inventory.ListShareArgs{
|
||||
PaginationArgs: &inventory.PaginationArgs{
|
||||
Page: s.Page - 1,
|
||||
PageSize: s.PageSize,
|
||||
OrderBy: s.OrderBy,
|
||||
Order: inventory.OrderDirection(s.OrderDirection),
|
||||
},
|
||||
UserID: userID,
|
||||
FileID: fileID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list shares", err)
|
||||
}
|
||||
|
||||
var userList []model.User
|
||||
model.DB.Where("id in (?)", userIDs).Find(&userList)
|
||||
siteUrl := dep.SettingProvider().SiteURL(c)
|
||||
|
||||
for _, v := range userList {
|
||||
users[v.ID] = v
|
||||
}
|
||||
return &ListShareResponse{
|
||||
Pagination: res.PaginationResults,
|
||||
Shares: lo.Map(res.Shares, func(share *ent.Share, _ int) GetShareResponse {
|
||||
var (
|
||||
uid string
|
||||
shareLink string
|
||||
)
|
||||
|
||||
if share.Edges.User != nil {
|
||||
uid = hashid.EncodeUserID(hasher, share.Edges.User.ID)
|
||||
}
|
||||
|
||||
shareLink = routes.MasterShareUrl(siteUrl, hashid.EncodeShareID(hasher, share.ID), share.Password).String()
|
||||
|
||||
return GetShareResponse{
|
||||
Share: share,
|
||||
UserHashID: uid,
|
||||
ShareLink: shareLink,
|
||||
}
|
||||
}),
|
||||
}, nil
|
||||
|
||||
return serializer.Response{Data: map[string]interface{}{
|
||||
"total": total,
|
||||
"items": res,
|
||||
"users": users,
|
||||
"ids": hashIDs,
|
||||
}}
|
||||
}
|
||||
|
||||
type (
|
||||
SingleShareService struct {
|
||||
ShareID int `uri:"id" binding:"required"`
|
||||
}
|
||||
SingleShareParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *SingleShareService) Get(c *gin.Context) (*GetShareResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
shareClient := dep.ShareClient()
|
||||
hasher := dep.HashIDEncoder()
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadShareFile{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadShareUser{}, true)
|
||||
share, err := shareClient.GetByID(ctx, s.ShareID)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get share", err)
|
||||
}
|
||||
|
||||
var (
|
||||
uid string
|
||||
shareLink string
|
||||
)
|
||||
|
||||
if share.Edges.User != nil {
|
||||
uid = hashid.EncodeShareID(hasher, share.Edges.User.ID)
|
||||
}
|
||||
|
||||
siteUrl := dep.SettingProvider().SiteURL(c)
|
||||
shareLink = routes.MasterShareUrl(siteUrl, hashid.EncodeShareID(hasher, share.ID), share.Password).String()
|
||||
|
||||
return &GetShareResponse{
|
||||
Share: share,
|
||||
UserHashID: uid,
|
||||
ShareLink: shareLink,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
BatchShareService struct {
|
||||
ShareIDs []int `json:"ids" binding:"required"`
|
||||
}
|
||||
BatchShareParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *BatchShareService) Delete(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
shareClient := dep.ShareClient()
|
||||
|
||||
if err := shareClient.DeleteBatch(c, s.ShareIDs); err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to delete shares", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/email"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/thumb"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/constants"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/thumb"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -33,133 +42,371 @@ type SettingChangeService struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// BatchSettingGet 设定批量获取服务
|
||||
type BatchSettingGet struct {
|
||||
Keys []string `json:"keys"`
|
||||
}
|
||||
|
||||
// MailTestService 邮件测试服务
|
||||
type MailTestService struct {
|
||||
Email string `json:"to" binding:"email"`
|
||||
}
|
||||
|
||||
// Send 发送测试邮件
|
||||
func (service *MailTestService) Send() serializer.Response {
|
||||
if err := email.Send(service.Email, "Cloudreve Email delivery test", "This is a test Email, to test Cloudreve Email delivery settings"); err != nil {
|
||||
return serializer.Err(serializer.CodeFailedSendEmail, err.Error(), nil)
|
||||
}
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Get 获取设定值
|
||||
func (service *BatchSettingGet) Get() serializer.Response {
|
||||
options := model.GetSettingByNames(service.Keys...)
|
||||
return serializer.Response{Data: options}
|
||||
}
|
||||
|
||||
// Change 批量更改站点设定
|
||||
func (service *BatchSettingChangeService) Change() serializer.Response {
|
||||
cacheClean := make([]string, 0, len(service.Options))
|
||||
tx := model.DB.Begin()
|
||||
|
||||
for _, setting := range service.Options {
|
||||
|
||||
if err := tx.Model(&model.Setting{}).Where("name = ?", setting.Key).Update("value", setting.Value).Error; err != nil {
|
||||
cache.Deletes(cacheClean, "setting_")
|
||||
tx.Rollback()
|
||||
return serializer.Err(serializer.CodeUpdateSetting, "Setting "+setting.Key+" failed to update", err)
|
||||
}
|
||||
|
||||
cacheClean = append(cacheClean, setting.Key)
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return serializer.DBErr("Failed to update setting", err)
|
||||
}
|
||||
|
||||
cache.Deletes(cacheClean, "setting_")
|
||||
//cacheClean := make([]string, 0, len(service.Options))
|
||||
//tx := model.DB.Begin()
|
||||
//
|
||||
//for _, setting := range service.Options {
|
||||
//
|
||||
// if err := tx.Model(&model.Setting{}).Where("name = ?", setting.Key).Update("value", setting.Value).Error; err != nil {
|
||||
// cache.Deletes(cacheClean, "setting_")
|
||||
// tx.Rollback()
|
||||
// return serializer.ErrDeprecated(serializer.CodeUpdateSetting, "Setting "+setting.Key+" failed to update", err)
|
||||
// }
|
||||
//
|
||||
// cacheClean = append(cacheClean, setting.Key)
|
||||
//}
|
||||
//
|
||||
//if err := tx.Commit().Error; err != nil {
|
||||
// return serializer.DBErrDeprecated("Failed to update setting", err)
|
||||
//}
|
||||
//
|
||||
//cache.Deletes(cacheClean, "setting_")
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
const (
|
||||
SummaryRangeDays = 12
|
||||
MetricCacheKey = "admin_summary"
|
||||
metricErrMsg = "Failed to generate metrics summary"
|
||||
)
|
||||
|
||||
type (
|
||||
SummaryService struct {
|
||||
Generate bool `form:"generate"`
|
||||
}
|
||||
SummaryParamCtx struct{}
|
||||
)
|
||||
|
||||
// Summary 获取站点统计概况
|
||||
func (service *NoParamService) Summary() serializer.Response {
|
||||
// 获取版本信息
|
||||
versions := map[string]string{
|
||||
"backend": conf.BackendVersion,
|
||||
"db": conf.RequiredDBVersion,
|
||||
"commit": conf.LastCommit,
|
||||
"is_pro": conf.IsPro,
|
||||
func (s *SummaryService) Summary(c *gin.Context) (*HomepageSummary, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
kv := dep.KV()
|
||||
res := &HomepageSummary{
|
||||
Version: &Version{
|
||||
Version: constants.BackendVersion,
|
||||
Pro: constants.IsProBool,
|
||||
Commit: constants.LastCommit,
|
||||
},
|
||||
SiteURls: lo.Map(dep.SettingProvider().AllSiteURLs(c), func(item *url.URL, index int) string {
|
||||
return item.String()
|
||||
}),
|
||||
}
|
||||
|
||||
if res, ok := cache.Get("admin_summary"); ok {
|
||||
resMap := res.(map[string]interface{})
|
||||
resMap["version"] = versions
|
||||
resMap["siteURL"] = model.GetSettingByName("siteURL")
|
||||
return serializer.Response{Data: resMap}
|
||||
if summary, ok := kv.Get(MetricCacheKey); ok {
|
||||
summaryCasted := summary.(MetricsSummary)
|
||||
res.MetricsSummary = &summaryCasted
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// 统计每日概况
|
||||
total := 12
|
||||
files := make([]int, total)
|
||||
users := make([]int, total)
|
||||
shares := make([]int, total)
|
||||
date := make([]string, total)
|
||||
if !s.Generate {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
summary := &MetricsSummary{
|
||||
Files: make([]int, SummaryRangeDays),
|
||||
Users: make([]int, SummaryRangeDays),
|
||||
Shares: make([]int, SummaryRangeDays),
|
||||
Dates: make([]time.Time, SummaryRangeDays),
|
||||
GeneratedAt: time.Now(),
|
||||
}
|
||||
|
||||
fileClient := dep.FileClient()
|
||||
userClient := dep.UserClient()
|
||||
shareClient := dep.ShareClient()
|
||||
|
||||
toRound := time.Now()
|
||||
timeBase := time.Date(toRound.Year(), toRound.Month(), toRound.Day()+1, 0, 0, 0, 0, toRound.Location())
|
||||
for day := range files {
|
||||
start := timeBase.Add(-time.Duration(total-day) * time.Hour * 24)
|
||||
end := timeBase.Add(-time.Duration(total-day-1) * time.Hour * 24)
|
||||
date[day] = start.Format("1月2日")
|
||||
model.DB.Model(&model.User{}).Where("created_at BETWEEN ? AND ?", start, end).Count(&users[day])
|
||||
model.DB.Model(&model.File{}).Where("created_at BETWEEN ? AND ?", start, end).Count(&files[day])
|
||||
model.DB.Model(&model.Share{}).Where("created_at BETWEEN ? AND ?", start, end).Count(&shares[day])
|
||||
for day := range summary.Files {
|
||||
start := timeBase.Add(-time.Duration(SummaryRangeDays-day) * time.Hour * 24)
|
||||
end := timeBase.Add(-time.Duration(SummaryRangeDays-day-1) * time.Hour * 24)
|
||||
summary.Dates[day] = start
|
||||
fileTotal, err := fileClient.CountByTimeRange(c, &start, &end)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, metricErrMsg, nil)
|
||||
}
|
||||
userTotal, err := userClient.CountByTimeRange(c, &start, &end)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, metricErrMsg, nil)
|
||||
}
|
||||
shareTotal, err := shareClient.CountByTimeRange(c, &start, &end)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, metricErrMsg, nil)
|
||||
}
|
||||
summary.Files[day] = fileTotal
|
||||
summary.Users[day] = userTotal
|
||||
summary.Shares[day] = shareTotal
|
||||
}
|
||||
|
||||
// 统计总数
|
||||
fileTotal := 0
|
||||
userTotal := 0
|
||||
publicShareTotal := 0
|
||||
secretShareTotal := 0
|
||||
model.DB.Model(&model.User{}).Count(&userTotal)
|
||||
model.DB.Model(&model.File{}).Count(&fileTotal)
|
||||
model.DB.Model(&model.Share{}).Where("password = ?", "").Count(&publicShareTotal)
|
||||
model.DB.Model(&model.Share{}).Where("password <> ?", "").Count(&secretShareTotal)
|
||||
|
||||
resp := map[string]interface{}{
|
||||
"date": date,
|
||||
"files": files,
|
||||
"users": users,
|
||||
"shares": shares,
|
||||
"version": versions,
|
||||
"siteURL": model.GetSettingByName("siteURL"),
|
||||
"fileTotal": fileTotal,
|
||||
"userTotal": userTotal,
|
||||
"publicShareTotal": publicShareTotal,
|
||||
"secretShareTotal": secretShareTotal,
|
||||
var err error
|
||||
summary.FileTotal, err = fileClient.CountByTimeRange(c, nil, nil)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, metricErrMsg, nil)
|
||||
}
|
||||
summary.UserTotal, err = userClient.CountByTimeRange(c, nil, nil)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, metricErrMsg, nil)
|
||||
}
|
||||
summary.ShareTotal, err = shareClient.CountByTimeRange(c, nil, nil)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, metricErrMsg, nil)
|
||||
}
|
||||
summary.EntitiesTotal, err = fileClient.CountEntityByTimeRange(c, nil, nil)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, metricErrMsg, nil)
|
||||
}
|
||||
|
||||
cache.Set("admin_summary", resp, 86400)
|
||||
return serializer.Response{
|
||||
Data: resp,
|
||||
}
|
||||
_ = kv.Set(MetricCacheKey, *summary, 86400)
|
||||
res.MetricsSummary = summary
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ThumbGeneratorTestService 缩略图生成测试服务
|
||||
type ThumbGeneratorTestService struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Executable string `json:"executable" binding:"required"`
|
||||
}
|
||||
type (
|
||||
ThumbGeneratorTestService struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Executable string `json:"executable" binding:"required"`
|
||||
}
|
||||
ThumbGeneratorTestParamCtx struct{}
|
||||
)
|
||||
|
||||
// Test 通过获取生成器版本来测试
|
||||
func (s *ThumbGeneratorTestService) Test(c *gin.Context) serializer.Response {
|
||||
func (s *ThumbGeneratorTestService) Test(c *gin.Context) (string, error) {
|
||||
version, err := thumb.TestGenerator(c, s.Name, s.Executable)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeParamErr, err.Error(), err)
|
||||
return "", serializer.NewError(serializer.CodeParamErr, "Failed to invoke generator: "+err.Error(), err)
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Data: version,
|
||||
}
|
||||
return version, nil
|
||||
}
|
||||
|
||||
type (
|
||||
GetSettingService struct {
|
||||
Keys []string `json:"keys" binding:"required"`
|
||||
}
|
||||
GetSettingParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *GetSettingService) GetSetting(c *gin.Context) (map[string]string, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
res, err := dep.SettingClient().Gets(c, lo.Filter(s.Keys, func(item string, index int) bool {
|
||||
return item != "secret_key"
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get settings", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type (
|
||||
SetSettingService struct {
|
||||
Settings map[string]string `json:"settings" binding:"required"`
|
||||
}
|
||||
SetSettingParamCtx struct{}
|
||||
SettingPreProcessor func(ctx context.Context, settings map[string]string) error
|
||||
SettingPostProcessor func(ctx context.Context, settings map[string]string) error
|
||||
)
|
||||
|
||||
var (
|
||||
preprocessors = map[string]SettingPreProcessor{
|
||||
"siteURL": siteUrlPreProcessor,
|
||||
"mime_mapping": mimeMappingPreProcessor,
|
||||
"secret_key": secretKeyPreProcessor,
|
||||
}
|
||||
postprocessors = map[string]SettingPostProcessor{
|
||||
"mime_mapping": mimeMappingPostProcessor,
|
||||
"media_meta_exif": mediaMetaPostProcessor,
|
||||
"media_meta_music": mediaMetaPostProcessor,
|
||||
"media_meta_ffprobe": mediaMetaPostProcessor,
|
||||
"smtpUser": emailPostProcessor,
|
||||
"smtpPass": emailPostProcessor,
|
||||
"smtpHost": emailPostProcessor,
|
||||
"smtpPort": emailPostProcessor,
|
||||
"smtpEncryption": emailPostProcessor,
|
||||
"smtpFrom": emailPostProcessor,
|
||||
"replyTo": emailPostProcessor,
|
||||
"fromName": emailPostProcessor,
|
||||
"fromAdress": emailPostProcessor,
|
||||
"queue_media_meta_worker_num": mediaMetaQueuePostProcessor,
|
||||
"queue_media_meta_max_execution": mediaMetaQueuePostProcessor,
|
||||
"queue_media_meta_backoff_factor": mediaMetaQueuePostProcessor,
|
||||
"queue_media_meta_backoff_max_duration": mediaMetaQueuePostProcessor,
|
||||
"queue_media_meta_max_retry": mediaMetaQueuePostProcessor,
|
||||
"queue_media_meta_retry_delay": mediaMetaQueuePostProcessor,
|
||||
"queue_thumb_worker_num": thumbQueuePostProcessor,
|
||||
"queue_thumb_max_execution": thumbQueuePostProcessor,
|
||||
"queue_thumb_backoff_factor": thumbQueuePostProcessor,
|
||||
"queue_thumb_backoff_max_duration": thumbQueuePostProcessor,
|
||||
"queue_thumb_max_retry": thumbQueuePostProcessor,
|
||||
"queue_thumb_retry_delay": thumbQueuePostProcessor,
|
||||
"queue_recycle_worker_num": entityRecycleQueuePostProcessor,
|
||||
"queue_recycle_max_execution": entityRecycleQueuePostProcessor,
|
||||
"queue_recycle_backoff_factor": entityRecycleQueuePostProcessor,
|
||||
"queue_recycle_backoff_max_duration": entityRecycleQueuePostProcessor,
|
||||
"queue_recycle_max_retry": entityRecycleQueuePostProcessor,
|
||||
"queue_recycle_retry_delay": entityRecycleQueuePostProcessor,
|
||||
"queue_io_intense_worker_num": ioIntenseQueuePostProcessor,
|
||||
"queue_io_intense_max_execution": ioIntenseQueuePostProcessor,
|
||||
"queue_io_intense_backoff_factor": ioIntenseQueuePostProcessor,
|
||||
"queue_io_intense_backoff_max_duration": ioIntenseQueuePostProcessor,
|
||||
"queue_io_intense_max_retry": ioIntenseQueuePostProcessor,
|
||||
"queue_io_intense_retry_delay": ioIntenseQueuePostProcessor,
|
||||
"queue_remote_download_worker_num": remoteDownloadQueuePostProcessor,
|
||||
"queue_remote_download_max_execution": remoteDownloadQueuePostProcessor,
|
||||
"queue_remote_download_backoff_factor": remoteDownloadQueuePostProcessor,
|
||||
"queue_remote_download_backoff_max_duration": remoteDownloadQueuePostProcessor,
|
||||
"queue_remote_download_max_retry": remoteDownloadQueuePostProcessor,
|
||||
"queue_remote_download_retry_delay": remoteDownloadQueuePostProcessor,
|
||||
"secret_key": secretKeyPostProcessor,
|
||||
}
|
||||
)
|
||||
|
||||
func (s *SetSettingService) SetSetting(c *gin.Context) (map[string]string, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
kv := dep.KV()
|
||||
settingClient := dep.SettingClient()
|
||||
|
||||
// Preprocess settings
|
||||
allPreprocessors := make(map[string]SettingPreProcessor)
|
||||
allPostprocessors := make(map[string]SettingPostProcessor)
|
||||
for k, _ := range s.Settings {
|
||||
if preprocessor, ok := preprocessors[k]; ok {
|
||||
fnName := reflect.TypeOf(preprocessor).Name()
|
||||
if _, ok := allPreprocessors[fnName]; !ok {
|
||||
allPreprocessors[fnName] = preprocessor
|
||||
}
|
||||
}
|
||||
|
||||
if postprocessor, ok := postprocessors[k]; ok {
|
||||
fnName := reflect.TypeOf(postprocessor).Name()
|
||||
if _, ok := allPostprocessors[fnName]; !ok {
|
||||
allPostprocessors[fnName] = postprocessor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute all preprocessors
|
||||
for _, preprocessor := range allPreprocessors {
|
||||
if err := preprocessor(c, s.Settings); err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Failed to validate settings", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Save to db
|
||||
sc, tx, ctx, err := inventory.WithTx(c, settingClient)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to create transaction", err)
|
||||
}
|
||||
|
||||
if err := sc.Set(ctx, s.Settings); err != nil {
|
||||
_ = inventory.Rollback(tx)
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to save settings", err)
|
||||
}
|
||||
|
||||
if err := inventory.Commit(tx); err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to commit transaction", err)
|
||||
}
|
||||
|
||||
// Clean cache
|
||||
if err := kv.Delete(setting.KvSettingPrefix, lo.Keys(s.Settings)...); err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to clear cache", err)
|
||||
}
|
||||
|
||||
// Execute post preprocessors
|
||||
for _, postprocessor := range allPostprocessors {
|
||||
if err := postprocessor(ctx, s.Settings); err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Failed to post process settings", err)
|
||||
}
|
||||
}
|
||||
|
||||
return s.Settings, nil
|
||||
}
|
||||
|
||||
func siteUrlPreProcessor(ctx context.Context, settings map[string]string) error {
|
||||
siteURL := settings["siteURL"]
|
||||
urls := strings.Split(siteURL, ",")
|
||||
for index, u := range urls {
|
||||
urlParsed, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse siteURL %q: %w", u, err)
|
||||
}
|
||||
|
||||
urls[index] = urlParsed.String()
|
||||
}
|
||||
settings["siteURL"] = strings.Join(urls, ",")
|
||||
return nil
|
||||
}
|
||||
|
||||
func secretKeyPreProcessor(ctx context.Context, settings map[string]string) error {
|
||||
settings["secret_key"] = util.RandStringRunes(256)
|
||||
return nil
|
||||
}
|
||||
|
||||
func mimeMappingPreProcessor(ctx context.Context, settings map[string]string) error {
|
||||
var mapping map[string]string
|
||||
if err := json.Unmarshal([]byte(settings["mime_mapping"]), &mapping); err != nil {
|
||||
return serializer.NewError(serializer.CodeParamErr, "Invalid mime mapping", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mimeMappingPostProcessor(ctx context.Context, settings map[string]string) error {
|
||||
dep := dependency.FromContext(ctx)
|
||||
dep.MimeDetector(context.WithValue(ctx, dependency.ReloadCtx{}, true))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mediaMetaPostProcessor(ctx context.Context, settings map[string]string) error {
|
||||
dep := dependency.FromContext(ctx)
|
||||
dep.MediaMetaExtractor(context.WithValue(ctx, dependency.ReloadCtx{}, true))
|
||||
return nil
|
||||
}
|
||||
|
||||
func emailPostProcessor(ctx context.Context, settings map[string]string) error {
|
||||
dep := dependency.FromContext(ctx)
|
||||
dep.EmailClient(context.WithValue(ctx, dependency.ReloadCtx{}, true))
|
||||
return nil
|
||||
}
|
||||
|
||||
func mediaMetaQueuePostProcessor(ctx context.Context, settings map[string]string) error {
|
||||
dep := dependency.FromContext(ctx)
|
||||
dep.MediaMetaQueue(context.WithValue(ctx, dependency.ReloadCtx{}, true)).Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
func ioIntenseQueuePostProcessor(ctx context.Context, settings map[string]string) error {
|
||||
dep := dependency.FromContext(ctx)
|
||||
dep.IoIntenseQueue(context.WithValue(ctx, dependency.ReloadCtx{}, true)).Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
func remoteDownloadQueuePostProcessor(ctx context.Context, settings map[string]string) error {
|
||||
dep := dependency.FromContext(ctx)
|
||||
dep.RemoteDownloadQueue(context.WithValue(ctx, dependency.ReloadCtx{}, true)).Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
func entityRecycleQueuePostProcessor(ctx context.Context, settings map[string]string) error {
|
||||
dep := dependency.FromContext(ctx)
|
||||
dep.EntityRecycleQueue(context.WithValue(ctx, dependency.ReloadCtx{}, true)).Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
func thumbQueuePostProcessor(ctx context.Context, settings map[string]string) error {
|
||||
dep := dependency.FromContext(ctx)
|
||||
dep.ThumbQueue(context.WithValue(ctx, dependency.ReloadCtx{}, true)).Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
func secretKeyPostProcessor(ctx context.Context, settings map[string]string) error {
|
||||
dep := dependency.FromContext(ctx)
|
||||
dep.KV().Delete(manager.EntityUrlCacheKeyPrefix)
|
||||
settings["secret_key"] = ""
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,159 +1,251 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/task"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/task"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/queue"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// TaskBatchService 任务批量操作服务
|
||||
type TaskBatchService struct {
|
||||
ID []uint `json:"id" binding:"min=1"`
|
||||
func GetQueueMetrics(c *gin.Context) ([]QueueMetric, error) {
|
||||
res := []QueueMetric{}
|
||||
dep := dependency.FromContext(c)
|
||||
|
||||
mediaMeta := dep.MediaMetaQueue(c)
|
||||
entityRecycle := dep.EntityRecycleQueue(c)
|
||||
ioIntense := dep.IoIntenseQueue(c)
|
||||
remoteDownload := dep.RemoteDownloadQueue(c)
|
||||
thumb := dep.ThumbQueue(c)
|
||||
|
||||
res = append(res, QueueMetric{
|
||||
Name: setting.QueueTypeMediaMeta,
|
||||
BusyWorkers: mediaMeta.BusyWorkers(),
|
||||
SuccessTasks: mediaMeta.SuccessTasks(),
|
||||
FailureTasks: mediaMeta.FailureTasks(),
|
||||
SubmittedTasks: mediaMeta.SubmittedTasks(),
|
||||
SuspendingTasks: mediaMeta.SuspendingTasks(),
|
||||
})
|
||||
res = append(res, QueueMetric{
|
||||
Name: setting.QueueTypeEntityRecycle,
|
||||
BusyWorkers: entityRecycle.BusyWorkers(),
|
||||
SuccessTasks: entityRecycle.SuccessTasks(),
|
||||
FailureTasks: entityRecycle.FailureTasks(),
|
||||
SubmittedTasks: entityRecycle.SubmittedTasks(),
|
||||
SuspendingTasks: entityRecycle.SuspendingTasks(),
|
||||
})
|
||||
res = append(res, QueueMetric{
|
||||
Name: setting.QueueTypeIOIntense,
|
||||
BusyWorkers: ioIntense.BusyWorkers(),
|
||||
SuccessTasks: ioIntense.SuccessTasks(),
|
||||
FailureTasks: ioIntense.FailureTasks(),
|
||||
SubmittedTasks: ioIntense.SubmittedTasks(),
|
||||
SuspendingTasks: ioIntense.SuspendingTasks(),
|
||||
})
|
||||
res = append(res, QueueMetric{
|
||||
Name: setting.QueueTypeRemoteDownload,
|
||||
BusyWorkers: remoteDownload.BusyWorkers(),
|
||||
SuccessTasks: remoteDownload.SuccessTasks(),
|
||||
FailureTasks: remoteDownload.FailureTasks(),
|
||||
SubmittedTasks: remoteDownload.SubmittedTasks(),
|
||||
SuspendingTasks: remoteDownload.SuspendingTasks(),
|
||||
})
|
||||
res = append(res, QueueMetric{
|
||||
Name: setting.QueueTypeThumb,
|
||||
BusyWorkers: thumb.BusyWorkers(),
|
||||
SuccessTasks: thumb.SuccessTasks(),
|
||||
FailureTasks: thumb.FailureTasks(),
|
||||
SubmittedTasks: thumb.SubmittedTasks(),
|
||||
SuspendingTasks: thumb.SuspendingTasks(),
|
||||
})
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ImportTaskService 导入任务
|
||||
type ImportTaskService struct {
|
||||
UID uint `json:"uid" binding:"required"`
|
||||
PolicyID uint `json:"policy_id" binding:"required"`
|
||||
Src string `json:"src" binding:"required,min=1,max=65535"`
|
||||
Dst string `json:"dst" binding:"required,min=1,max=65535"`
|
||||
Recursive bool `json:"recursive"`
|
||||
}
|
||||
const (
|
||||
taskTypeCondition = "task_type"
|
||||
taskStatusCondition = "task_status"
|
||||
taskCorrelationIDCondition = "task_correlation_id"
|
||||
taskUserIDCondition = "task_user_id"
|
||||
)
|
||||
|
||||
func (s *AdminListService) Tasks(c *gin.Context) (*ListTaskResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
taskClient := dep.TaskClient()
|
||||
hasher := dep.HashIDEncoder()
|
||||
var (
|
||||
err error
|
||||
userID int
|
||||
correlationID *uuid.UUID
|
||||
status []task.Status
|
||||
taskType []string
|
||||
)
|
||||
|
||||
if s.Conditions[taskTypeCondition] != "" {
|
||||
taskType = []string{s.Conditions[taskTypeCondition]}
|
||||
}
|
||||
|
||||
if s.Conditions[taskStatusCondition] != "" {
|
||||
status = []task.Status{task.Status(s.Conditions[taskStatusCondition])}
|
||||
}
|
||||
|
||||
if s.Conditions[taskCorrelationIDCondition] != "" {
|
||||
cid, err := uuid.FromString(s.Conditions[taskCorrelationIDCondition])
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid task correlation ID", err)
|
||||
}
|
||||
correlationID = &cid
|
||||
}
|
||||
|
||||
if s.Conditions[taskUserIDCondition] != "" {
|
||||
userID, err = strconv.Atoi(s.Conditions[taskUserIDCondition])
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid task user ID", err)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadTaskUser{}, true)
|
||||
res, err := taskClient.List(ctx, &inventory.ListTaskArgs{
|
||||
PaginationArgs: &inventory.PaginationArgs{
|
||||
Page: s.Page - 1,
|
||||
PageSize: s.PageSize,
|
||||
OrderBy: s.OrderBy,
|
||||
Order: inventory.OrderDirection(s.OrderDirection),
|
||||
},
|
||||
UserID: userID,
|
||||
CorrelationID: correlationID,
|
||||
Types: taskType,
|
||||
Status: status,
|
||||
})
|
||||
|
||||
// Create 新建导入任务
|
||||
func (service *ImportTaskService) Create(c *gin.Context, user *model.User) serializer.Response {
|
||||
// 创建任务
|
||||
job, err := task.NewImportTask(service.UID, service.PolicyID, service.Src, service.Dst, service.Recursive)
|
||||
if err != nil {
|
||||
return serializer.DBErr("Failed to create task record.", err)
|
||||
}
|
||||
task.TaskPoll.Submit(job)
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Delete 删除任务
|
||||
func (service *TaskBatchService) Delete(c *gin.Context) serializer.Response {
|
||||
if err := model.DB.Where("id in (?)", service.ID).Delete(&model.Download{}).Error; err != nil {
|
||||
return serializer.DBErr("Failed to delete task records", err)
|
||||
}
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// DeleteGeneral 删除常规任务
|
||||
func (service *TaskBatchService) DeleteGeneral(c *gin.Context) serializer.Response {
|
||||
if err := model.DB.Where("id in (?)", service.ID).Delete(&model.Task{}).Error; err != nil {
|
||||
return serializer.DBErr("Failed to delete task records", err)
|
||||
}
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Tasks 列出常规任务
|
||||
func (service *AdminListService) Tasks() serializer.Response {
|
||||
var res []model.Task
|
||||
total := 0
|
||||
|
||||
tx := model.DB.Model(&model.Task{})
|
||||
if service.OrderBy != "" {
|
||||
tx = tx.Order(service.OrderBy)
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list tasks", err)
|
||||
}
|
||||
|
||||
for k, v := range service.Conditions {
|
||||
tx = tx.Where(k+" = ?", v)
|
||||
}
|
||||
|
||||
if len(service.Searches) > 0 {
|
||||
search := ""
|
||||
for k, v := range service.Searches {
|
||||
search += k + " like '%" + v + "%' OR "
|
||||
tasks := make([]queue.Task, 0, len(res.Tasks))
|
||||
nodeMap := make(map[int]*ent.Node)
|
||||
for _, t := range res.Tasks {
|
||||
task, err := queue.NewTaskFromModel(t)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to parse task", err)
|
||||
}
|
||||
search = strings.TrimSuffix(search, " OR ")
|
||||
tx = tx.Where(search)
|
||||
}
|
||||
|
||||
// 计算总数用于分页
|
||||
tx.Count(&total)
|
||||
|
||||
// 查询记录
|
||||
tx.Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
|
||||
|
||||
// 查询对应用户,同时计算HashID
|
||||
users := make(map[uint]model.User)
|
||||
for _, file := range res {
|
||||
users[file.UserID] = model.User{}
|
||||
}
|
||||
|
||||
userIDs := make([]uint, 0, len(users))
|
||||
for k := range users {
|
||||
userIDs = append(userIDs, k)
|
||||
}
|
||||
|
||||
var userList []model.User
|
||||
model.DB.Where("id in (?)", userIDs).Find(&userList)
|
||||
|
||||
for _, v := range userList {
|
||||
users[v.ID] = v
|
||||
}
|
||||
|
||||
return serializer.Response{Data: map[string]interface{}{
|
||||
"total": total,
|
||||
"items": res,
|
||||
"users": users,
|
||||
}}
|
||||
}
|
||||
|
||||
// Downloads 列出离线下载任务
|
||||
func (service *AdminListService) Downloads() serializer.Response {
|
||||
var res []model.Download
|
||||
total := 0
|
||||
|
||||
tx := model.DB.Model(&model.Download{})
|
||||
if service.OrderBy != "" {
|
||||
tx = tx.Order(service.OrderBy)
|
||||
}
|
||||
|
||||
for k, v := range service.Conditions {
|
||||
tx = tx.Where(k+" = ?", v)
|
||||
}
|
||||
|
||||
if len(service.Searches) > 0 {
|
||||
search := ""
|
||||
for k, v := range service.Searches {
|
||||
search += k + " like '%" + v + "%' OR "
|
||||
summary := task.Summarize(hasher)
|
||||
if summary != nil && summary.NodeID > 0 {
|
||||
if _, ok := nodeMap[summary.NodeID]; !ok {
|
||||
nodeMap[summary.NodeID] = nil
|
||||
}
|
||||
}
|
||||
search = strings.TrimSuffix(search, " OR ")
|
||||
tx = tx.Where(search)
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
|
||||
// 计算总数用于分页
|
||||
tx.Count(&total)
|
||||
|
||||
// 查询记录
|
||||
tx.Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
|
||||
|
||||
// 查询对应用户,同时计算HashID
|
||||
users := make(map[uint]model.User)
|
||||
for _, file := range res {
|
||||
users[file.UserID] = model.User{}
|
||||
// Get nodes
|
||||
nodes, err := dep.NodeClient().GetNodeByIds(c, lo.Keys(nodeMap))
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to query nodes", err)
|
||||
}
|
||||
for _, n := range nodes {
|
||||
nodeMap[n.ID] = n
|
||||
}
|
||||
|
||||
userIDs := make([]uint, 0, len(users))
|
||||
for k := range users {
|
||||
userIDs = append(userIDs, k)
|
||||
}
|
||||
return &ListTaskResponse{
|
||||
Pagination: res.PaginationResults,
|
||||
Tasks: lo.Map(res.Tasks, func(task *ent.Task, i int) GetTaskResponse {
|
||||
var (
|
||||
uid string
|
||||
node *ent.Node
|
||||
summary *queue.Summary
|
||||
)
|
||||
|
||||
var userList []model.User
|
||||
model.DB.Where("id in (?)", userIDs).Find(&userList)
|
||||
if task.Edges.User != nil {
|
||||
uid = hashid.EncodeUserID(hasher, task.Edges.User.ID)
|
||||
}
|
||||
|
||||
for _, v := range userList {
|
||||
users[v.ID] = v
|
||||
}
|
||||
t := tasks[i]
|
||||
summary = t.Summarize(hasher)
|
||||
if summary != nil && summary.NodeID > 0 {
|
||||
node = nodeMap[summary.NodeID]
|
||||
}
|
||||
|
||||
return serializer.Response{Data: map[string]interface{}{
|
||||
"total": total,
|
||||
"items": res,
|
||||
"users": users,
|
||||
}}
|
||||
return GetTaskResponse{
|
||||
Task: task,
|
||||
UserHashID: uid,
|
||||
Node: node,
|
||||
Summary: summary,
|
||||
}
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
SingleTaskService struct {
|
||||
ID int `uri:"id" json:"id" binding:"required"`
|
||||
}
|
||||
SingleTaskParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *SingleTaskService) Get(c *gin.Context) (*GetTaskResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
taskClient := dep.TaskClient()
|
||||
hasher := dep.HashIDEncoder()
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadTaskUser{}, true)
|
||||
task, err := taskClient.GetTaskByID(ctx, s.ID)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get task", err)
|
||||
}
|
||||
|
||||
t, err := queue.NewTaskFromModel(task)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to parse task", err)
|
||||
}
|
||||
|
||||
summary := t.Summarize(hasher)
|
||||
var (
|
||||
node *ent.Node
|
||||
userHashID string
|
||||
)
|
||||
|
||||
if summary != nil && summary.NodeID > 0 {
|
||||
node, _ = dep.NodeClient().GetNodeById(c, summary.NodeID)
|
||||
}
|
||||
|
||||
if task.Edges.User != nil {
|
||||
userHashID = hashid.EncodeUserID(hasher, task.Edges.User.ID)
|
||||
}
|
||||
|
||||
return &GetTaskResponse{
|
||||
Task: task,
|
||||
Summary: summary,
|
||||
Node: node,
|
||||
UserHashID: userHashID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
BatchTaskService struct {
|
||||
IDs []int `json:"ids" binding:"required"`
|
||||
}
|
||||
BatchTaskParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *BatchTaskService) Delete(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
taskClient := dep.TaskClient()
|
||||
|
||||
err := taskClient.DeleteByIDs(c, s.IDs...)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to delete tasks", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
170
service/admin/tools.go
Normal file
170
service/admin/tools.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
request2 "github.com/cloudreve/Cloudreve/v4/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/wopi"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-mail/mail"
|
||||
)
|
||||
|
||||
type (
|
||||
HashIDService struct {
|
||||
ID int `json:"id"`
|
||||
Type int `json:"type"`
|
||||
HashID string `json:"hash_id"`
|
||||
}
|
||||
HashIDParamCtx struct{}
|
||||
)
|
||||
|
||||
func (service *HashIDService) Encode(c *gin.Context) (string, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
res, err := dep.HashIDEncoder().Encode([]int{service.ID, service.Type})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (service *HashIDService) Decode(c *gin.Context) (int, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
res, err := dep.HashIDEncoder().Decode(service.HashID, service.Type)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type (
|
||||
BsEncodeService struct {
|
||||
Bool []int `json:"bool"`
|
||||
}
|
||||
BsEncodeParamCtx struct{}
|
||||
BsEncodeRes struct {
|
||||
Hex string
|
||||
B64 []byte
|
||||
}
|
||||
)
|
||||
|
||||
func (service *BsEncodeService) Encode(c *gin.Context) (*BsEncodeRes, error) {
|
||||
bs := &boolset.BooleanSet{}
|
||||
for _, v := range service.Bool {
|
||||
boolset.Set(v, true, bs)
|
||||
}
|
||||
|
||||
res, err := bs.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &BsEncodeRes{
|
||||
Hex: hex.EncodeToString(res),
|
||||
B64: res,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
BsDecodeService struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
BsDecodeParamCtx struct{}
|
||||
BsDecodeRes struct {
|
||||
Bool []int `json:"bool"`
|
||||
}
|
||||
)
|
||||
|
||||
func (service *BsDecodeService) Decode(c *gin.Context) (*BsDecodeRes, error) {
|
||||
bs, err := boolset.FromString(service.Code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := []int{}
|
||||
for i := 0; i < len(*bs)*8; i++ {
|
||||
if bs.Enabled(i) {
|
||||
res = append(res, i)
|
||||
}
|
||||
}
|
||||
|
||||
return &BsDecodeRes{
|
||||
Bool: res,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
FetchWOPIDiscoveryService struct {
|
||||
Endpoint string `form:"endpoint" binding:"required"`
|
||||
}
|
||||
FetchWOPIDiscoveryParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *FetchWOPIDiscoveryService) Fetch(c *gin.Context) (*setting.ViewerGroup, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
requestClient := dep.RequestClient(request2.WithContext(c), request2.WithLogger(dep.Logger()))
|
||||
content, err := requestClient.Request("GET", s.Endpoint, nil).CheckHTTPResponse(http.StatusOK).GetResponse()
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "WOPI endpoint id unavailable", err)
|
||||
}
|
||||
|
||||
vg, err := wopi.DiscoveryXmlToViewerGroup(content)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Failed to parse WOPI response", err)
|
||||
}
|
||||
|
||||
return vg, nil
|
||||
}
|
||||
|
||||
type (
|
||||
TestSMTPService struct {
|
||||
Settings map[string]string `json:"settings" binding:"required"`
|
||||
To string `json:"to" binding:"required,email"`
|
||||
}
|
||||
TestSMTPParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *TestSMTPService) Test(c *gin.Context) error {
|
||||
port, err := strconv.Atoi(s.Settings["smtpPort"])
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeParamErr, "Invalid SMTP port", err)
|
||||
}
|
||||
|
||||
d := mail.NewDialer(s.Settings["smtpHost"], port, s.Settings["smtpUser"], s.Settings["smtpPass"])
|
||||
d.SSL = false
|
||||
if setting.IsTrueValue(s.Settings["smtpEncryption"]) {
|
||||
d.SSL = true
|
||||
}
|
||||
d.StartTLSPolicy = mail.OpportunisticStartTLS
|
||||
|
||||
sender, err := d.Dial()
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to connect to SMTP server: "+err.Error(), err)
|
||||
}
|
||||
|
||||
m := mail.NewMessage()
|
||||
m.SetHeader("From", s.Settings["fromAdress"])
|
||||
m.SetAddressHeader("Reply-To", s.Settings["replyTo"], s.Settings["fromName"])
|
||||
m.SetHeader("To", s.To)
|
||||
m.SetHeader("Subject", "Cloudreve SMTP Test")
|
||||
m.SetBody("text/plain", "This is a test email from Cloudreve.")
|
||||
|
||||
err = mail.Send(sender, m)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to send test email: "+err.Error(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ClearEntityUrlCache(c *gin.Context) {
|
||||
dep := dependency.FromContext(c)
|
||||
dep.KV().Delete(manager.EntityUrlCacheKeyPrefix)
|
||||
}
|
||||
@@ -2,17 +2,24 @@ package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"strconv"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/user"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// AddUserService 用户添加服务
|
||||
type AddUserService struct {
|
||||
User model.User `json:"User" binding:"required"`
|
||||
Password string `json:"password"`
|
||||
//User model.User `json:"User" binding:"required"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// UserService 用户ID服务
|
||||
@@ -25,148 +32,219 @@ type UserBatchService struct {
|
||||
ID []uint `json:"id" binding:"min=1"`
|
||||
}
|
||||
|
||||
// Ban 封禁/解封用户
|
||||
func (service *UserService) Ban() serializer.Response {
|
||||
user, err := model.GetUserByID(service.ID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeUserNotFound, "", err)
|
||||
}
|
||||
const (
|
||||
userStatusCondition = "user_status"
|
||||
userGroupCondition = "user_group"
|
||||
userNickCondition = "user_nick"
|
||||
userEmailCondition = "user_email"
|
||||
)
|
||||
|
||||
if user.ID == 1 {
|
||||
return serializer.Err(serializer.CodeInvalidActionOnDefaultUser, "", err)
|
||||
}
|
||||
func (service *AdminListService) Users(c *gin.Context) (*ListUserResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
hasher := dep.HashIDEncoder()
|
||||
userClient := dep.UserClient()
|
||||
|
||||
if user.Status == model.Active {
|
||||
user.SetStatus(model.Baned)
|
||||
} else {
|
||||
user.SetStatus(model.Active)
|
||||
}
|
||||
ctx := context.WithValue(c, inventory.LoadUserGroup{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadUserPasskey{}, true)
|
||||
|
||||
return serializer.Response{Data: user.Status}
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (service *UserBatchService) Delete() serializer.Response {
|
||||
for _, uid := range service.ID {
|
||||
user, err := model.GetUserByID(uid)
|
||||
var (
|
||||
err error
|
||||
groupID int
|
||||
)
|
||||
if service.Conditions[userGroupCondition] != "" {
|
||||
groupID, err = strconv.Atoi(service.Conditions[userGroupCondition])
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeUserNotFound, "", err)
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid group ID", err)
|
||||
}
|
||||
|
||||
// 不能删除初始用户
|
||||
if uid == 1 {
|
||||
return serializer.Err(serializer.CodeInvalidActionOnDefaultUser, "", err)
|
||||
}
|
||||
|
||||
// 删除与此用户相关的所有资源
|
||||
|
||||
fs, err := filesystem.NewFileSystem(&user)
|
||||
// 删除所有文件
|
||||
root, err := fs.User.Root()
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "User's root folder not exist", err)
|
||||
}
|
||||
fs.Delete(context.Background(), []uint{root.ID}, []uint{}, false, false)
|
||||
|
||||
// 删除相关任务
|
||||
model.DB.Where("user_id = ?", uid).Delete(&model.Download{})
|
||||
model.DB.Where("user_id = ?", uid).Delete(&model.Task{})
|
||||
|
||||
// 删除标签
|
||||
model.DB.Where("user_id = ?", uid).Delete(&model.Tag{})
|
||||
|
||||
// 删除WebDAV账号
|
||||
model.DB.Where("user_id = ?", uid).Delete(&model.Webdav{})
|
||||
|
||||
// 删除此用户
|
||||
model.DB.Unscoped().Delete(user)
|
||||
|
||||
}
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Get 获取用户详情
|
||||
func (service *UserService) Get() serializer.Response {
|
||||
group, err := model.GetUserByID(service.ID)
|
||||
res, err := userClient.ListUsers(ctx, &inventory.ListUserParameters{
|
||||
PaginationArgs: &inventory.PaginationArgs{
|
||||
Page: service.Page - 1,
|
||||
PageSize: service.PageSize,
|
||||
OrderBy: service.OrderBy,
|
||||
Order: inventory.OrderDirection(service.OrderDirection),
|
||||
},
|
||||
Status: user.Status(service.Conditions[userStatusCondition]),
|
||||
GroupID: groupID,
|
||||
Nick: service.Conditions[userNickCondition],
|
||||
Email: service.Conditions[userEmailCondition],
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeUserNotFound, "", err)
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list users", err)
|
||||
}
|
||||
|
||||
return serializer.Response{Data: group}
|
||||
}
|
||||
|
||||
// Add 添加用户
|
||||
func (service *AddUserService) Add() serializer.Response {
|
||||
if service.User.ID > 0 {
|
||||
|
||||
user, _ := model.GetUserByID(service.User.ID)
|
||||
if service.Password != "" {
|
||||
user.SetPassword(service.Password)
|
||||
}
|
||||
|
||||
// 只更新必要字段
|
||||
user.Nick = service.User.Nick
|
||||
user.Email = service.User.Email
|
||||
user.GroupID = service.User.GroupID
|
||||
user.Status = service.User.Status
|
||||
user.TwoFactor = service.User.TwoFactor
|
||||
|
||||
// 检查愚蠢操作
|
||||
if user.ID == 1 {
|
||||
if user.GroupID != 1 {
|
||||
return serializer.Err(serializer.CodeChangeGroupForDefaultUser, "", nil)
|
||||
return &ListUserResponse{
|
||||
Pagination: res.PaginationResults,
|
||||
Users: lo.Map(res.Users, func(user *ent.User, _ int) GetUserResponse {
|
||||
return GetUserResponse{
|
||||
User: user,
|
||||
HashID: hashid.EncodeUserID(hasher, user.ID),
|
||||
TwoFAEnabled: user.TwoFactorSecret != "",
|
||||
}
|
||||
if user.Status != model.Active {
|
||||
return serializer.Err(serializer.CodeInvalidActionOnDefaultUser, "", nil)
|
||||
}
|
||||
}
|
||||
|
||||
if err := model.DB.Save(&user).Error; err != nil {
|
||||
return serializer.DBErr("Failed to save user record", err)
|
||||
}
|
||||
} else {
|
||||
service.User.SetPassword(service.Password)
|
||||
if err := model.DB.Create(&service.User).Error; err != nil {
|
||||
return serializer.DBErr("Failed to create user record", err)
|
||||
}
|
||||
}
|
||||
|
||||
return serializer.Response{Data: service.User.ID}
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Users 列出用户
|
||||
func (service *AdminListService) Users() serializer.Response {
|
||||
var res []model.User
|
||||
total := 0
|
||||
type (
|
||||
SingleUserService struct {
|
||||
ID int `uri:"id" json:"id" binding:"required"`
|
||||
}
|
||||
SingleUserParamCtx struct{}
|
||||
)
|
||||
|
||||
tx := model.DB.Model(&model.User{})
|
||||
if service.OrderBy != "" {
|
||||
tx = tx.Order(service.OrderBy)
|
||||
func (service *SingleUserService) Get(c *gin.Context) (*GetUserResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
hasher := dep.HashIDEncoder()
|
||||
userClient := dep.UserClient()
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadUserGroup{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadUserPasskey{}, true)
|
||||
|
||||
user, err := userClient.GetByID(ctx, service.ID)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get user", err)
|
||||
}
|
||||
|
||||
for k, v := range service.Conditions {
|
||||
tx = tx.Where(k+" = ?", v)
|
||||
m := manager.NewFileManager(dep, user)
|
||||
capacity, err := m.Capacity(ctx)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to get user capacity", err)
|
||||
}
|
||||
|
||||
if len(service.Searches) > 0 {
|
||||
search := ""
|
||||
for k, v := range service.Searches {
|
||||
search += (k + " like '%" + v + "%' OR ")
|
||||
}
|
||||
search = strings.TrimSuffix(search, " OR ")
|
||||
tx = tx.Where(search)
|
||||
}
|
||||
|
||||
// 计算总数用于分页
|
||||
tx.Count(&total)
|
||||
|
||||
// 查询记录
|
||||
tx.Set("gorm:auto_preload", true).Limit(service.PageSize).Offset((service.Page - 1) * service.PageSize).Find(&res)
|
||||
|
||||
// 补齐缺失用户组
|
||||
|
||||
return serializer.Response{Data: map[string]interface{}{
|
||||
"total": total,
|
||||
"items": res,
|
||||
}}
|
||||
return &GetUserResponse{
|
||||
User: user,
|
||||
HashID: hashid.EncodeUserID(hasher, user.ID),
|
||||
TwoFAEnabled: user.TwoFactorSecret != "",
|
||||
Capacity: capacity,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *SingleUserService) CalibrateStorage(c *gin.Context) (*GetUserResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadUserGroup{}, true)
|
||||
_, err := userClient.CalculateStorage(ctx, service.ID)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to calculate storage", err)
|
||||
}
|
||||
|
||||
subService := &SingleUserService{ID: service.ID}
|
||||
return subService.Get(c)
|
||||
}
|
||||
|
||||
type (
|
||||
UpsertUserService struct {
|
||||
User *ent.User `json:"user" binding:"required"`
|
||||
Password string `json:"password"`
|
||||
TwoFA string `json:"two_fa"`
|
||||
}
|
||||
UpsertUserParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *UpsertUserService) Update(c *gin.Context) (*GetUserResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadUserGroup{}, true)
|
||||
existing, err := userClient.GetByID(ctx, s.User.ID)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get user", err)
|
||||
}
|
||||
|
||||
if s.User.ID == 1 && existing.Edges.Group.Permissions.Enabled(int(types.GroupPermissionIsAdmin)) {
|
||||
if s.User.GroupUsers != existing.GroupUsers {
|
||||
return nil, serializer.NewError(serializer.CodeInvalidActionOnDefaultUser, "Cannot change default user's group", nil)
|
||||
}
|
||||
|
||||
if s.User.Status != user.StatusActive {
|
||||
return nil, serializer.NewError(serializer.CodeInvalidActionOnDefaultUser, "Cannot change default user's status", nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
newUser, err := userClient.Upsert(ctx, s.User, s.Password, s.TwoFA)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to update user", err)
|
||||
}
|
||||
|
||||
service := &SingleUserService{ID: newUser.ID}
|
||||
return service.Get(c)
|
||||
}
|
||||
|
||||
func (s *UpsertUserService) Create(c *gin.Context) (*GetUserResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
|
||||
if s.Password == "" {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Password is required", nil)
|
||||
}
|
||||
|
||||
if s.User.ID != 0 {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "ID must be 0", nil)
|
||||
}
|
||||
|
||||
user, err := userClient.Upsert(c, s.User, s.Password, s.TwoFA)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to create user", err)
|
||||
}
|
||||
|
||||
service := &SingleUserService{ID: user.ID}
|
||||
return service.Get(c)
|
||||
|
||||
}
|
||||
|
||||
type (
|
||||
BatchUserService struct {
|
||||
IDs []int `json:"ids" binding:"min=1"`
|
||||
}
|
||||
BatchUserParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *BatchUserService) Delete(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
fileClient := dep.FileClient()
|
||||
|
||||
current := inventory.UserFromContext(c)
|
||||
ae := serializer.NewAggregateError()
|
||||
for _, id := range s.IDs {
|
||||
if current.ID == id || id == 1 {
|
||||
ae.Add(strconv.Itoa(id), serializer.NewError(serializer.CodeInvalidActionOnDefaultUser, "Cannot delete current user", nil))
|
||||
continue
|
||||
}
|
||||
|
||||
fc, tx, ctx, err := inventory.WithTx(c, fileClient)
|
||||
if err != nil {
|
||||
ae.Add(strconv.Itoa(id), serializer.NewError(serializer.CodeDBError, "Failed to start transaction", err))
|
||||
continue
|
||||
}
|
||||
|
||||
uc, _, ctx, err := inventory.WithTx(ctx, userClient)
|
||||
if err != nil {
|
||||
ae.Add(strconv.Itoa(id), serializer.NewError(serializer.CodeDBError, "Failed to start transaction", err))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := fc.DeleteByUser(ctx, id); err != nil {
|
||||
_ = inventory.Rollback(tx)
|
||||
ae.Add(strconv.Itoa(id), serializer.NewError(serializer.CodeDBError, "Failed to delete user files", err))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := uc.Delete(ctx, id); err != nil {
|
||||
_ = inventory.Rollback(tx)
|
||||
ae.Add(strconv.Itoa(id), serializer.NewError(serializer.CodeDBError, "Failed to delete user", err))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := inventory.Commit(tx); err != nil {
|
||||
ae.Add(strconv.Itoa(id), serializer.NewError(serializer.CodeDBError, "Failed to commit transaction", err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return ae.Aggregate()
|
||||
}
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
package aria2
|
||||
|
||||
import (
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/common"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/monitor"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/mq"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AddURLService 添加URL离线下载服务
|
||||
type BatchAddURLService struct {
|
||||
URLs []string `json:"url" binding:"required"`
|
||||
Dst string `json:"dst" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
// Add 主机批量创建新的链接离线下载任务
|
||||
func (service *BatchAddURLService) Add(c *gin.Context, taskType int) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromContext(c)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 检查用户组权限
|
||||
if !fs.User.Group.OptionsSerialized.Aria2 {
|
||||
return serializer.Err(serializer.CodeGroupNotAllowed, "", nil)
|
||||
}
|
||||
|
||||
// 存放目录是否存在
|
||||
if exist, _ := fs.IsPathExist(service.Dst); !exist {
|
||||
return serializer.Err(serializer.CodeParentNotExist, "", nil)
|
||||
}
|
||||
|
||||
// 检查批量任务数量
|
||||
limit := fs.User.Group.OptionsSerialized.Aria2BatchSize
|
||||
if limit > 0 && len(service.URLs) > limit {
|
||||
return serializer.Err(serializer.CodeBatchAria2Size, "", nil)
|
||||
}
|
||||
|
||||
res := make([]serializer.Response, 0, len(service.URLs))
|
||||
for _, target := range service.URLs {
|
||||
subService := &AddURLService{
|
||||
URL: target,
|
||||
Dst: service.Dst,
|
||||
}
|
||||
|
||||
addRes := subService.Add(c, fs, taskType)
|
||||
res = append(res, addRes)
|
||||
}
|
||||
|
||||
return serializer.Response{Data: res}
|
||||
}
|
||||
|
||||
// AddURLService 添加URL离线下载服务
|
||||
type AddURLService struct {
|
||||
URL string `json:"url" binding:"required"`
|
||||
Dst string `json:"dst" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
// Add 主机创建新的链接离线下载任务
|
||||
func (service *AddURLService) Add(c *gin.Context, fs *filesystem.FileSystem, taskType int) serializer.Response {
|
||||
if fs == nil {
|
||||
var err error
|
||||
// 创建文件系统
|
||||
fs, err = filesystem.NewFileSystemFromContext(c)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 检查用户组权限
|
||||
if !fs.User.Group.OptionsSerialized.Aria2 {
|
||||
return serializer.Err(serializer.CodeGroupNotAllowed, "", nil)
|
||||
}
|
||||
|
||||
// 存放目录是否存在
|
||||
if exist, _ := fs.IsPathExist(service.Dst); !exist {
|
||||
return serializer.Err(serializer.CodeParentNotExist, "", nil)
|
||||
}
|
||||
}
|
||||
|
||||
downloads := model.GetDownloadsByStatusAndUser(0, fs.User.ID, common.Downloading, common.Paused, common.Ready)
|
||||
limit := fs.User.Group.OptionsSerialized.Aria2BatchSize
|
||||
if limit > 0 && len(downloads)+1 > limit {
|
||||
return serializer.Err(serializer.CodeBatchAria2Size, "", nil)
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
task := &model.Download{
|
||||
Status: common.Ready,
|
||||
Type: taskType,
|
||||
Dst: service.Dst,
|
||||
UserID: fs.User.ID,
|
||||
Source: service.URL,
|
||||
}
|
||||
|
||||
// 获取 Aria2 负载均衡器
|
||||
lb := aria2.GetLoadBalancer()
|
||||
|
||||
// 获取 Aria2 实例
|
||||
err, node := cluster.Default.BalanceNodeByFeature("aria2", lb)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to get Aria2 instance", err)
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
gid, err := node.GetAria2Instance().CreateTask(task, fs.User.Group.OptionsSerialized.Aria2Options)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateTaskError, "", err)
|
||||
}
|
||||
|
||||
task.GID = gid
|
||||
task.NodeID = node.ID()
|
||||
_, err = task.Create()
|
||||
if err != nil {
|
||||
return serializer.DBErr("Failed to create task record", err)
|
||||
}
|
||||
|
||||
// 创建任务监控
|
||||
monitor.NewMonitor(task, cluster.Default, mq.GlobalMQ)
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Add 从机创建新的链接离线下载任务
|
||||
func Add(c *gin.Context, service *serializer.SlaveAria2Call) serializer.Response {
|
||||
caller, _ := c.Get("MasterAria2Instance")
|
||||
|
||||
// 创建任务
|
||||
gid, err := caller.(common.Aria2).CreateTask(service.Task, service.GroupOptions)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to create aria2 task", err)
|
||||
}
|
||||
|
||||
// 创建事件通知回调
|
||||
siteID, _ := c.Get("MasterSiteID")
|
||||
mq.GlobalMQ.SubscribeCallback(gid, func(message mq.Message) {
|
||||
if err := cluster.DefaultController.SendNotification(siteID.(string), message.TriggeredBy, message); err != nil {
|
||||
util.Log().Warning("Failed to send remote download task status change notifications: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
return serializer.Response{Data: gid}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
package aria2
|
||||
|
||||
import (
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/aria2/common"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SelectFileService 选择要下载的文件服务
|
||||
type SelectFileService struct {
|
||||
Indexes []int `json:"indexes" binding:"required"`
|
||||
}
|
||||
|
||||
// DownloadTaskService 下载任务管理服务
|
||||
type DownloadTaskService struct {
|
||||
GID string `uri:"gid" binding:"required"`
|
||||
}
|
||||
|
||||
// DownloadListService 下载列表服务
|
||||
type DownloadListService struct {
|
||||
Page uint `form:"page"`
|
||||
}
|
||||
|
||||
// Finished 获取已完成的任务
|
||||
func (service *DownloadListService) Finished(c *gin.Context, user *model.User) serializer.Response {
|
||||
// 查找下载记录
|
||||
downloads := model.GetDownloadsByStatusAndUser(service.Page, user.ID, common.Error, common.Complete, common.Canceled, common.Unknown)
|
||||
for key, download := range downloads {
|
||||
node := cluster.Default.GetNodeByID(download.GetNodeID())
|
||||
if node != nil {
|
||||
downloads[key].NodeName = node.DBModel().Name
|
||||
}
|
||||
}
|
||||
|
||||
return serializer.BuildFinishedListResponse(downloads)
|
||||
}
|
||||
|
||||
// Downloading 获取正在下载中的任务
|
||||
func (service *DownloadListService) Downloading(c *gin.Context, user *model.User) serializer.Response {
|
||||
// 查找下载记录
|
||||
downloads := model.GetDownloadsByStatusAndUser(service.Page, user.ID, common.Downloading, common.Seeding, common.Paused, common.Ready)
|
||||
intervals := make(map[uint]int)
|
||||
for key, download := range downloads {
|
||||
if _, ok := intervals[download.ID]; !ok {
|
||||
if node := cluster.Default.GetNodeByID(download.GetNodeID()); node != nil {
|
||||
intervals[download.ID] = node.DBModel().Aria2OptionsSerialized.Interval
|
||||
}
|
||||
}
|
||||
|
||||
node := cluster.Default.GetNodeByID(download.GetNodeID())
|
||||
if node != nil {
|
||||
downloads[key].NodeName = node.DBModel().Name
|
||||
}
|
||||
}
|
||||
|
||||
return serializer.BuildDownloadingResponse(downloads, intervals)
|
||||
}
|
||||
|
||||
// Delete 取消或删除下载任务
|
||||
func (service *DownloadTaskService) Delete(c *gin.Context) serializer.Response {
|
||||
userCtx, _ := c.Get("user")
|
||||
user := userCtx.(*model.User)
|
||||
|
||||
// 查找下载记录
|
||||
download, err := model.GetDownloadByGid(c.Param("gid"), user.ID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotFound, "Download record not found", err)
|
||||
}
|
||||
|
||||
if download.Status >= common.Error && download.Status <= common.Unknown {
|
||||
// 如果任务已完成,则删除任务记录
|
||||
if err := download.Delete(); err != nil {
|
||||
return serializer.DBErr("Failed to delete task record", err)
|
||||
}
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// 取消任务
|
||||
node := cluster.Default.GetNodeByID(download.GetNodeID())
|
||||
if node == nil {
|
||||
return serializer.Err(serializer.CodeNodeOffline, "", err)
|
||||
}
|
||||
|
||||
if err := node.GetAria2Instance().Cancel(download); err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, "Operation failed", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Select 选取要下载的文件
|
||||
func (service *SelectFileService) Select(c *gin.Context) serializer.Response {
|
||||
userCtx, _ := c.Get("user")
|
||||
user := userCtx.(*model.User)
|
||||
|
||||
// 查找下载记录
|
||||
download, err := model.GetDownloadByGid(c.Param("gid"), user.ID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotFound, "Download record not found", err)
|
||||
}
|
||||
|
||||
if download.StatusInfo.BitTorrent.Mode != "multi" || (download.Status != common.Downloading && download.Status != common.Paused) {
|
||||
return serializer.ParamErr("You cannot select files for this task", nil)
|
||||
}
|
||||
|
||||
// 选取下载
|
||||
node := cluster.Default.GetNodeByID(download.GetNodeID())
|
||||
if err := node.GetAria2Instance().Select(download, service.Indexes); err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, "Operation failed", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
|
||||
}
|
||||
|
||||
// SlaveStatus 从机查询离线任务状态
|
||||
func SlaveStatus(c *gin.Context, service *serializer.SlaveAria2Call) serializer.Response {
|
||||
caller, _ := c.Get("MasterAria2Instance")
|
||||
|
||||
// 查询任务
|
||||
status, err := caller.(common.Aria2).Status(service.Task)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to query remote download task status", err)
|
||||
}
|
||||
|
||||
return serializer.NewResponseWithGobData(status)
|
||||
|
||||
}
|
||||
|
||||
// SlaveCancel 取消从机离线下载任务
|
||||
func SlaveCancel(c *gin.Context, service *serializer.SlaveAria2Call) serializer.Response {
|
||||
caller, _ := c.Get("MasterAria2Instance")
|
||||
|
||||
// 查询任务
|
||||
err := caller.(common.Aria2).Cancel(service.Task)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to cancel task", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
|
||||
}
|
||||
|
||||
// SlaveSelect 从机选取离线下载任务文件
|
||||
func SlaveSelect(c *gin.Context, service *serializer.SlaveAria2Call) serializer.Response {
|
||||
caller, _ := c.Get("MasterAria2Instance")
|
||||
|
||||
// 查询任务
|
||||
err := caller.(common.Aria2).Select(service.Task, service.Files)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to select files", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
|
||||
}
|
||||
|
||||
// SlaveSelect 从机选取离线下载任务文件
|
||||
func SlaveDeleteTemp(c *gin.Context, service *serializer.SlaveAria2Call) serializer.Response {
|
||||
caller, _ := c.Get("MasterAria2Instance")
|
||||
|
||||
// 查询任务
|
||||
err := caller.(common.Aria2).DeleteTempFile(service.Task)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to delete temp files", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
|
||||
}
|
||||
178
service/basic/site.go
Normal file
178
service/basic/site.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/cloudreve/Cloudreve/v4/service/user"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mojocn/base64Captcha"
|
||||
)
|
||||
|
||||
// SiteConfig 站点全局设置序列
|
||||
type SiteConfig struct {
|
||||
// Basic Section
|
||||
InstanceID string `json:"instance_id,omitempty"`
|
||||
SiteName string `json:"title,omitempty"`
|
||||
Themes string `json:"themes,omitempty"`
|
||||
DefaultTheme string `json:"default_theme,omitempty"`
|
||||
User *user.User `json:"user,omitempty"`
|
||||
Logo string `json:"logo,omitempty"`
|
||||
LogoLight string `json:"logo_light,omitempty"`
|
||||
|
||||
// Login Section
|
||||
LoginCaptcha bool `json:"login_captcha,omitempty"`
|
||||
RegCaptcha bool `json:"reg_captcha,omitempty"`
|
||||
ForgetCaptcha bool `json:"forget_captcha,omitempty"`
|
||||
Authn bool `json:"authn,omitempty"`
|
||||
ReCaptchaKey string `json:"captcha_ReCaptchaKey,omitempty"`
|
||||
CaptchaType setting.CaptchaType `json:"captcha_type,omitempty"`
|
||||
TurnstileSiteID string `json:"turnstile_site_id,omitempty"`
|
||||
RegisterEnabled bool `json:"register_enabled,omitempty"`
|
||||
TosUrl string `json:"tos_url,omitempty"`
|
||||
PrivacyPolicyUrl string `json:"privacy_policy_url,omitempty"`
|
||||
|
||||
// Explorer section
|
||||
Icons string `json:"icons,omitempty"`
|
||||
EmojiPreset string `json:"emoji_preset,omitempty"`
|
||||
MapProvider setting.MapProvider `json:"map_provider,omitempty"`
|
||||
GoogleMapTileType setting.MapGoogleTileType `json:"google_map_tile_type,omitempty"`
|
||||
FileViewers []setting.ViewerGroup `json:"file_viewers,omitempty"`
|
||||
MaxBatchSize int `json:"max_batch_size,omitempty"`
|
||||
ThumbnailWidth int `json:"thumbnail_width,omitempty"`
|
||||
ThumbnailHeight int `json:"thumbnail_height,omitempty"`
|
||||
|
||||
// App settings
|
||||
AppPromotion bool `json:"app_promotion,omitempty"`
|
||||
|
||||
//EmailActive bool `json:"emailActive"`
|
||||
//QQLogin bool `json:"QQLogin"`
|
||||
//ScoreEnabled bool `json:"score_enabled"`
|
||||
//ShareScoreRate string `json:"share_score_rate"`
|
||||
//HomepageViewMethod string `json:"home_view_method"`
|
||||
//ShareViewMethod string `json:"share_view_method"`
|
||||
//WopiExts []string `json:"wopi_exts"`
|
||||
//AppFeedbackLink string `json:"app_feedback"`
|
||||
//AppForumLink string `json:"app_forum"`
|
||||
}
|
||||
|
||||
type (
|
||||
GetSettingService struct {
|
||||
Section string `uri:"section" binding:"required"`
|
||||
}
|
||||
GetSettingParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *GetSettingService) GetSiteConfig(c *gin.Context) (*SiteConfig, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
settings := dep.SettingProvider()
|
||||
|
||||
switch s.Section {
|
||||
case "login":
|
||||
legalDocs := settings.LegalDocuments(c)
|
||||
return &SiteConfig{
|
||||
LoginCaptcha: settings.LoginCaptchaEnabled(c),
|
||||
RegCaptcha: settings.RegCaptchaEnabled(c),
|
||||
ForgetCaptcha: settings.ForgotPasswordCaptchaEnabled(c),
|
||||
Authn: settings.AuthnEnabled(c),
|
||||
RegisterEnabled: settings.RegisterEnabled(c),
|
||||
PrivacyPolicyUrl: legalDocs.PrivacyPolicy,
|
||||
TosUrl: legalDocs.TermsOfService,
|
||||
}, nil
|
||||
case "explorer":
|
||||
explorerSettings := settings.ExplorerFrontendSettings(c)
|
||||
mapSettings := settings.MapSetting(c)
|
||||
fileViewers := settings.FileViewers(c)
|
||||
maxBatchSize := settings.MaxBatchedFile(c)
|
||||
w, h := settings.ThumbSize(c)
|
||||
for i := range fileViewers {
|
||||
for j := range fileViewers[i].Viewers {
|
||||
fileViewers[i].Viewers[j].WopiActions = nil
|
||||
}
|
||||
}
|
||||
return &SiteConfig{
|
||||
MaxBatchSize: maxBatchSize,
|
||||
FileViewers: fileViewers,
|
||||
Icons: explorerSettings.Icons,
|
||||
MapProvider: mapSettings.Provider,
|
||||
GoogleMapTileType: mapSettings.GoogleTileType,
|
||||
ThumbnailWidth: w,
|
||||
ThumbnailHeight: h,
|
||||
}, nil
|
||||
case "emojis":
|
||||
emojis := settings.EmojiPresets(c)
|
||||
return &SiteConfig{
|
||||
EmojiPreset: emojis,
|
||||
}, nil
|
||||
case "app":
|
||||
appSetting := settings.AppSetting(c)
|
||||
return &SiteConfig{
|
||||
AppPromotion: appSetting.Promotion,
|
||||
}, nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
u := inventory.UserFromContext(c)
|
||||
siteBasic := settings.SiteBasic(c)
|
||||
themes := settings.Theme(c)
|
||||
userRes := user.BuildUser(u, dep.HashIDEncoder())
|
||||
logo := settings.Logo(c)
|
||||
reCaptcha := settings.ReCaptcha(c)
|
||||
appSetting := settings.AppSetting(c)
|
||||
|
||||
return &SiteConfig{
|
||||
InstanceID: siteBasic.ID,
|
||||
SiteName: siteBasic.Name,
|
||||
Themes: themes.Themes,
|
||||
DefaultTheme: themes.DefaultTheme,
|
||||
User: &userRes,
|
||||
Logo: logo.Normal,
|
||||
LogoLight: logo.Light,
|
||||
CaptchaType: settings.CaptchaType(c),
|
||||
TurnstileSiteID: settings.TurnstileCaptcha(c).Key,
|
||||
ReCaptchaKey: reCaptcha.Key,
|
||||
AppPromotion: appSetting.Promotion,
|
||||
}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
CaptchaSessionPrefix = "captcha_session_"
|
||||
CaptchaTTL = 1800 // 30 minutes
|
||||
)
|
||||
|
||||
type (
|
||||
CaptchaResponse struct {
|
||||
Image string `json:"image"`
|
||||
Ticket string `json:"ticket"`
|
||||
}
|
||||
)
|
||||
|
||||
// GetCaptchaImage generates captcha session
|
||||
func GetCaptchaImage(c *gin.Context) *CaptchaResponse {
|
||||
dep := dependency.FromContext(c)
|
||||
captchaSettings := dep.SettingProvider().Captcha(c)
|
||||
var configD = base64Captcha.ConfigCharacter{
|
||||
Height: captchaSettings.Height,
|
||||
Width: captchaSettings.Width,
|
||||
Mode: int(captchaSettings.Mode),
|
||||
ComplexOfNoiseText: captchaSettings.ComplexOfNoiseText,
|
||||
ComplexOfNoiseDot: captchaSettings.ComplexOfNoiseDot,
|
||||
IsShowHollowLine: captchaSettings.IsShowHollowLine,
|
||||
IsShowNoiseDot: captchaSettings.IsShowNoiseDot,
|
||||
IsShowNoiseText: captchaSettings.IsShowNoiseText,
|
||||
IsShowSlimeLine: captchaSettings.IsShowSlimeLine,
|
||||
IsShowSineLine: captchaSettings.IsShowSineLine,
|
||||
CaptchaLen: captchaSettings.Length,
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
idKeyD, capD := base64Captcha.GenerateCaptcha("", configD)
|
||||
|
||||
base64stringD := base64Captcha.CaptchaWriteToBase64Encoding(capD)
|
||||
|
||||
return &CaptchaResponse{
|
||||
Image: base64stringD,
|
||||
Ticket: idKeyD,
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,8 @@
|
||||
package callback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/googledrive"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/onedrive"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// OauthService OAuth 存储策略授权回调服务
|
||||
@@ -23,110 +13,111 @@ type OauthService struct {
|
||||
Scope string `form:"scope"`
|
||||
}
|
||||
|
||||
// GDriveAuth Google Drive 更新认证信息
|
||||
func (service *OauthService) GDriveAuth(c *gin.Context) serializer.Response {
|
||||
if service.Error != "" {
|
||||
return serializer.ParamErr(service.Error, nil)
|
||||
}
|
||||
|
||||
// validate required scope
|
||||
if missing, found := lo.Find[string](googledrive.RequiredScope, func(item string) bool {
|
||||
return !strings.Contains(service.Scope, item)
|
||||
}); found {
|
||||
return serializer.ParamErr(fmt.Sprintf("Missing required scope: %s", missing), nil)
|
||||
}
|
||||
|
||||
policyID, ok := util.GetSession(c, "googledrive_oauth_policy").(uint)
|
||||
if !ok {
|
||||
return serializer.Err(serializer.CodeNotFound, "", nil)
|
||||
}
|
||||
|
||||
util.DeleteSession(c, "googledrive_oauth_policy")
|
||||
|
||||
policy, err := model.GetPolicyByID(policyID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodePolicyNotExist, "", nil)
|
||||
}
|
||||
|
||||
client, err := googledrive.NewClient(&policy)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to initialize Google Drive client", err)
|
||||
}
|
||||
|
||||
credential, err := client.ObtainToken(c, service.Code, "")
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to fetch AccessToken", err)
|
||||
}
|
||||
|
||||
// 更新存储策略的 RefreshToken
|
||||
client.Policy.AccessKey = credential.RefreshToken
|
||||
if err := client.Policy.SaveAndClearCache(); err != nil {
|
||||
return serializer.DBErr("Failed to update RefreshToken", err)
|
||||
}
|
||||
|
||||
cache.Deletes([]string{client.Policy.AccessKey}, googledrive.TokenCachePrefix)
|
||||
return serializer.Response{}
|
||||
}
|
||||
//
|
||||
//// GDriveAuth Google Drive 更新认证信息
|
||||
//func (service *OauthService) GDriveAuth(c *gin.Context) serializer.Response {
|
||||
// if service.Error != "" {
|
||||
// return serializer.ParamErrDeprecated(service.Error, nil)
|
||||
// }
|
||||
//
|
||||
// // validate required scope
|
||||
// if missing, found := lo.Find[string](googledrive.RequiredScope, func(item string) bool {
|
||||
// return !strings.Contains(service.Scope, item)
|
||||
// }); found {
|
||||
// return serializer.ParamErrDeprecated(fmt.Sprintf("Missing required scope: %s", missing), nil)
|
||||
// }
|
||||
//
|
||||
// policyID, ok := util.GetSession(c, "googledrive_oauth_policy").(uint)
|
||||
// if !ok {
|
||||
// return serializer.ErrDeprecated(serializer.CodeNotFound, "", nil)
|
||||
// }
|
||||
//
|
||||
// util.DeleteSession(c, "googledrive_oauth_policy")
|
||||
//
|
||||
// policy, err := model.GetPolicyByID(policyID)
|
||||
// if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodePolicyNotExist, "", nil)
|
||||
// }
|
||||
//
|
||||
// client, err := googledrive.NewClient(&policy)
|
||||
// if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodeInternalSetting, "Failed to initialize Google Drive client", err)
|
||||
// }
|
||||
//
|
||||
// credential, err := client.ObtainToken(c, service.Code, "")
|
||||
// if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodeInternalSetting, "Failed to fetch AccessToken", err)
|
||||
// }
|
||||
//
|
||||
// // 更新存储策略的 RefreshToken
|
||||
// client.Policy.AccessKey = credential.RefreshToken
|
||||
// if err := client.Policy.SaveAndClearCache(); err != nil {
|
||||
// return serializer.DBErrDeprecated("Failed to update RefreshToken", err)
|
||||
// }
|
||||
//
|
||||
// cache.Deletes([]string{client.Policy.AccessKey}, googledrive.TokenCachePrefix)
|
||||
// return serializer.Response{}
|
||||
//}
|
||||
|
||||
// OdAuth OneDrive 更新认证信息
|
||||
func (service *OauthService) OdAuth(c *gin.Context) serializer.Response {
|
||||
if service.Error != "" {
|
||||
return serializer.ParamErr(service.ErrorMsg, nil)
|
||||
}
|
||||
|
||||
policyID, ok := util.GetSession(c, "onedrive_oauth_policy").(uint)
|
||||
if !ok {
|
||||
return serializer.Err(serializer.CodeNotFound, "", nil)
|
||||
}
|
||||
|
||||
util.DeleteSession(c, "onedrive_oauth_policy")
|
||||
|
||||
policy, err := model.GetPolicyByID(policyID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodePolicyNotExist, "", nil)
|
||||
}
|
||||
|
||||
client, err := onedrive.NewClient(&policy)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to initialize OneDrive client", err)
|
||||
}
|
||||
|
||||
credential, err := client.ObtainToken(c, onedrive.WithCode(service.Code))
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to fetch AccessToken", err)
|
||||
}
|
||||
|
||||
// 更新存储策略的 RefreshToken
|
||||
client.Policy.AccessKey = credential.RefreshToken
|
||||
if err := client.Policy.SaveAndClearCache(); err != nil {
|
||||
return serializer.DBErr("Failed to update RefreshToken", err)
|
||||
}
|
||||
|
||||
cache.Deletes([]string{client.Policy.AccessKey}, "onedrive_")
|
||||
if client.Policy.OptionsSerialized.OdDriver != "" && strings.Contains(client.Policy.OptionsSerialized.OdDriver, "http") {
|
||||
if err := querySharePointSiteID(c, client.Policy); err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to query SharePoint site ID", err)
|
||||
}
|
||||
}
|
||||
//if service.Error != "" {
|
||||
// return serializer.ParamErrDeprecated(service.ErrorMsg, nil)
|
||||
//}
|
||||
//
|
||||
//policyID, ok := util.GetSession(c, "onedrive_oauth_policy").(uint)
|
||||
//if !ok {
|
||||
// return serializer.ErrDeprecated(serializer.CodeNotFound, "", nil)
|
||||
//}
|
||||
//
|
||||
//util.DeleteSession(c, "onedrive_oauth_policy")
|
||||
//
|
||||
//policy, err := model.GetPolicyByID(policyID)
|
||||
//if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodePolicyNotExist, "", nil)
|
||||
//}
|
||||
//
|
||||
//client, err := onedrive.NewClient(&policy)
|
||||
//if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodeInternalSetting, "Failed to initialize OneDrive client", err)
|
||||
//}
|
||||
//
|
||||
//credential, err := client.ObtainToken(c, onedrive.WithCode(service.Code))
|
||||
//if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodeInternalSetting, "Failed to fetch AccessToken", err)
|
||||
//}
|
||||
//
|
||||
//// 更新存储策略的 RefreshToken
|
||||
//client.Policy.AccessKey = credential.RefreshToken
|
||||
//if err := client.Policy.SaveAndClearCache(); err != nil {
|
||||
// return serializer.DBErrDeprecated("Failed to update RefreshToken", err)
|
||||
//}
|
||||
//
|
||||
//cache.Deletes([]string{client.Policy.AccessKey}, "onedrive_")
|
||||
//if client.Policy.OptionsSerialized.OdDriver != "" && strings.Contains(client.Policy.OptionsSerialized.OdDriver, "http") {
|
||||
// if err := querySharePointSiteID(c, client.Policy); err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodeInternalSetting, "Failed to query SharePoint basic ID", err)
|
||||
// }
|
||||
//}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
func querySharePointSiteID(ctx context.Context, policy *model.Policy) error {
|
||||
client, err := onedrive.NewClient(policy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//func querySharePointSiteID(ctx context.Context, policy *model.Policy) error {
|
||||
//client, err := onedrive.NewClient(policy)
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
//
|
||||
//id, err := client.GetSiteIDByURL(ctx, client.Policy.OptionsSerialized.OdDriver)
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
//
|
||||
//client.Policy.OptionsSerialized.OdDriver = fmt.Sprintf("sites/%s/drive", id)
|
||||
//if err := client.Policy.SaveAndClearCache(); err != nil {
|
||||
// return err
|
||||
//}
|
||||
|
||||
id, err := client.GetSiteIDByURL(ctx, client.Policy.OptionsSerialized.OdDriver)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client.Policy.OptionsSerialized.OdDriver = fmt.Sprintf("sites/%s/drive", id)
|
||||
if err := client.Policy.SaveAndClearCache(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
//return nil
|
||||
//}
|
||||
|
||||
@@ -1,33 +1,17 @@
|
||||
package callback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/cos"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/onedrive"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/s3"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/onedrive"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CallbackProcessService 上传请求回调正文接口
|
||||
type CallbackProcessService interface {
|
||||
GetBody() serializer.UploadCallback
|
||||
}
|
||||
|
||||
// RemoteUploadCallbackService 远程存储上传回调请求服务
|
||||
type RemoteUploadCallbackService struct {
|
||||
Data serializer.UploadCallback `json:"data" binding:"required"`
|
||||
}
|
||||
|
||||
// GetBody 返回回调正文
|
||||
func (service RemoteUploadCallbackService) GetBody() serializer.UploadCallback {
|
||||
return service.Data
|
||||
}
|
||||
|
||||
// UploadCallbackService OOS/七牛云存储上传回调请求服务
|
||||
@@ -35,7 +19,7 @@ type UploadCallbackService struct {
|
||||
Name string `json:"name"`
|
||||
SourceName string `json:"source_name"`
|
||||
PicInfo string `json:"pic_info"`
|
||||
Size uint64 `json:"size"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// UpyunCallbackService 又拍云上传回调请求服务
|
||||
@@ -53,209 +37,109 @@ type OneDriveCallback struct {
|
||||
Meta *onedrive.FileInfo
|
||||
}
|
||||
|
||||
// COSCallback COS 客户端回调正文
|
||||
type COSCallback struct {
|
||||
Bucket string `form:"bucket"`
|
||||
Etag string `form:"etag"`
|
||||
}
|
||||
|
||||
// S3Callback S3 客户端回调正文
|
||||
type S3Callback struct {
|
||||
}
|
||||
|
||||
// GetBody 返回回调正文
|
||||
func (service UpyunCallbackService) GetBody() serializer.UploadCallback {
|
||||
res := serializer.UploadCallback{}
|
||||
if service.Width != "" {
|
||||
res.PicInfo = service.Width + "," + service.Height
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// GetBody 返回回调正文
|
||||
func (service UploadCallbackService) GetBody() serializer.UploadCallback {
|
||||
return serializer.UploadCallback{
|
||||
PicInfo: service.PicInfo,
|
||||
}
|
||||
}
|
||||
|
||||
// GetBody 返回回调正文
|
||||
func (service OneDriveCallback) GetBody() serializer.UploadCallback {
|
||||
var picInfo = "0,0"
|
||||
if service.Meta.Image.Width != 0 {
|
||||
picInfo = fmt.Sprintf("%d,%d", service.Meta.Image.Width, service.Meta.Image.Height)
|
||||
}
|
||||
return serializer.UploadCallback{
|
||||
PicInfo: picInfo,
|
||||
}
|
||||
}
|
||||
|
||||
// GetBody 返回回调正文
|
||||
func (service COSCallback) GetBody() serializer.UploadCallback {
|
||||
return serializer.UploadCallback{
|
||||
PicInfo: "",
|
||||
}
|
||||
}
|
||||
|
||||
// GetBody 返回回调正文
|
||||
func (service S3Callback) GetBody() serializer.UploadCallback {
|
||||
return serializer.UploadCallback{
|
||||
PicInfo: "",
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessCallback 处理上传结果回调
|
||||
func ProcessCallback(service CallbackProcessService, c *gin.Context) serializer.Response {
|
||||
callbackBody := service.GetBody()
|
||||
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromCallback(c)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, err.Error(), err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
func ProcessCallback(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
m := manager.NewFileManager(dep, user)
|
||||
defer m.Recycle()
|
||||
|
||||
// 获取上传会话
|
||||
uploadSession := c.MustGet(filesystem.UploadSessionCtx).(*serializer.UploadSession)
|
||||
uploadSession := c.MustGet(manager.UploadSessionCtx).(*fs.UploadSession)
|
||||
|
||||
// 查找上传会话创建的占位文件
|
||||
file, err := model.GetFilesByUploadSession(uploadSession.Key, fs.User.ID)
|
||||
_, err := m.CompleteUpload(c, uploadSession)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeUploadSessionExpired, "LocalUpload session file placeholder not exist", err)
|
||||
return fmt.Errorf("failed to complete upload: %w", err)
|
||||
}
|
||||
|
||||
fileData := fsctx.FileStream{
|
||||
Size: uploadSession.Size,
|
||||
Name: uploadSession.Name,
|
||||
VirtualPath: uploadSession.VirtualPath,
|
||||
SavePath: uploadSession.SavePath,
|
||||
Mode: fsctx.Nop,
|
||||
Model: file,
|
||||
LastModified: uploadSession.LastModified,
|
||||
}
|
||||
|
||||
// 占位符未扣除容量需要校验和扣除
|
||||
if !fs.Policy.IsUploadPlaceholderWithSize() {
|
||||
fs.Use("AfterUpload", filesystem.HookValidateCapacity)
|
||||
fs.Use("AfterUpload", filesystem.HookChunkUploaded)
|
||||
}
|
||||
|
||||
fs.Use("AfterUpload", filesystem.HookPopPlaceholderToFile(callbackBody.PicInfo))
|
||||
fs.Use("AfterValidateFailed", filesystem.HookDeleteTempFile)
|
||||
err = fs.Upload(context.Background(), &fileData)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeUploadFailed, err.Error(), err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PreProcess 对OneDrive客户端回调进行预处理验证
|
||||
func (service *OneDriveCallback) PreProcess(c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromCallback(c)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
//// PreProcess 对OneDrive客户端回调进行预处理验证
|
||||
//func (service *OneDriveCallback) PreProcess(c *gin.Context) serializer.Response {
|
||||
// // 创建文件系统
|
||||
// fs, err := filesystem.NewFileSystemFromCallback(c)
|
||||
// if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodeCreateFSError, "", err)
|
||||
// }
|
||||
// defer fs.Recycle()
|
||||
//
|
||||
// // 获取回调会话
|
||||
// uploadSession := c.MustGet(filesystem.UploadSessionCtx).(*serializer.UploadSession)
|
||||
//
|
||||
// // 获取文件信息
|
||||
// info, err := fs.Handler.(onedrive.Driver).Client.Meta(context.Background(), "", uploadSession.SavePath)
|
||||
// if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodeQueryMetaFailed, "", err)
|
||||
// }
|
||||
//
|
||||
// // 验证与回调会话中是否一致
|
||||
// actualPath := strings.TrimPrefix(uploadSession.SavePath, "/")
|
||||
// isSizeCheckFailed := uploadSession.Size != info.Size
|
||||
//
|
||||
// // SharePoint 会对 Office 文档增加 meta data 导致文件大小不一致,这里增加 1 MB 宽容
|
||||
// // See: https://github.com/OneDrive/onedrive-api-docs/issues/935
|
||||
// if (strings.Contains(fs.Policy.OptionsSerialized.OdDriver, "sharepoint.com") || strings.Contains(fs.Policy.OptionsSerialized.OdDriver, "sharepoint.cn")) && isSizeCheckFailed && (info.Size > uploadSession.Size) && (info.Size-uploadSession.Size <= 1048576) {
|
||||
// isSizeCheckFailed = false
|
||||
// }
|
||||
//
|
||||
// if isSizeCheckFailed || !strings.EqualFold(info.GetSourcePath(), actualPath) {
|
||||
// fs.Handler.(onedrive.Driver).Client.Delete(context.Background(), []string{info.GetSourcePath()})
|
||||
// return serializer.ErrDeprecated(serializer.CodeMetaMismatch, "", err)
|
||||
// }
|
||||
// service.Meta = info
|
||||
// return ProcessCallback(c)
|
||||
//}
|
||||
//
|
||||
|
||||
// 获取回调会话
|
||||
uploadSession := c.MustGet(filesystem.UploadSessionCtx).(*serializer.UploadSession)
|
||||
|
||||
// 获取文件信息
|
||||
info, err := fs.Handler.(onedrive.Driver).Client.Meta(context.Background(), "", uploadSession.SavePath)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeQueryMetaFailed, "", err)
|
||||
}
|
||||
|
||||
// 验证与回调会话中是否一致
|
||||
actualPath := strings.TrimPrefix(uploadSession.SavePath, "/")
|
||||
isSizeCheckFailed := uploadSession.Size != info.Size
|
||||
|
||||
// SharePoint 会对 Office 文档增加 meta data 导致文件大小不一致,这里增加 1 MB 宽容
|
||||
// See: https://github.com/OneDrive/onedrive-api-docs/issues/935
|
||||
if (strings.Contains(fs.Policy.OptionsSerialized.OdDriver, "sharepoint.com") || strings.Contains(fs.Policy.OptionsSerialized.OdDriver, "sharepoint.cn")) && isSizeCheckFailed && (info.Size > uploadSession.Size) && (info.Size-uploadSession.Size <= 1048576) {
|
||||
isSizeCheckFailed = false
|
||||
}
|
||||
|
||||
if isSizeCheckFailed || !strings.EqualFold(info.GetSourcePath(), actualPath) {
|
||||
fs.Handler.(onedrive.Driver).Client.Delete(context.Background(), []string{info.GetSourcePath()})
|
||||
return serializer.Err(serializer.CodeMetaMismatch, "", err)
|
||||
}
|
||||
service.Meta = info
|
||||
return ProcessCallback(service, c)
|
||||
}
|
||||
|
||||
// PreProcess 对COS客户端回调进行预处理
|
||||
func (service *COSCallback) PreProcess(c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromCallback(c)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 获取回调会话
|
||||
uploadSession := c.MustGet(filesystem.UploadSessionCtx).(*serializer.UploadSession)
|
||||
|
||||
// 获取文件信息
|
||||
info, err := fs.Handler.(cos.Driver).Meta(context.Background(), uploadSession.SavePath)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeMetaMismatch, "", err)
|
||||
}
|
||||
|
||||
// 验证实际文件信息与回调会话中是否一致
|
||||
if uploadSession.Size != info.Size || uploadSession.Key != info.CallbackKey {
|
||||
return serializer.Err(serializer.CodeMetaMismatch, "", err)
|
||||
}
|
||||
|
||||
return ProcessCallback(service, c)
|
||||
}
|
||||
|
||||
// PreProcess 对S3客户端回调进行预处理
|
||||
func (service *S3Callback) PreProcess(c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromCallback(c)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 获取回调会话
|
||||
uploadSession := c.MustGet(filesystem.UploadSessionCtx).(*serializer.UploadSession)
|
||||
|
||||
// 获取文件信息
|
||||
info, err := fs.Handler.(*s3.Driver).Meta(context.Background(), uploadSession.SavePath)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeMetaMismatch, "", err)
|
||||
}
|
||||
|
||||
// 验证实际文件信息与回调会话中是否一致
|
||||
if uploadSession.Size != info.Size {
|
||||
return serializer.Err(serializer.CodeMetaMismatch, "", err)
|
||||
}
|
||||
|
||||
return ProcessCallback(service, c)
|
||||
}
|
||||
|
||||
// PreProcess 对从机客户端回调进行预处理验证
|
||||
func (service *UploadCallbackService) PreProcess(c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromCallback(c)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 获取回调会话
|
||||
uploadSession := c.MustGet(filesystem.UploadSessionCtx).(*serializer.UploadSession)
|
||||
|
||||
// 验证文件大小
|
||||
if uploadSession.Size != service.Size {
|
||||
fs.Handler.Delete(context.Background(), []string{uploadSession.SavePath})
|
||||
return serializer.Err(serializer.CodeMetaMismatch, "", err)
|
||||
}
|
||||
|
||||
return ProcessCallback(service, c)
|
||||
}
|
||||
//
|
||||
//// PreProcess 对S3客户端回调进行预处理
|
||||
//func (service *S3Callback) PreProcess(c *gin.Context) serializer.Response {
|
||||
// // 创建文件系统
|
||||
// fs, err := filesystem.NewFileSystemFromCallback(c)
|
||||
// if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodeCreateFSError, "", err)
|
||||
// }
|
||||
// defer fs.Recycle()
|
||||
//
|
||||
// // 获取回调会话
|
||||
// uploadSession := c.MustGet(filesystem.UploadSessionCtx).(*serializer.UploadSession)
|
||||
//
|
||||
// // 获取文件信息
|
||||
// info, err := fs.Handler.(*s3.Driver).Meta(context.Background(), uploadSession.SavePath)
|
||||
// if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodeMetaMismatch, "", err)
|
||||
// }
|
||||
//
|
||||
// // 验证实际文件信息与回调会话中是否一致
|
||||
// if uploadSession.Size != info.Size {
|
||||
// return serializer.ErrDeprecated(serializer.CodeMetaMismatch, "", err)
|
||||
// }
|
||||
//
|
||||
// return ProcessCallback(service, c)
|
||||
//}
|
||||
//
|
||||
//// PreProcess 对OneDrive客户端回调进行预处理验证
|
||||
//func (service *UploadCallbackService) PreProcess(c *gin.Context) serializer.Response {
|
||||
// // 创建文件系统
|
||||
// fs, err := filesystem.NewFileSystemFromCallback(c)
|
||||
// if err != nil {
|
||||
// return serializer.ErrDeprecated(serializer.CodeCreateFSError, "", err)
|
||||
// }
|
||||
// defer fs.Recycle()
|
||||
//
|
||||
// // 获取回调会话
|
||||
// uploadSession := c.MustGet(filesystem.UploadSessionCtx).(*serializer.UploadSession)
|
||||
//
|
||||
// // 验证文件大小
|
||||
// if uploadSession.Size != service.Size {
|
||||
// fs.Handler.Delete(context.Background(), []string{uploadSession.SavePath})
|
||||
// return serializer.ErrDeprecated(serializer.CodeMetaMismatch, "", err)
|
||||
// }
|
||||
//
|
||||
// return ProcessCallback(service, c)
|
||||
//}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package explorer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DirectoryService 创建新目录服务
|
||||
type DirectoryService struct {
|
||||
Path string `uri:"path" json:"path" binding:"required,min=1,max=65535"`
|
||||
}
|
||||
|
||||
// ListDirectory 列出目录内容
|
||||
func (service *DirectoryService) ListDirectory(c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromContext(c)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 上下文
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// 获取子项目
|
||||
objects, err := fs.List(ctx, service.Path, nil)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
|
||||
}
|
||||
|
||||
var parentID uint
|
||||
if len(fs.DirTarget) > 0 {
|
||||
parentID = fs.DirTarget[0].ID
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Code: 0,
|
||||
Data: serializer.BuildObjectList(parentID, objects, fs.Policy),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDirectory 创建目录
|
||||
func (service *DirectoryService) CreateDirectory(c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromContext(c)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 上下文
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// 创建目录
|
||||
_, err = fs.CreateDirectory(ctx, service.Path)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFolderFailed, err.Error(), err)
|
||||
}
|
||||
return serializer.Response{
|
||||
Code: 0,
|
||||
}
|
||||
|
||||
}
|
||||
118
service/explorer/entity.go
Normal file
118
service/explorer/entity.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package explorer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type (
|
||||
EntityDownloadParameterCtx struct{}
|
||||
EntityDownloadService struct {
|
||||
Name string `uri:"name" binding:"required"`
|
||||
SpeedLimit int64 `uri:"speed"`
|
||||
Src string `uri:"src"`
|
||||
}
|
||||
)
|
||||
|
||||
// Serve serves file content
|
||||
func (s *EntityDownloadService) Serve(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
m := manager.NewFileManager(dep, user)
|
||||
defer m.Recycle()
|
||||
|
||||
entitySource, err := m.GetEntitySource(c, hashid.FromContext(c))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get entity source: %w", err)
|
||||
}
|
||||
|
||||
defer entitySource.Close()
|
||||
|
||||
// Set cache header for public resource
|
||||
settings := dep.SettingProvider()
|
||||
maxAge := settings.PublicResourceMaxAge(c)
|
||||
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
|
||||
|
||||
isDownload := c.Query(routes.IsDownloadQuery) != ""
|
||||
isThumb := c.Query(routes.IsThumbQuery) != ""
|
||||
entitySource.Serve(c.Writer, c.Request,
|
||||
entitysource.WithSpeedLimit(s.SpeedLimit),
|
||||
entitysource.WithDownload(isDownload),
|
||||
entitysource.WithDisplayName(s.Name),
|
||||
entitysource.WithContext(c),
|
||||
entitysource.WithThumb(isThumb),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
SetCurrentVersionParamCtx struct{}
|
||||
SetCurrentVersionService struct {
|
||||
Uri string `uri:"uri" binding:"required"`
|
||||
Version string `uri:"version" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
// Set sets the current version of the file
|
||||
func (s *SetCurrentVersionService) Set(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
m := manager.NewFileManager(dep, user)
|
||||
defer m.Recycle()
|
||||
|
||||
uri, err := fs.NewUriFromString(s.Uri)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
|
||||
}
|
||||
|
||||
versionId, err := dep.HashIDEncoder().Decode(s.Version, hashid.EntityID)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeParamErr, "unknown version id", err)
|
||||
}
|
||||
|
||||
if err := m.SetCurrentVersion(c, uri, versionId); err != nil {
|
||||
return fmt.Errorf("failed to set current version: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
DeleteVersionParamCtx struct{}
|
||||
DeleteVersionService struct {
|
||||
Uri string `uri:"uri" binding:"required"`
|
||||
Version string `uri:"version" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
// Delete deletes the version of the file
|
||||
func (s *DeleteVersionService) Delete(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
m := manager.NewFileManager(dep, user)
|
||||
defer m.Recycle()
|
||||
|
||||
uri, err := fs.NewUriFromString(s.Uri)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
|
||||
}
|
||||
|
||||
versionId, err := dep.HashIDEncoder().Decode(s.Version, hashid.EntityID)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeParamErr, "unknown version id", err)
|
||||
}
|
||||
|
||||
if err := m.DeleteVersion(c, uri, versionId); err != nil {
|
||||
return fmt.Errorf("failed to delete version: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
37
service/explorer/metadata.go
Normal file
37
service/explorer/metadata.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package explorer
|
||||
|
||||
import (
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type (
|
||||
PatchMetadataService struct {
|
||||
Uris []string `json:"uris" binding:"required"`
|
||||
Patches []fs.MetadataPatch `json:"patches" binding:"required,dive"`
|
||||
}
|
||||
|
||||
PatchMetadataParameterCtx struct{}
|
||||
)
|
||||
|
||||
func (s *PatchMetadataService) GetUris() []string {
|
||||
return s.Uris
|
||||
}
|
||||
|
||||
func (s *PatchMetadataService) Patch(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
m := manager.NewFileManager(dep, user)
|
||||
defer m.Recycle()
|
||||
|
||||
uris, err := fs.NewUriFromStrings(s.Uris...)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
|
||||
}
|
||||
|
||||
return m.PatchMedata(c, uris, s.Patches...)
|
||||
}
|
||||
@@ -1,467 +0,0 @@
|
||||
package explorer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"math"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/task"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ItemMoveService 处理多文件/目录移动
|
||||
type ItemMoveService struct {
|
||||
SrcDir string `json:"src_dir" binding:"required,min=1,max=65535"`
|
||||
Src ItemIDService `json:"src"`
|
||||
Dst string `json:"dst" binding:"required,min=1,max=65535"`
|
||||
}
|
||||
|
||||
// ItemRenameService 处理多文件/目录重命名
|
||||
type ItemRenameService struct {
|
||||
Src ItemIDService `json:"src"`
|
||||
NewName string `json:"new_name" binding:"required,min=1,max=255"`
|
||||
}
|
||||
|
||||
// ItemService 处理多文件/目录相关服务
|
||||
type ItemService struct {
|
||||
Items []uint `json:"items"`
|
||||
Dirs []uint `json:"dirs"`
|
||||
}
|
||||
|
||||
// ItemIDService 处理多文件/目录相关服务,字段值为HashID,可通过Raw()方法获取原始ID
|
||||
type ItemIDService struct {
|
||||
Items []string `json:"items"`
|
||||
Dirs []string `json:"dirs"`
|
||||
Source *ItemService
|
||||
Force bool `json:"force"`
|
||||
UnlinkOnly bool `json:"unlink"`
|
||||
}
|
||||
|
||||
// ItemCompressService 文件压缩任务服务
|
||||
type ItemCompressService struct {
|
||||
Src ItemIDService `json:"src"`
|
||||
Dst string `json:"dst" binding:"required,min=1,max=65535"`
|
||||
Name string `json:"name" binding:"required,min=1,max=255"`
|
||||
}
|
||||
|
||||
// ItemDecompressService 文件解压缩任务服务
|
||||
type ItemDecompressService struct {
|
||||
Src string `json:"src"`
|
||||
Dst string `json:"dst" binding:"required,min=1,max=65535"`
|
||||
Encoding string `json:"encoding"`
|
||||
}
|
||||
|
||||
// ItemPropertyService 获取对象属性服务
|
||||
type ItemPropertyService struct {
|
||||
ID string `binding:"required"`
|
||||
TraceRoot bool `form:"trace_root"`
|
||||
IsFolder bool `form:"is_folder"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register(ItemIDService{})
|
||||
}
|
||||
|
||||
// Raw 批量解码HashID,获取原始ID
|
||||
func (service *ItemIDService) Raw() *ItemService {
|
||||
if service.Source != nil {
|
||||
return service.Source
|
||||
}
|
||||
|
||||
service.Source = &ItemService{
|
||||
Dirs: make([]uint, 0, len(service.Dirs)),
|
||||
Items: make([]uint, 0, len(service.Items)),
|
||||
}
|
||||
for _, folder := range service.Dirs {
|
||||
id, err := hashid.DecodeHashID(folder, hashid.FolderID)
|
||||
if err == nil {
|
||||
service.Source.Dirs = append(service.Source.Dirs, id)
|
||||
}
|
||||
}
|
||||
for _, file := range service.Items {
|
||||
id, err := hashid.DecodeHashID(file, hashid.FileID)
|
||||
if err == nil {
|
||||
service.Source.Items = append(service.Source.Items, id)
|
||||
}
|
||||
}
|
||||
|
||||
return service.Source
|
||||
}
|
||||
|
||||
// CreateDecompressTask 创建文件解压缩任务
|
||||
func (service *ItemDecompressService) CreateDecompressTask(c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromContext(c)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 检查用户组权限
|
||||
if !fs.User.Group.OptionsSerialized.ArchiveTask {
|
||||
return serializer.Err(serializer.CodeGroupNotAllowed, "", nil)
|
||||
}
|
||||
|
||||
// 存放目录是否存在
|
||||
if exist, _ := fs.IsPathExist(service.Dst); !exist {
|
||||
return serializer.Err(serializer.CodeParentNotExist, "", nil)
|
||||
}
|
||||
|
||||
// 压缩包是否存在
|
||||
exist, file := fs.IsFileExist(service.Src)
|
||||
if !exist {
|
||||
return serializer.Err(serializer.CodeFileNotFound, "", nil)
|
||||
}
|
||||
|
||||
// 文件尺寸限制
|
||||
if fs.User.Group.OptionsSerialized.DecompressSize != 0 && file.Size > fs.User.Group.
|
||||
OptionsSerialized.DecompressSize {
|
||||
return serializer.Err(serializer.CodeFileTooLarge, "", nil)
|
||||
}
|
||||
|
||||
// 支持的压缩格式后缀
|
||||
var (
|
||||
suffixes = []string{".zip", ".gz", ".xz", ".tar", ".rar"}
|
||||
matched bool
|
||||
)
|
||||
for _, suffix := range suffixes {
|
||||
if strings.HasSuffix(file.Name, suffix) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return serializer.Err(serializer.CodeUnsupportedArchiveType, "", nil)
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
job, err := task.NewDecompressTask(fs.User, service.Src, service.Dst, service.Encoding)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateTaskError, "", err)
|
||||
}
|
||||
task.TaskPoll.Submit(job)
|
||||
|
||||
return serializer.Response{}
|
||||
|
||||
}
|
||||
|
||||
// CreateCompressTask 创建文件压缩任务
|
||||
func (service *ItemCompressService) CreateCompressTask(c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromContext(c)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 检查用户组权限
|
||||
if !fs.User.Group.OptionsSerialized.ArchiveTask {
|
||||
return serializer.Err(serializer.CodeGroupNotAllowed, "", nil)
|
||||
}
|
||||
|
||||
// 补齐压缩文件扩展名(如果没有)
|
||||
if !strings.HasSuffix(service.Name, ".zip") {
|
||||
service.Name += ".zip"
|
||||
}
|
||||
|
||||
// 存放目录是否存在,是否重名
|
||||
if exist, _ := fs.IsPathExist(service.Dst); !exist {
|
||||
return serializer.Err(serializer.CodeParentNotExist, "", nil)
|
||||
}
|
||||
if exist, _ := fs.IsFileExist(path.Join(service.Dst, service.Name)); exist {
|
||||
return serializer.ParamErr("File "+service.Name+" already exist", nil)
|
||||
}
|
||||
|
||||
// 检查文件名合法性
|
||||
if !fs.ValidateLegalName(context.Background(), service.Name) {
|
||||
return serializer.Err(serializer.CodeIllegalObjectName, "", nil)
|
||||
}
|
||||
if !fs.ValidateExtension(context.Background(), service.Name) {
|
||||
return serializer.Err(serializer.CodeFileTypeNotAllowed, "", nil)
|
||||
}
|
||||
|
||||
// 递归列出待压缩子目录
|
||||
folders, err := model.GetRecursiveChildFolder(service.Src.Raw().Dirs, fs.User.ID, true)
|
||||
if err != nil {
|
||||
return serializer.DBErr("Failed to list folders", err)
|
||||
}
|
||||
|
||||
// 列出所有待压缩文件
|
||||
files, err := model.GetChildFilesOfFolders(&folders)
|
||||
if err != nil {
|
||||
return serializer.DBErr("Failed to list files", err)
|
||||
}
|
||||
|
||||
// 计算待压缩文件大小
|
||||
var totalSize uint64
|
||||
for i := 0; i < len(files); i++ {
|
||||
totalSize += files[i].Size
|
||||
}
|
||||
|
||||
// 文件尺寸限制
|
||||
if fs.User.Group.OptionsSerialized.CompressSize != 0 && totalSize > fs.User.Group.
|
||||
OptionsSerialized.CompressSize {
|
||||
return serializer.Err(serializer.CodeFileTooLarge, "", nil)
|
||||
}
|
||||
|
||||
// 按照平均压缩率计算用户空间是否足够
|
||||
compressRatio := 0.4
|
||||
spaceNeeded := uint64(math.Round(float64(totalSize) * compressRatio))
|
||||
if fs.User.GetRemainingCapacity() < spaceNeeded {
|
||||
return serializer.Err(serializer.CodeInsufficientCapacity, "", err)
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
job, err := task.NewCompressTask(fs.User, path.Join(service.Dst, service.Name), service.Src.Raw().Dirs,
|
||||
service.Src.Raw().Items)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateTaskError, "", err)
|
||||
}
|
||||
task.TaskPoll.Submit(job)
|
||||
|
||||
return serializer.Response{}
|
||||
|
||||
}
|
||||
|
||||
// Archive 创建归档
|
||||
func (service *ItemIDService) Archive(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromContext(c)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 检查用户组权限
|
||||
if !fs.User.Group.OptionsSerialized.ArchiveDownload {
|
||||
return serializer.Err(serializer.CodeGroupNotAllowed, "", nil)
|
||||
}
|
||||
|
||||
// 创建打包下载会话
|
||||
ttl := model.GetIntSetting("archive_timeout", 30)
|
||||
downloadSessionID := util.RandStringRunes(16)
|
||||
cache.Set("archive_"+downloadSessionID, *service, ttl)
|
||||
cache.Set("archive_user_"+downloadSessionID, *fs.User, ttl)
|
||||
signURL, err := auth.SignURI(
|
||||
auth.General,
|
||||
fmt.Sprintf("/api/v3/file/archive/%s/archive.zip", downloadSessionID),
|
||||
int64(ttl),
|
||||
)
|
||||
|
||||
return serializer.Response{
|
||||
Code: 0,
|
||||
Data: signURL.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// Delete 删除对象
|
||||
func (service *ItemIDService) Delete(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromContext(c)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
force, unlink := false, false
|
||||
if fs.User.Group.OptionsSerialized.AdvanceDelete {
|
||||
force = service.Force
|
||||
unlink = service.UnlinkOnly
|
||||
}
|
||||
|
||||
// 删除对象
|
||||
items := service.Raw()
|
||||
err = fs.Delete(ctx, items.Dirs, items.Items, force, unlink)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Code: 0,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Move 移动对象
|
||||
func (service *ItemMoveService) Move(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromContext(c)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 移动对象
|
||||
items := service.Src.Raw()
|
||||
err = fs.Move(ctx, items.Dirs, items.Items, service.SrcDir, service.Dst)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Code: 0,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Copy 复制对象
|
||||
func (service *ItemMoveService) Copy(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
// 复制操作只能对一个目录或文件对象进行操作
|
||||
if len(service.Src.Items)+len(service.Src.Dirs) > 1 {
|
||||
return filesystem.ErrOneObjectOnly
|
||||
}
|
||||
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromContext(c)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 复制对象
|
||||
err = fs.Copy(ctx, service.Src.Raw().Dirs, service.Src.Raw().Items, service.SrcDir, service.Dst)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Code: 0,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Rename 重命名对象
|
||||
func (service *ItemRenameService) Rename(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
// 重命名作只能对一个目录或文件对象进行操作
|
||||
if len(service.Src.Items)+len(service.Src.Dirs) > 1 {
|
||||
return filesystem.ErrOneObjectOnly
|
||||
}
|
||||
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromContext(c)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 重命名对象
|
||||
err = fs.Rename(ctx, service.Src.Raw().Dirs, service.Src.Raw().Items, service.NewName)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Code: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// GetProperty 获取对象的属性
|
||||
func (service *ItemPropertyService) GetProperty(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
userCtx, _ := c.Get("user")
|
||||
user := userCtx.(*model.User)
|
||||
|
||||
var props serializer.ObjectProps
|
||||
props.QueryDate = time.Now()
|
||||
|
||||
// 如果是文件对象
|
||||
if !service.IsFolder {
|
||||
res, err := hashid.DecodeHashID(service.ID, hashid.FileID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotFound, "", err)
|
||||
}
|
||||
|
||||
file, err := model.GetFilesByIDs([]uint{res}, user.ID)
|
||||
if err != nil {
|
||||
return serializer.DBErr("Failed to query file records", err)
|
||||
}
|
||||
|
||||
props.CreatedAt = file[0].CreatedAt
|
||||
props.UpdatedAt = file[0].UpdatedAt
|
||||
props.Policy = file[0].GetPolicy().Name
|
||||
props.Size = file[0].Size
|
||||
|
||||
// 查找父目录
|
||||
if service.TraceRoot {
|
||||
parent, err := model.GetFoldersByIDs([]uint{file[0].FolderID}, user.ID)
|
||||
if err != nil {
|
||||
return serializer.DBErr("Parent folder record not exist", err)
|
||||
}
|
||||
|
||||
if err := parent[0].TraceRoot(); err != nil {
|
||||
return serializer.DBErr("Failed to trace root folder", err)
|
||||
}
|
||||
|
||||
props.Path = path.Join(parent[0].Position, parent[0].Name)
|
||||
}
|
||||
} else {
|
||||
res, err := hashid.DecodeHashID(service.ID, hashid.FolderID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotFound, "", err)
|
||||
}
|
||||
|
||||
folder, err := model.GetFoldersByIDs([]uint{res}, user.ID)
|
||||
if err != nil {
|
||||
return serializer.DBErr("Failed to query folder records", err)
|
||||
}
|
||||
|
||||
props.CreatedAt = folder[0].CreatedAt
|
||||
props.UpdatedAt = folder[0].UpdatedAt
|
||||
|
||||
// 如果对象是目录, 先尝试返回缓存结果
|
||||
if cacheRes, ok := cache.Get(fmt.Sprintf("folder_props_%d", res)); ok {
|
||||
res := cacheRes.(serializer.ObjectProps)
|
||||
res.CreatedAt = props.CreatedAt
|
||||
res.UpdatedAt = props.UpdatedAt
|
||||
return serializer.Response{Data: res}
|
||||
}
|
||||
|
||||
// 统计子目录
|
||||
childFolders, err := model.GetRecursiveChildFolder([]uint{folder[0].ID},
|
||||
user.ID, true)
|
||||
if err != nil {
|
||||
return serializer.DBErr("Failed to list child folders", err)
|
||||
}
|
||||
props.ChildFolderNum = len(childFolders) - 1
|
||||
|
||||
// 统计子文件
|
||||
files, err := model.GetChildFilesOfFolders(&childFolders)
|
||||
if err != nil {
|
||||
return serializer.DBErr("Failed to list child files", err)
|
||||
}
|
||||
|
||||
// 统计子文件个数和大小
|
||||
props.ChildFileNum = len(files)
|
||||
for i := 0; i < len(files); i++ {
|
||||
props.Size += files[i].Size
|
||||
}
|
||||
|
||||
// 查找父目录
|
||||
if service.TraceRoot {
|
||||
if err := folder[0].TraceRoot(); err != nil {
|
||||
return serializer.DBErr("Failed to list child folders", err)
|
||||
}
|
||||
|
||||
props.Path = folder[0].Position
|
||||
}
|
||||
|
||||
// 如果列取对象是目录,则缓存结果
|
||||
cache.Set(fmt.Sprintf("folder_props_%d", res), props,
|
||||
model.GetIntSetting("folder_props_timeout", 300))
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Code: 0,
|
||||
Data: props,
|
||||
}
|
||||
}
|
||||
75
service/explorer/pin.go
Normal file
75
service/explorer/pin.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package explorer
|
||||
|
||||
import (
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type (
|
||||
PinFileService struct {
|
||||
Uri string `json:"uri" binding:"required"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
PinFileParameterCtx struct{}
|
||||
)
|
||||
|
||||
// PinFileService pins new uri to sidebar
|
||||
func (service *PinFileService) PinFile(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
|
||||
uri, err := fs.NewUriFromString(service.Uri)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
|
||||
}
|
||||
|
||||
uriStr := uri.String()
|
||||
for _, pin := range user.Settings.Pined {
|
||||
if pin.Uri == uriStr {
|
||||
if pin.Name != service.Name {
|
||||
return serializer.NewError(serializer.CodeObjectExist, "uri already pinned with different name", nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
user.Settings.Pined = append(user.Settings.Pined, types.PinedFile{
|
||||
Uri: uriStr,
|
||||
Name: service.Name,
|
||||
})
|
||||
if err := userClient.SaveSettings(c, user); err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "failed to save settings", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnpinFile removes uri from sidebar
|
||||
func (service *PinFileService) UnpinFile(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
|
||||
uri, err := fs.NewUriFromString(service.Uri)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
|
||||
}
|
||||
|
||||
uriStr := uri.String()
|
||||
user.Settings.Pined = lo.Filter(user.Settings.Pined, func(pin types.PinedFile, index int) bool {
|
||||
return pin.Uri != uriStr
|
||||
})
|
||||
|
||||
if err := userClient.SaveSettings(c, user); err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "failed to save settings", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
436
service/explorer/response.go
Normal file
436
service/explorer/response.go
Normal file
@@ -0,0 +1,436 @@
|
||||
package explorer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/queue"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/cloudreve/Cloudreve/v4/service/user"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type DirectLinkResponse struct {
|
||||
Link string `json:"link"`
|
||||
FileUrl string `json:"file_url"`
|
||||
}
|
||||
|
||||
func BuildDirectLinkResponse(links []manager.DirectLink) []DirectLinkResponse {
|
||||
if len(links) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var res []DirectLinkResponse
|
||||
for _, link := range links {
|
||||
res = append(res, DirectLinkResponse{
|
||||
Link: link.Url,
|
||||
FileUrl: link.File.Uri(false).String(),
|
||||
})
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
const PathMyRedacted = "redacted"
|
||||
|
||||
type TaskResponse struct {
|
||||
CreatedAt time.Time `json:"created_at,"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Type string `json:"type"`
|
||||
Node *user.Node `json:"node,omitempty"`
|
||||
Summary *queue.Summary `json:"summary,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
ErrorHistory []string `json:"error_history,omitempty"`
|
||||
Duration int64 `json:"duration,omitempty"`
|
||||
ResumeTime int64 `json:"resume_time,omitempty"`
|
||||
RetryCount int `json:"retry_count,omitempty"`
|
||||
}
|
||||
|
||||
type TaskListResponse struct {
|
||||
Tasks []TaskResponse `json:"tasks"`
|
||||
Pagination *inventory.PaginationResults `json:"pagination"`
|
||||
}
|
||||
|
||||
func BuildTaskListResponse(tasks []queue.Task, res *inventory.ListTaskResult, nodeMap map[int]*ent.Node, hasher hashid.Encoder) *TaskListResponse {
|
||||
return &TaskListResponse{
|
||||
Pagination: res.PaginationResults,
|
||||
Tasks: lo.Map(tasks, func(t queue.Task, index int) TaskResponse {
|
||||
var (
|
||||
node *ent.Node
|
||||
s = t.Summarize(hasher)
|
||||
)
|
||||
|
||||
if s.NodeID > 0 {
|
||||
node = nodeMap[s.NodeID]
|
||||
}
|
||||
return *BuildTaskResponse(t, node, hasher)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func BuildTaskResponse(task queue.Task, node *ent.Node, hasher hashid.Encoder) *TaskResponse {
|
||||
model := task.Model()
|
||||
t := &TaskResponse{
|
||||
Status: string(task.Status()),
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
ID: hashid.EncodeTaskID(hasher, task.ID()),
|
||||
Type: task.Type(),
|
||||
Summary: task.Summarize(hasher),
|
||||
Error: auth.RedactSensitiveValues(model.PublicState.Error),
|
||||
ErrorHistory: lo.Map(model.PublicState.ErrorHistory, func(s string, index int) string {
|
||||
return auth.RedactSensitiveValues(s)
|
||||
}),
|
||||
Duration: model.PublicState.ExecutedDuration.Milliseconds(),
|
||||
ResumeTime: model.PublicState.ResumeTime,
|
||||
RetryCount: model.PublicState.RetryCount,
|
||||
}
|
||||
|
||||
if node != nil {
|
||||
t.Node = user.BuildNode(node, hasher)
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
type UploadSessionResponse struct {
|
||||
SessionID string `json:"session_id"`
|
||||
UploadID string `json:"upload_id"`
|
||||
ChunkSize int64 `json:"chunk_size"` // 分块大小,0 为部分快
|
||||
Expires int64 `json:"expires"` // 上传凭证过期时间, Unix 时间戳
|
||||
UploadURLs []string `json:"upload_urls,omitempty"`
|
||||
Credential string `json:"credential,omitempty"`
|
||||
AccessKey string `json:"ak,omitempty"`
|
||||
KeyTime string `json:"keyTime,omitempty"` // COS用有效期
|
||||
CompleteURL string `json:"completeURL,omitempty"`
|
||||
StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"`
|
||||
Uri string `json:"uri"`
|
||||
CallbackSecret string `json:"callback_secret"`
|
||||
MimeType string `json:"mime_type,omitempty"`
|
||||
UploadPolicy string `json:"upload_policy,omitempty"`
|
||||
}
|
||||
|
||||
func BuildUploadSessionResponse(session *fs.UploadCredential, hasher hashid.Encoder) *UploadSessionResponse {
|
||||
return &UploadSessionResponse{
|
||||
SessionID: session.SessionID,
|
||||
ChunkSize: session.ChunkSize,
|
||||
Expires: session.Expires,
|
||||
UploadURLs: session.UploadURLs,
|
||||
Credential: session.Credential,
|
||||
CompleteURL: session.CompleteURL,
|
||||
Uri: session.Uri,
|
||||
UploadID: session.UploadID,
|
||||
StoragePolicy: BuildStoragePolicy(session.StoragePolicy, hasher),
|
||||
CallbackSecret: session.CallbackSecret,
|
||||
MimeType: session.MimeType,
|
||||
UploadPolicy: session.UploadPolicy,
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
FileVersionPostMessage 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
|
||||
SupportsLocks bool
|
||||
|
||||
EnableShare bool
|
||||
}
|
||||
|
||||
type ViewerSessionResponse struct {
|
||||
Session *manager.ViewerSession `json:"session"`
|
||||
WopiSrc string `json:"wopi_src,omitempty"`
|
||||
}
|
||||
|
||||
type ListResponse struct {
|
||||
Files []FileResponse `json:"files"`
|
||||
Parent FileResponse `json:"parent,omitempty"`
|
||||
Pagination *inventory.PaginationResults `json:"pagination"`
|
||||
Props *fs.NavigatorProps `json:"props"`
|
||||
// ContextHint is used to speed up following operations under this listed directory.
|
||||
// It persists some intermedia state so that the following request don't need to query database again.
|
||||
// All the operations under this directory that supports context hint should carry this value in header
|
||||
// as X-Cr-Context-Hint.
|
||||
ContextHint *uuid.UUID `json:"context_hint"`
|
||||
RecursionLimitReached bool `json:"recursion_limit_reached,omitempty"`
|
||||
MixedType bool `json:"mixed_type"`
|
||||
SingleFileView bool `json:"single_file_view,omitempty"`
|
||||
StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"`
|
||||
}
|
||||
|
||||
type FileResponse struct {
|
||||
Type int `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Size int64 `json:"size"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Shared bool `json:"shared,omitempty"`
|
||||
Capability *boolset.BooleanSet `json:"capability,omitempty"`
|
||||
Owned bool `json:"owned,omitempty"`
|
||||
PrimaryEntity string `json:"primary_entity,omitempty"`
|
||||
|
||||
FolderSummary *fs.FolderSummary `json:"folder_summary,omitempty"`
|
||||
ExtendedInfo *ExtendedInfo `json:"extended_info,omitempty"`
|
||||
}
|
||||
|
||||
type ExtendedInfo struct {
|
||||
StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"`
|
||||
StorageUsed int64 `json:"storage_used"`
|
||||
Shares []Share `json:"shares,omitempty"`
|
||||
Entities []Entity `json:"entities,omitempty"`
|
||||
}
|
||||
|
||||
type StoragePolicy struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
AllowedSuffix []string `json:"allowed_suffix,omitempty"`
|
||||
Type types.PolicyType `json:"type"`
|
||||
MaxSize int64 `json:"max_size"`
|
||||
Relay bool `json:"relay,omitempty"`
|
||||
}
|
||||
|
||||
type Entity struct {
|
||||
ID string `json:"id"`
|
||||
Size int64 `json:"size"`
|
||||
Type types.EntityType `json:"type"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"`
|
||||
CreatedBy *user.User `json:"created_by,omitempty"`
|
||||
}
|
||||
|
||||
type Share struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
RemainDownloads *int `json:"remain_downloads,omitempty"`
|
||||
Visited int `json:"visited"`
|
||||
Downloaded int `json:"downloaded,omitempty"`
|
||||
Expires *time.Time `json:"expires,omitempty"`
|
||||
Unlocked bool `json:"unlocked"`
|
||||
SourceType *types.FileType `json:"source_type,omitempty"`
|
||||
Owner user.User `json:"owner"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
Expired bool `json:"expired"`
|
||||
Url string `json:"url"`
|
||||
|
||||
// Only viewable by owner
|
||||
IsPrivate bool `json:"is_private,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
|
||||
// Only viewable if explicitly unlocked by owner
|
||||
SourceUri string `json:"source_uri,omitempty"`
|
||||
}
|
||||
|
||||
func BuildShare(s *ent.Share, base *url.URL, hasher hashid.Encoder, requester *ent.User, owner *ent.User,
|
||||
name string, t types.FileType, unlocked bool) *Share {
|
||||
redactLevel := user.RedactLevelAnonymous
|
||||
if !inventory.IsAnonymousUser(requester) {
|
||||
redactLevel = user.RedactLevelUser
|
||||
}
|
||||
res := Share{
|
||||
Name: name,
|
||||
ID: hashid.EncodeShareID(hasher, s.ID),
|
||||
Unlocked: unlocked,
|
||||
Owner: user.BuildUserRedacted(owner, redactLevel, hasher),
|
||||
Expired: inventory.IsShareExpired(s) != nil,
|
||||
Url: BuildShareLink(s, hasher, base),
|
||||
CreatedAt: s.CreatedAt,
|
||||
Visited: s.Views,
|
||||
SourceType: util.ToPtr(t),
|
||||
}
|
||||
|
||||
if unlocked {
|
||||
res.RemainDownloads = s.RemainDownloads
|
||||
res.Downloaded = s.Downloads
|
||||
res.Expires = s.Expires
|
||||
res.Password = s.Password
|
||||
}
|
||||
|
||||
if requester.ID == owner.ID {
|
||||
res.IsPrivate = s.Password != ""
|
||||
}
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
func BuildListResponse(ctx context.Context, u *ent.User, parent fs.File, res *fs.ListFileResult, hasher hashid.Encoder) *ListResponse {
|
||||
r := &ListResponse{
|
||||
Files: lo.Map(res.Files, func(f fs.File, index int) FileResponse {
|
||||
return *BuildFileResponse(ctx, u, f, hasher, res.Props.Capability)
|
||||
}),
|
||||
Pagination: res.Pagination,
|
||||
Props: res.Props,
|
||||
ContextHint: res.ContextHint,
|
||||
RecursionLimitReached: res.RecursionLimitReached,
|
||||
MixedType: res.MixedType,
|
||||
SingleFileView: res.SingleFileView,
|
||||
StoragePolicy: BuildStoragePolicy(res.StoragePolicy, hasher),
|
||||
}
|
||||
|
||||
if !res.Parent.IsNil() {
|
||||
r.Parent = *BuildFileResponse(ctx, u, res.Parent, hasher, res.Props.Capability)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func BuildFileResponse(ctx context.Context, u *ent.User, f fs.File, hasher hashid.Encoder, cap *boolset.BooleanSet) *FileResponse {
|
||||
var owner *ent.User
|
||||
if f != nil {
|
||||
owner = f.Owner()
|
||||
}
|
||||
|
||||
if cap == nil {
|
||||
cap = f.Capabilities()
|
||||
}
|
||||
|
||||
res := &FileResponse{
|
||||
Type: int(f.Type()),
|
||||
ID: hashid.EncodeFileID(hasher, f.ID()),
|
||||
Name: f.DisplayName(),
|
||||
CreatedAt: f.CreatedAt(),
|
||||
UpdatedAt: f.UpdatedAt(),
|
||||
Size: f.Size(),
|
||||
Metadata: f.Metadata(),
|
||||
Path: f.Uri(false).String(),
|
||||
Shared: f.Shared(),
|
||||
Capability: cap,
|
||||
Owned: owner == nil || owner.ID == u.ID,
|
||||
FolderSummary: f.FolderSummary(),
|
||||
ExtendedInfo: BuildExtendedInfo(ctx, u, f, hasher),
|
||||
PrimaryEntity: hashid.EncodeEntityID(hasher, f.PrimaryEntityID()),
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func BuildExtendedInfo(ctx context.Context, u *ent.User, f fs.File, hasher hashid.Encoder) *ExtendedInfo {
|
||||
extendedInfo := f.ExtendedInfo()
|
||||
if extendedInfo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ext := &ExtendedInfo{
|
||||
StoragePolicy: BuildStoragePolicy(extendedInfo.StoragePolicy, hasher),
|
||||
StorageUsed: extendedInfo.StorageUsed,
|
||||
Entities: lo.Map(f.Entities(), func(e fs.Entity, index int) Entity {
|
||||
return BuildEntity(extendedInfo, e, hasher)
|
||||
}),
|
||||
}
|
||||
|
||||
dep := dependency.FromContext(ctx)
|
||||
base := dep.SettingProvider().SiteURL(ctx)
|
||||
if u.ID == f.OwnerID() {
|
||||
// Only owner can see the shares settings.
|
||||
ext.Shares = lo.Map(extendedInfo.Shares, func(s *ent.Share, index int) Share {
|
||||
return *BuildShare(s, base, hasher, u, u, f.DisplayName(), f.Type(), true)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return ext
|
||||
}
|
||||
|
||||
func BuildEntity(extendedInfo *fs.FileExtendedInfo, e fs.Entity, hasher hashid.Encoder) Entity {
|
||||
var u *user.User
|
||||
createdBy := e.CreatedBy()
|
||||
if createdBy != nil {
|
||||
userRedacted := user.BuildUserRedacted(e.CreatedBy(), user.RedactLevelAnonymous, hasher)
|
||||
u = &userRedacted
|
||||
}
|
||||
return Entity{
|
||||
ID: hashid.EncodeEntityID(hasher, e.ID()),
|
||||
Type: e.Type(),
|
||||
CreatedAt: e.CreatedAt(),
|
||||
StoragePolicy: BuildStoragePolicy(extendedInfo.EntityStoragePolicies[e.PolicyID()], hasher),
|
||||
Size: e.Size(),
|
||||
CreatedBy: u,
|
||||
}
|
||||
}
|
||||
|
||||
func BuildShareLink(s *ent.Share, hasher hashid.Encoder, base *url.URL) string {
|
||||
shareId := hashid.EncodeShareID(hasher, s.ID)
|
||||
return routes.MasterShareUrl(base, shareId, s.Password).String()
|
||||
}
|
||||
|
||||
func BuildStoragePolicy(sp *ent.StoragePolicy, hasher hashid.Encoder) *StoragePolicy {
|
||||
if sp == nil {
|
||||
return nil
|
||||
}
|
||||
return &StoragePolicy{
|
||||
ID: hashid.EncodePolicyID(hasher, sp.ID),
|
||||
Name: sp.Name,
|
||||
Type: types.PolicyType(sp.Type),
|
||||
MaxSize: sp.MaxSize,
|
||||
AllowedSuffix: sp.Settings.FileType,
|
||||
Relay: sp.Settings.Relay,
|
||||
}
|
||||
}
|
||||
|
||||
func WriteEventSourceHeader(c *gin.Context) {
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("X-Accel-Buffering", "no")
|
||||
}
|
||||
|
||||
// WriteEventSource writes a Server-Sent Event to the client.
|
||||
func WriteEventSource(c *gin.Context, event string, data any) {
|
||||
c.Writer.Write([]byte(fmt.Sprintf("event: %s\n", event)))
|
||||
c.Writer.Write([]byte("data:"))
|
||||
json.NewEncoder(c.Writer).Encode(data)
|
||||
c.Writer.Write([]byte("\n"))
|
||||
c.Writer.Flush()
|
||||
}
|
||||
|
||||
var ErrSSETakeOver = errors.New("SSE take over")
|
||||
@@ -1,88 +0,0 @@
|
||||
package explorer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ItemSearchService 文件搜索服务
|
||||
type ItemSearchService struct {
|
||||
Type string `uri:"type" binding:"required"`
|
||||
Keywords string `uri:"keywords" binding:"required"`
|
||||
Path string `form:"path"`
|
||||
}
|
||||
|
||||
// Search 执行搜索
|
||||
func (service *ItemSearchService) Search(c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromContext(c)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
if service.Path != "" {
|
||||
ok, parent := fs.IsPathExist(service.Path)
|
||||
if !ok {
|
||||
return serializer.Err(serializer.CodeParentNotExist, "", nil)
|
||||
}
|
||||
|
||||
fs.Root = parent
|
||||
}
|
||||
|
||||
switch service.Type {
|
||||
case "keywords":
|
||||
return service.SearchKeywords(c, fs, "%"+service.Keywords+"%")
|
||||
case "image":
|
||||
return service.SearchKeywords(c, fs, "%.bmp", "%.iff", "%.png", "%.gif", "%.jpg", "%.jpeg", "%.psd", "%.svg", "%.webp")
|
||||
case "video":
|
||||
return service.SearchKeywords(c, fs, "%.mp4", "%.flv", "%.avi", "%.wmv", "%.mkv", "%.rm", "%.rmvb", "%.mov", "%.ogv")
|
||||
case "audio":
|
||||
return service.SearchKeywords(c, fs, "%.mp3", "%.flac", "%.ape", "%.wav", "%.acc", "%.ogg", "%.midi", "%.mid")
|
||||
case "doc":
|
||||
return service.SearchKeywords(c, fs, "%.txt", "%.md", "%.pdf", "%.doc", "%.docx", "%.ppt", "%.pptx", "%.xls", "%.xlsx", "%.pub")
|
||||
case "tag":
|
||||
if tid, err := hashid.DecodeHashID(service.Keywords, hashid.TagID); err == nil {
|
||||
if tag, err := model.GetTagsByID(tid, fs.User.ID); err == nil {
|
||||
if tag.Type == model.FileTagType {
|
||||
exp := strings.Split(tag.Expression, "\n")
|
||||
expInput := make([]interface{}, len(exp))
|
||||
for i := 0; i < len(exp); i++ {
|
||||
expInput[i] = exp[i]
|
||||
}
|
||||
return service.SearchKeywords(c, fs, expInput...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return serializer.Err(serializer.CodeNotFound, "", nil)
|
||||
default:
|
||||
return serializer.ParamErr("Unknown search type", nil)
|
||||
}
|
||||
}
|
||||
|
||||
// SearchKeywords 根据关键字搜索文件
|
||||
func (service *ItemSearchService) SearchKeywords(c *gin.Context, fs *filesystem.FileSystem, keywords ...interface{}) serializer.Response {
|
||||
// 上下文
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// 获取子项目
|
||||
objects, err := fs.Search(ctx, keywords...)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Code: 0,
|
||||
Data: map[string]interface{}{
|
||||
"parent": 0,
|
||||
"objects": objects,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,20 @@
|
||||
package explorer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/task"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/task/slavetask"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver/local"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/samber/lo"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SlaveDownloadService 从机文件下載服务
|
||||
@@ -46,148 +41,211 @@ type SlaveListService struct {
|
||||
Recursive bool `json:"recursive"`
|
||||
}
|
||||
|
||||
// ServeFile 通过签名的URL下载从机文件
|
||||
func (service *SlaveDownloadService) ServeFile(ctx context.Context, c *gin.Context, isDownload bool) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewAnonymousFileSystem()
|
||||
// SlaveServe serves file content
|
||||
func (s *EntityDownloadService) SlaveServe(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
m := manager.NewFileManager(dep, nil)
|
||||
defer m.Recycle()
|
||||
|
||||
src, err := base64.URLEncoding.DecodeString(s.Src)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
return fmt.Errorf("failed to decode src: %w", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 解码文件路径
|
||||
fileSource, err := base64.RawURLEncoding.DecodeString(service.PathEncoded)
|
||||
entity, err := local.NewLocalFileEntity(types.EntityTypeVersion, string(src))
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeFileNotFound, "", err)
|
||||
return fs.ErrPathNotExist.WithError(err)
|
||||
}
|
||||
|
||||
// 根据URL里的信息创建一个文件对象和用户对象
|
||||
file := model.File{
|
||||
Name: service.Name,
|
||||
SourceName: string(fileSource),
|
||||
Policy: model.Policy{
|
||||
Model: gorm.Model{ID: 1},
|
||||
Type: "local",
|
||||
},
|
||||
}
|
||||
fs.User = &model.User{
|
||||
Group: model.Group{SpeedLimit: service.Speed},
|
||||
}
|
||||
fs.FileTarget = []model.File{file}
|
||||
|
||||
// 开始处理下载
|
||||
ctx = context.WithValue(ctx, fsctx.GinCtx, c)
|
||||
rs, err := fs.GetDownloadContent(ctx, 0)
|
||||
entitySource, err := m.GetEntitySource(c, 0, fs.WithEntity(entity))
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
|
||||
}
|
||||
defer rs.Close()
|
||||
|
||||
// 设置下载文件名
|
||||
if isDownload {
|
||||
c.Header("Content-Disposition", "attachment; filename=\""+url.PathEscape(fs.FileTarget[0].Name)+"\"")
|
||||
return fmt.Errorf("failed to get entity source: %w", err)
|
||||
}
|
||||
|
||||
// 发送文件
|
||||
http.ServeContent(c.Writer, c.Request, fs.FileTarget[0].Name, time.Now(), rs)
|
||||
defer entitySource.Close()
|
||||
|
||||
return serializer.Response{}
|
||||
// Set cache header for public resource
|
||||
settings := dep.SettingProvider()
|
||||
maxAge := settings.PublicResourceMaxAge(c)
|
||||
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
|
||||
|
||||
isDownload := c.Query(routes.IsDownloadQuery) != ""
|
||||
entitySource.Serve(c.Writer, c.Request,
|
||||
entitysource.WithSpeedLimit(s.SpeedLimit),
|
||||
entitysource.WithDownload(isDownload),
|
||||
entitysource.WithDisplayName(s.Name),
|
||||
entitysource.WithContext(c),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 通过签名的URL删除从机文件
|
||||
func (service *SlaveFilesService) Delete(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewAnonymousFileSystem()
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
type (
|
||||
SlaveCreateUploadSessionParamCtx struct{}
|
||||
// SlaveCreateUploadSessionService 从机上传会话服务
|
||||
SlaveCreateUploadSessionService struct {
|
||||
Session fs.UploadSession `json:"session" binding:"required"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 删除文件
|
||||
failed, err := fs.Handler.Delete(ctx, service.Files)
|
||||
if err != nil {
|
||||
// 将Data字段写为字符串方便主控端解析
|
||||
data, _ := json.Marshal(serializer.RemoteDeleteRequest{Files: failed})
|
||||
|
||||
return serializer.Response{
|
||||
Code: serializer.CodeNotFullySuccess,
|
||||
Data: string(data),
|
||||
Msg: fmt.Sprintf("Failed to delete %d files(s)", len(failed)),
|
||||
Error: err.Error(),
|
||||
}
|
||||
}
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Thumb 通过签名URL获取从机文件缩略图
|
||||
func (service *SlaveFileService) Thumb(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewAnonymousFileSystem()
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 解码文件路径
|
||||
fileSource, err := base64.RawURLEncoding.DecodeString(service.PathEncoded)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeFileNotFound, "", err)
|
||||
}
|
||||
fs.FileTarget = []model.File{{SourceName: string(fileSource), Name: fmt.Sprintf("%s.%s", fileSource, service.Ext), PicInfo: "1,1"}}
|
||||
|
||||
// 获取缩略图
|
||||
resp, err := fs.GetThumb(ctx, 0)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, "Failed to get thumb", err)
|
||||
}
|
||||
|
||||
defer resp.Content.Close()
|
||||
http.ServeContent(c.Writer, c.Request, "thumb.png", time.Now(), resp.Content)
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// CreateTransferTask 创建从机文件转存任务
|
||||
func CreateTransferTask(c *gin.Context, req *serializer.SlaveTransferReq) serializer.Response {
|
||||
if id, ok := c.Get("MasterSiteID"); ok {
|
||||
job := &slavetask.TransferTask{
|
||||
Req: req,
|
||||
MasterID: id.(string),
|
||||
}
|
||||
|
||||
if err := cluster.DefaultController.SubmitTask(job.MasterID, job, req.Hash(job.MasterID), func(job interface{}) {
|
||||
task.TaskPoll.Submit(job.(task.Job))
|
||||
}); err != nil {
|
||||
return serializer.Err(serializer.CodeCreateTaskError, "", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
return serializer.ParamErr("未知的主机节点ID", nil)
|
||||
}
|
||||
|
||||
// SlaveListService 从机上传会话服务
|
||||
type SlaveCreateUploadSessionService struct {
|
||||
Session serializer.UploadSession `json:"session" binding:"required"`
|
||||
TTL int64 `json:"ttl"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
}
|
||||
)
|
||||
|
||||
// Create 从机创建上传会话
|
||||
func (service *SlaveCreateUploadSessionService) Create(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
if !service.Overwrite && util.Exists(service.Session.SavePath) {
|
||||
return serializer.Err(serializer.CodeConflict, "placeholder file already exist", nil)
|
||||
func (service *SlaveCreateUploadSessionService) Create(c *gin.Context) error {
|
||||
mode := fs.ModeNone
|
||||
if service.Overwrite {
|
||||
mode = fs.ModeOverwrite
|
||||
}
|
||||
|
||||
err := cache.Set(
|
||||
filesystem.UploadSessionCachePrefix+service.Session.Key,
|
||||
service.Session,
|
||||
int(service.TTL),
|
||||
)
|
||||
req := &fs.UploadRequest{
|
||||
Mode: mode,
|
||||
Props: service.Session.Props.Copy(),
|
||||
}
|
||||
|
||||
dep := dependency.FromContext(c)
|
||||
m := manager.NewFileManager(dep, nil)
|
||||
_, err := m.CreateUploadSession(c, req, fs.WithUploadSession(&service.Session))
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCacheOperation, "Failed to create upload session in slave node", err)
|
||||
return serializer.NewError(serializer.CodeCacheOperation, "Failed to create upload session in slave node", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
SlaveMetaParamCtx struct{}
|
||||
SlaveMetaService struct {
|
||||
Src string `uri:"src" binding:"required"`
|
||||
Ext string `uri:"ext" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
// MediaMeta retrieves media metadata
|
||||
func (s *SlaveMetaService) MediaMeta(c *gin.Context) ([]driver.MediaMeta, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
m := manager.NewFileManager(dep, nil)
|
||||
defer m.Recycle()
|
||||
|
||||
src, err := base64.URLEncoding.DecodeString(s.Src)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode src: %w", err)
|
||||
}
|
||||
|
||||
entity, err := local.NewLocalFileEntity(types.EntityTypeVersion, string(src))
|
||||
if err != nil {
|
||||
return nil, fs.ErrPathNotExist.WithError(err)
|
||||
}
|
||||
|
||||
entitySource, err := m.GetEntitySource(c, 0, fs.WithEntity(entity))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get entity source: %w", err)
|
||||
}
|
||||
defer entitySource.Close()
|
||||
|
||||
extractor := dep.MediaMetaExtractor(c)
|
||||
res, err := extractor.Extract(c, s.Ext, entitySource)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract media meta: %w", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type (
|
||||
SlaveThumbParamCtx struct{}
|
||||
SlaveThumbService struct {
|
||||
Src string `uri:"src" binding:"required"`
|
||||
Ext string `uri:"ext" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
func (s *SlaveThumbService) Thumb(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
m := manager.NewFileManager(dep, nil)
|
||||
defer m.Recycle()
|
||||
|
||||
src, err := base64.URLEncoding.DecodeString(s.Src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode src: %w", err)
|
||||
}
|
||||
|
||||
settings := dep.SettingProvider()
|
||||
var entity fs.Entity
|
||||
entity, err = local.NewLocalFileEntity(types.EntityTypeThumbnail, string(src)+settings.ThumbSlaveSidecarSuffix(c))
|
||||
if err != nil {
|
||||
srcEntity, err := local.NewLocalFileEntity(types.EntityTypeVersion, string(src))
|
||||
if err != nil {
|
||||
return fs.ErrPathNotExist.WithError(err)
|
||||
}
|
||||
|
||||
entity, err = m.SubmitAndAwaitThumbnailTask(c, nil, s.Ext, srcEntity)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to submit and await thumbnail task: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
entitySource, err := m.GetEntitySource(c, 0, fs.WithEntity(entity))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get thumb entity source: %w", err)
|
||||
}
|
||||
|
||||
defer entitySource.Close()
|
||||
|
||||
// Set cache header for public resource
|
||||
maxAge := settings.PublicResourceMaxAge(c)
|
||||
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
|
||||
|
||||
entitySource.Serve(c.Writer, c.Request,
|
||||
entitysource.WithContext(c),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
SlaveDeleteUploadSessionParamCtx struct{}
|
||||
SlaveDeleteUploadSessionService struct {
|
||||
ID string `uri:"sessionId" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
// Delete deletes an upload session from slave node
|
||||
func (service *SlaveDeleteUploadSessionService) Delete(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
m := manager.NewFileManager(dep, nil)
|
||||
defer m.Recycle()
|
||||
|
||||
err := m.CancelUploadSession(c, nil, service.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("slave failed to delete upload session: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
SlaveDeleteFileParamCtx struct{}
|
||||
SlaveDeleteFileService struct {
|
||||
Files []string `json:"files" binding:"required,gt=0"`
|
||||
}
|
||||
)
|
||||
|
||||
func (service *SlaveDeleteFileService) Delete(c *gin.Context) ([]string, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
m := manager.NewFileManager(dep, nil)
|
||||
defer m.Recycle()
|
||||
d := m.LocalDriver(nil)
|
||||
|
||||
// Try to delete thumbnail sidecar
|
||||
sidecarSuffix := dep.SettingProvider().ThumbSlaveSidecarSuffix(c)
|
||||
failed, err := d.Delete(c, lo.Map(service.Files, func(item string, index int) string {
|
||||
return item + sidecarSuffix
|
||||
})...)
|
||||
if err != nil {
|
||||
dep.Logger().Warning("Failed to delete thumbnail sidecar [%s]: %s", strings.Join(failed, ", "), err)
|
||||
}
|
||||
|
||||
failed, err = d.Delete(c, service.Files...)
|
||||
if err != nil {
|
||||
return failed, fmt.Errorf("slave failed to delete file: %w", err)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
package explorer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// FilterTagCreateService 文件分类标签创建服务
|
||||
type FilterTagCreateService struct {
|
||||
Expression string `json:"expression" binding:"required,min=1,max=65535"`
|
||||
Icon string `json:"icon" binding:"required,min=1,max=255"`
|
||||
Name string `json:"name" binding:"required,min=1,max=255"`
|
||||
Color string `json:"color" binding:"hexcolor|rgb|rgba|hsl"`
|
||||
}
|
||||
|
||||
// LinkTagCreateService 目录快捷方式标签创建服务
|
||||
type LinkTagCreateService struct {
|
||||
Path string `json:"path" binding:"required,min=1,max=65535"`
|
||||
Name string `json:"name" binding:"required,min=1,max=255"`
|
||||
}
|
||||
|
||||
// TagService 标签服务
|
||||
type TagService struct {
|
||||
}
|
||||
|
||||
// Delete 删除标签
|
||||
func (service *TagService) Delete(c *gin.Context, user *model.User) serializer.Response {
|
||||
id, _ := c.Get("object_id")
|
||||
if err := model.DeleteTagByID(id.(uint), user.ID); err != nil {
|
||||
return serializer.DBErr("Failed to delete a tag", err)
|
||||
}
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Create 创建标签
|
||||
func (service *LinkTagCreateService) Create(c *gin.Context, user *model.User) serializer.Response {
|
||||
// 创建标签
|
||||
tag := model.Tag{
|
||||
Name: service.Name,
|
||||
Icon: "FolderHeartOutline",
|
||||
Type: model.DirectoryLinkType,
|
||||
Expression: service.Path,
|
||||
UserID: user.ID,
|
||||
}
|
||||
id, err := tag.Create()
|
||||
if err != nil {
|
||||
return serializer.DBErr("Failed to create a tag", err)
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Data: hashid.HashID(id, hashid.TagID),
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建标签
|
||||
func (service *FilterTagCreateService) Create(c *gin.Context, user *model.User) serializer.Response {
|
||||
// 分割表达式,将通配符转换为SQL内的%
|
||||
expressions := strings.Split(service.Expression, "\n")
|
||||
for i := 0; i < len(expressions); i++ {
|
||||
expressions[i] = strings.ReplaceAll(expressions[i], "*", "%")
|
||||
if expressions[i] == "" {
|
||||
return serializer.ParamErr(fmt.Sprintf("The %d line contains an empty match expression", i+1), nil)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建标签
|
||||
tag := model.Tag{
|
||||
Name: service.Name,
|
||||
Icon: service.Icon,
|
||||
Color: service.Color,
|
||||
Type: model.FileTagType,
|
||||
Expression: strings.Join(expressions, "\n"),
|
||||
UserID: user.ID,
|
||||
}
|
||||
id, err := tag.Create()
|
||||
if err != nil {
|
||||
return serializer.DBErr("Failed to create a tag", err)
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Data: hashid.HashID(id, hashid.TagID),
|
||||
}
|
||||
}
|
||||
@@ -3,167 +3,158 @@ package explorer
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/local"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CreateUploadSessionService 获取上传凭证服务
|
||||
type CreateUploadSessionService struct {
|
||||
Path string `json:"path" binding:"required"`
|
||||
Size uint64 `json:"size" binding:"min=0"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
PolicyID string `json:"policy_id" binding:"required"`
|
||||
LastModified int64 `json:"last_modified"`
|
||||
MimeType string `json:"mime_type"`
|
||||
}
|
||||
type (
|
||||
CreateUploadSessionParameterCtx struct{}
|
||||
CreateUploadSessionService struct {
|
||||
Uri string `json:"uri" binding:"required"`
|
||||
Size int64 `json:"size" binding:"min=0"`
|
||||
LastModified int64 `json:"last_modified"`
|
||||
MimeType string `json:"mime_type"`
|
||||
PolicyID string `json:"policy_id"`
|
||||
Metadata map[string]string `json:"metadata" binding:"max=256"`
|
||||
EntityType string `json:"entity_type" binding:"eq=|eq=live_photo|eq=version"`
|
||||
}
|
||||
)
|
||||
|
||||
// Create 创建新的上传会话
|
||||
func (service *CreateUploadSessionService) Create(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromContext(c)
|
||||
func (service *CreateUploadSessionService) Create(c context.Context) (*UploadSessionResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
m := manager.NewFileManager(dep, user)
|
||||
defer m.Recycle()
|
||||
|
||||
uri, err := fs.NewUriFromString(service.Uri)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
|
||||
}
|
||||
|
||||
// 取得存储策略的ID
|
||||
rawID, err := hashid.DecodeHashID(service.PolicyID, hashid.PolicyID)
|
||||
var entityType *types.EntityType
|
||||
switch service.EntityType {
|
||||
case "live_photo":
|
||||
livePhoto := types.EntityTypeLivePhoto
|
||||
entityType = &livePhoto
|
||||
case "version":
|
||||
version := types.EntityTypeVersion
|
||||
entityType = &version
|
||||
}
|
||||
|
||||
hasher := dep.HashIDEncoder()
|
||||
policyId, err := hasher.Decode(service.PolicyID, hashid.PolicyID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodePolicyNotExist, "", err)
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "unknown policy id", err)
|
||||
}
|
||||
|
||||
if fs.Policy.ID != rawID {
|
||||
return serializer.Err(serializer.CodePolicyNotAllowed, "存储策略发生变化,请刷新文件列表并重新添加此任务", nil)
|
||||
uploadRequest := &fs.UploadRequest{
|
||||
Props: &fs.UploadProps{
|
||||
Uri: uri,
|
||||
Size: service.Size,
|
||||
|
||||
MimeType: service.MimeType,
|
||||
Metadata: service.Metadata,
|
||||
EntityType: entityType,
|
||||
PreferredStoragePolicy: policyId,
|
||||
},
|
||||
}
|
||||
|
||||
file := &fsctx.FileStream{
|
||||
Size: service.Size,
|
||||
Name: service.Name,
|
||||
VirtualPath: service.Path,
|
||||
File: ioutil.NopCloser(strings.NewReader("")),
|
||||
MimeType: service.MimeType,
|
||||
}
|
||||
if service.LastModified > 0 {
|
||||
lastModified := time.UnixMilli(service.LastModified)
|
||||
file.LastModified = &lastModified
|
||||
uploadRequest.Props.LastModified = &lastModified
|
||||
}
|
||||
credential, err := fs.CreateUploadSession(ctx, file)
|
||||
|
||||
credential, err := m.CreateUploadSession(c, uploadRequest)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Code: 0,
|
||||
Data: credential,
|
||||
}
|
||||
return BuildUploadSessionResponse(credential, hasher), nil
|
||||
}
|
||||
|
||||
// UploadService 本机及从机策略上传服务
|
||||
type UploadService struct {
|
||||
ID string `uri:"sessionId" binding:"required"`
|
||||
Index int `uri:"index" form:"index" binding:"min=0"`
|
||||
}
|
||||
type (
|
||||
UploadParameterCtx struct{}
|
||||
// UploadService 本机及从机策略上传服务
|
||||
UploadService struct {
|
||||
ID string `uri:"sessionId" binding:"required"`
|
||||
Index int `uri:"index" form:"index" binding:"min=0"`
|
||||
}
|
||||
)
|
||||
|
||||
// LocalUpload 处理本机文件分片上传
|
||||
func (service *UploadService) LocalUpload(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
uploadSessionRaw, ok := cache.Get(filesystem.UploadSessionCachePrefix + service.ID)
|
||||
func (service *UploadService) LocalUpload(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
kv := dep.KV()
|
||||
|
||||
uploadSessionRaw, ok := kv.Get(manager.UploadSessionCachePrefix + service.ID)
|
||||
if !ok {
|
||||
return serializer.Err(serializer.CodeUploadSessionExpired, "", nil)
|
||||
return serializer.NewError(serializer.CodeUploadSessionExpired, "", nil)
|
||||
}
|
||||
|
||||
uploadSession := uploadSessionRaw.(serializer.UploadSession)
|
||||
uploadSession := uploadSessionRaw.(fs.UploadSession)
|
||||
|
||||
fs, err := filesystem.NewFileSystemFromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
m := manager.NewFileManager(dep, user)
|
||||
defer m.Recycle()
|
||||
|
||||
if uploadSession.UID != user.ID {
|
||||
return serializer.NewError(serializer.CodeUploadSessionExpired, "", nil)
|
||||
}
|
||||
|
||||
// Confirm upload session and chunk index
|
||||
placeholder, err := m.ConfirmUploadSession(c, &uploadSession, service.Index)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
if uploadSession.UID != fs.User.ID {
|
||||
return serializer.Err(serializer.CodeUploadSessionExpired, "", nil)
|
||||
}
|
||||
|
||||
// 查找上传会话创建的占位文件
|
||||
file, err := model.GetFilesByUploadSession(service.ID, fs.User.ID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeUploadSessionExpired, "", err)
|
||||
}
|
||||
|
||||
// 重设 fs 存储策略
|
||||
if !uploadSession.Policy.IsTransitUpload(uploadSession.Size) {
|
||||
return serializer.Err(serializer.CodePolicyNotAllowed, "", err)
|
||||
}
|
||||
|
||||
fs.Policy = &uploadSession.Policy
|
||||
if err := fs.DispatchHandler(); err != nil {
|
||||
return serializer.Err(serializer.CodePolicyNotExist, "", err)
|
||||
}
|
||||
|
||||
expectedSizeStart := file.Size
|
||||
actualSizeStart := uint64(service.Index) * uploadSession.Policy.OptionsSerialized.ChunkSize
|
||||
if uploadSession.Policy.OptionsSerialized.ChunkSize == 0 && service.Index > 0 {
|
||||
return serializer.Err(serializer.CodeInvalidChunkIndex, "Chunk index cannot be greater than 0", nil)
|
||||
}
|
||||
|
||||
if expectedSizeStart < actualSizeStart {
|
||||
return serializer.Err(serializer.CodeInvalidChunkIndex, "Chunk must be uploaded in order", nil)
|
||||
}
|
||||
|
||||
if expectedSizeStart > actualSizeStart {
|
||||
util.Log().Info("Trying to overwrite chunk[%d] Start=%d", service.Index, actualSizeStart)
|
||||
}
|
||||
|
||||
return processChunkUpload(ctx, c, fs, &uploadSession, service.Index, file, fsctx.Append)
|
||||
return processChunkUpload(c, m, &uploadSession, service.Index, placeholder, fs.ModeOverwrite)
|
||||
}
|
||||
|
||||
// SlaveUpload 处理从机文件分片上传
|
||||
func (service *UploadService) SlaveUpload(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
uploadSessionRaw, ok := cache.Get(filesystem.UploadSessionCachePrefix + service.ID)
|
||||
func (service *UploadService) SlaveUpload(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
kv := dep.KV()
|
||||
|
||||
uploadSessionRaw, ok := kv.Get(manager.UploadSessionCachePrefix + service.ID)
|
||||
if !ok {
|
||||
return serializer.Err(serializer.CodeUploadSessionExpired, "", nil)
|
||||
return serializer.NewError(serializer.CodeUploadSessionExpired, "", nil)
|
||||
}
|
||||
|
||||
uploadSession := uploadSessionRaw.(serializer.UploadSession)
|
||||
uploadSession := uploadSessionRaw.(fs.UploadSession)
|
||||
|
||||
fs, err := filesystem.NewAnonymousFileSystem()
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
|
||||
fs.Handler = local.Driver{}
|
||||
|
||||
// 解析需要的参数
|
||||
// Parse chunk index from query
|
||||
service.Index, _ = strconv.Atoi(c.Query("chunk"))
|
||||
mode := fsctx.Append
|
||||
if c.GetHeader(auth.CrHeaderPrefix+"Overwrite") == "true" {
|
||||
mode |= fsctx.Overwrite
|
||||
}
|
||||
|
||||
return processChunkUpload(ctx, c, fs, &uploadSession, service.Index, nil, mode)
|
||||
m := manager.NewFileManager(dep, nil)
|
||||
defer m.Recycle()
|
||||
|
||||
return processChunkUpload(c, m, &uploadSession, service.Index, nil, fs.ModeOverwrite)
|
||||
}
|
||||
|
||||
func processChunkUpload(ctx context.Context, c *gin.Context, fs *filesystem.FileSystem, session *serializer.UploadSession, index int, file *model.File, mode fsctx.WriteMode) serializer.Response {
|
||||
func processChunkUpload(c *gin.Context, m manager.FileManager, session *fs.UploadSession, index int, file fs.File, mode fs.WriteMode) error {
|
||||
// 取得并校验文件大小是否符合分片要求
|
||||
chunkSize := session.Policy.OptionsSerialized.ChunkSize
|
||||
isLastChunk := session.Policy.OptionsSerialized.ChunkSize == 0 || uint64(index+1)*chunkSize >= session.Size
|
||||
chunkSize := session.ChunkSize
|
||||
isLastChunk := session.ChunkSize == 0 || int64(index+1)*chunkSize >= session.Props.Size
|
||||
expectedLength := chunkSize
|
||||
if isLastChunk {
|
||||
expectedLength = session.Size - uint64(index)*chunkSize
|
||||
expectedLength = session.Props.Size - int64(index)*chunkSize
|
||||
}
|
||||
|
||||
fileSize, err := strconv.ParseUint(c.Request.Header.Get("Content-Length"), 10, 64)
|
||||
rc, fileSize, err := request.SniffContentLength(c.Request)
|
||||
if err != nil || (expectedLength != fileSize) {
|
||||
return serializer.Err(
|
||||
return serializer.NewError(
|
||||
serializer.CodeInvalidContentLength,
|
||||
fmt.Sprintf("Invalid Content-Length (expected: %d)", expectedLength),
|
||||
err,
|
||||
@@ -172,121 +163,60 @@ func processChunkUpload(ctx context.Context, c *gin.Context, fs *filesystem.File
|
||||
|
||||
// 非首个分片时需要允许覆盖
|
||||
if index > 0 {
|
||||
mode |= fsctx.Overwrite
|
||||
mode |= fs.ModeOverwrite
|
||||
}
|
||||
|
||||
fileData := fsctx.FileStream{
|
||||
MimeType: c.Request.Header.Get("Content-Type"),
|
||||
File: c.Request.Body,
|
||||
Size: fileSize,
|
||||
Name: session.Name,
|
||||
VirtualPath: session.VirtualPath,
|
||||
SavePath: session.SavePath,
|
||||
Mode: mode,
|
||||
AppendStart: chunkSize * uint64(index),
|
||||
Model: file,
|
||||
LastModified: session.LastModified,
|
||||
}
|
||||
|
||||
// 给文件系统分配钩子
|
||||
fs.Use("AfterUploadCanceled", filesystem.HookTruncateFileTo(fileData.AppendStart))
|
||||
fs.Use("AfterValidateFailed", filesystem.HookTruncateFileTo(fileData.AppendStart))
|
||||
|
||||
if file != nil {
|
||||
fs.Use("BeforeUpload", filesystem.HookValidateCapacity)
|
||||
fs.Use("AfterUpload", filesystem.HookChunkUploaded)
|
||||
fs.Use("AfterValidateFailed", filesystem.HookChunkUploadFailed)
|
||||
if isLastChunk {
|
||||
fs.Use("AfterUpload", filesystem.HookPopPlaceholderToFile(""))
|
||||
fs.Use("AfterUpload", filesystem.HookDeleteUploadSession(session.Key))
|
||||
}
|
||||
} else {
|
||||
if isLastChunk {
|
||||
fs.Use("AfterUpload", filesystem.SlaveAfterUpload(session))
|
||||
fs.Use("AfterUpload", filesystem.HookDeleteUploadSession(session.Key))
|
||||
}
|
||||
req := &fs.UploadRequest{
|
||||
File: rc,
|
||||
Offset: chunkSize * int64(index),
|
||||
Props: session.Props.Copy(),
|
||||
Mode: mode,
|
||||
}
|
||||
|
||||
// 执行上传
|
||||
uploadCtx := context.WithValue(ctx, fsctx.GinCtx, c)
|
||||
err = fs.Upload(uploadCtx, &fileData)
|
||||
ctx := context.WithValue(c, cluster.SlaveNodeIDCtx{}, strconv.Itoa(session.Policy.NodeID))
|
||||
err = m.Upload(ctx, req, session.Policy)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeUploadFailed, err.Error(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
if rc, ok := req.File.(request.LimitReaderCloser); ok {
|
||||
if rc.Count() != expectedLength {
|
||||
err := fmt.Errorf("uploaded data(%d) does not match purposed size(%d)", rc.Count(), req.Props.Size)
|
||||
return serializer.NewError(serializer.CodeIOFailed, "Uploaded data does not match purposed size", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Finish upload
|
||||
if isLastChunk {
|
||||
_, err := m.CompleteUpload(ctx, session)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to complete upload: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UploadSessionService 上传会话服务
|
||||
type UploadSessionService struct {
|
||||
ID string `uri:"sessionId" binding:"required"`
|
||||
}
|
||||
type (
|
||||
DeleteUploadSessionParameterCtx struct{}
|
||||
DeleteUploadSessionService struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
Uri string `json:"uri" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
// Delete 删除指定上传会话
|
||||
func (service *UploadSessionService) Delete(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromContext(c)
|
||||
// Delete deletes the specified upload session
|
||||
func (service *DeleteUploadSessionService) Delete(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
m := manager.NewFileManager(dep, user)
|
||||
defer m.Recycle()
|
||||
|
||||
uri, err := fs.NewUriFromString(service.Uri)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 查找需要删除的上传会话的占位文件
|
||||
file, err := model.GetFilesByUploadSession(service.ID, fs.User.ID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeUploadSessionExpired, "", err)
|
||||
return serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
if err := fs.Delete(ctx, []uint{}, []uint{file.ID}, false, false); err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to delete upload session", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// SlaveDelete 从机删除指定上传会话
|
||||
func (service *UploadSessionService) SlaveDelete(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewAnonymousFileSystem()
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
session, ok := cache.Get(filesystem.UploadSessionCachePrefix + service.ID)
|
||||
if !ok {
|
||||
return serializer.Err(serializer.CodeUploadSessionExpired, "", nil)
|
||||
}
|
||||
|
||||
if _, err := fs.Handler.Delete(ctx, []string{session.(serializer.UploadSession).SavePath}); err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to delete temp file", err)
|
||||
}
|
||||
|
||||
cache.Deletes([]string{service.ID}, filesystem.UploadSessionCachePrefix)
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// DeleteAllUploadSession 删除当前用户的全部上传绘会话
|
||||
func DeleteAllUploadSession(ctx context.Context, c *gin.Context) serializer.Response {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromContext(c)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 查找需要删除的上传会话的占位文件
|
||||
files := model.GetUploadPlaceholderFiles(fs.User.ID)
|
||||
fileIDs := make([]uint, len(files))
|
||||
for i, file := range files {
|
||||
fileIDs[i] = file.ID
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
if err := fs.Delete(ctx, []uint{}, fileIDs, false, false); err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to cleanup upload session", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
return m.CancelUploadSession(c, uri, service.ID)
|
||||
}
|
||||
|
||||
394
service/explorer/viewer.go
Normal file
394
service/explorer/viewer.go
Normal file
@@ -0,0 +1,394 @@
|
||||
package explorer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/lock"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/wopi"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type WopiService struct {
|
||||
}
|
||||
|
||||
func prepareFs(c *gin.Context) (*fs.URI, manager.FileManager, *ent.User, *manager.ViewerSessionCache, dependency.Dep, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
m := manager.NewFileManager(dep, user)
|
||||
defer m.Recycle()
|
||||
|
||||
viewerSession := manager.ViewerSessionFromContext(c)
|
||||
uri, err := fs.NewUriFromString(viewerSession.Uri)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
|
||||
}
|
||||
|
||||
return uri, m, user, viewerSession, dep, nil
|
||||
}
|
||||
|
||||
func (service *WopiService) Unlock(c *gin.Context) error {
|
||||
_, m, _, _, dep, err := prepareFs(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l := dep.Logger()
|
||||
|
||||
lockToken := c.GetHeader(wopi.LockTokenHeader)
|
||||
if err = m.Unlock(c, lockToken); err != nil {
|
||||
l.Debug("WOPI unlock, not locked or not match: %w", err)
|
||||
c.Status(http.StatusConflict)
|
||||
c.Header(wopi.LockTokenHeader, "")
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *WopiService) RefreshLock(c *gin.Context) error {
|
||||
uri, m, _, _, dep, err := prepareFs(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l := dep.Logger()
|
||||
|
||||
// Make sure file exists and readable
|
||||
file, err := m.Get(c, uri, dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityLockFile))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get file: %w", err)
|
||||
}
|
||||
|
||||
lockToken := c.GetHeader(wopi.LockTokenHeader)
|
||||
release, _, err := m.ConfirmLock(c, file, file.Uri(false), lockToken)
|
||||
if err != nil {
|
||||
// File not locked for token not match
|
||||
|
||||
l.Debug("WOPI refresh lock, not locked or not match: %w", err)
|
||||
c.Status(http.StatusConflict)
|
||||
c.Header(wopi.LockTokenHeader, "")
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// refresh lock
|
||||
release()
|
||||
_, err = m.Refresh(c, wopi.LockDuration, lockToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Header(wopi.LockTokenHeader, lockToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *WopiService) Lock(c *gin.Context) error {
|
||||
uri, m, user, viewerSession, dep, err := prepareFs(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l := dep.Logger()
|
||||
|
||||
// Make sure file exists and readable
|
||||
file, err := m.Get(c, uri, dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityLockFile))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get file: %w", err)
|
||||
}
|
||||
|
||||
lockToken := c.GetHeader(wopi.LockTokenHeader)
|
||||
release, _, err := m.ConfirmLock(c, file, file.Uri(false), lockToken)
|
||||
if err != nil {
|
||||
// File not locked for token not match
|
||||
|
||||
// Try to lock using given token
|
||||
app := lock.Application{
|
||||
Type: string(fs.ApplicationViewer),
|
||||
ViewerID: viewerSession.ViewerID,
|
||||
}
|
||||
_, err = m.Lock(c, wopi.LockDuration, user, true, app, file.Uri(false), lockToken)
|
||||
if err != nil {
|
||||
// Token not match
|
||||
var lockConflict lock.ConflictError
|
||||
if errors.As(err, &lockConflict) {
|
||||
c.Status(http.StatusConflict)
|
||||
c.Header(wopi.LockTokenHeader, lockConflict[0].Token)
|
||||
|
||||
l.Debug("WOPI lock, lock conflict: %w", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to lock file: %w", err)
|
||||
}
|
||||
|
||||
// Lock success, return the token
|
||||
c.Header(wopi.LockTokenHeader, lockToken)
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// refresh lock
|
||||
release()
|
||||
_, err = m.Refresh(c, wopi.LockDuration, lockToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Header(wopi.LockTokenHeader, lockToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *WopiService) PutContent(c *gin.Context) error {
|
||||
uri, m, user, viewerSession, _, err := prepareFs(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Make sure file exists and readable
|
||||
file, err := m.Get(c, uri, dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityUploadFile))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get file: %w", err)
|
||||
}
|
||||
|
||||
var lockSession fs.LockSession
|
||||
lockToken := c.GetHeader(wopi.LockTokenHeader)
|
||||
if lockToken != "" {
|
||||
// File not locked for token not match
|
||||
|
||||
release, ls, err := m.ConfirmLock(c, file, file.Uri(false), lockToken)
|
||||
if err != nil {
|
||||
// File not locked for token not match
|
||||
|
||||
// Try to lock using given token
|
||||
app := lock.Application{
|
||||
Type: string(fs.ApplicationViewer),
|
||||
ViewerID: viewerSession.ViewerID,
|
||||
}
|
||||
ls, err := m.Lock(c, wopi.LockDuration, user, true, app, file.Uri(false), lockToken)
|
||||
if err != nil {
|
||||
// Token not match
|
||||
// If the file is currently locked and the X-WOPI-Lock value doesn't match the lock currently on the file, the host must
|
||||
//
|
||||
// Return a lock mismatch response (409 Conflict)
|
||||
// Include an X-WOPI-Lock response header containing the value of the current lock on the file.
|
||||
var lockConflict lock.ConflictError
|
||||
if errors.As(err, &lockConflict) {
|
||||
c.Status(http.StatusConflict)
|
||||
c.Header(wopi.LockTokenHeader, lockConflict[0].Token)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to lock file: %w", err)
|
||||
}
|
||||
|
||||
// In cases where the file is unlocked, the host must set X-WOPI-Lock to the empty string.
|
||||
c.Header(wopi.LockTokenHeader, "")
|
||||
_ = m.Unlock(c, ls.LastToken())
|
||||
} else {
|
||||
defer release()
|
||||
}
|
||||
|
||||
lockSession = ls
|
||||
}
|
||||
|
||||
subService := FileUpdateService{
|
||||
Uri: viewerSession.Uri,
|
||||
}
|
||||
|
||||
res, err := subService.PutContent(c, lockSession)
|
||||
if err != nil {
|
||||
var appErr serializer.AppError
|
||||
if errors.As(err, &appErr) {
|
||||
switch appErr.Code {
|
||||
case serializer.CodeFileTooLarge:
|
||||
c.Status(http.StatusRequestEntityTooLarge)
|
||||
c.Header(wopi.ServerErrorHeader, err.Error())
|
||||
case serializer.CodeNotFound:
|
||||
c.Status(http.StatusNotFound)
|
||||
c.Header(wopi.ServerErrorHeader, err.Error())
|
||||
case 0:
|
||||
c.Status(http.StatusOK)
|
||||
default:
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
c.Header(wopi.ItemVersionHeader, res.PrimaryEntity)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *WopiService) GetFile(c *gin.Context) error {
|
||||
uri, m, _, viewerSession, dep, err := prepareFs(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Make sure file exists and readable
|
||||
file, err := m.Get(c, uri, dbfs.WithExtendedInfo(), dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityDownloadFile))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get file: %w", err)
|
||||
}
|
||||
|
||||
versionType := types.EntityTypeVersion
|
||||
find, targetEntity := fs.FindDesiredEntity(file, viewerSession.Version, dep.HashIDEncoder(), &versionType)
|
||||
if !find {
|
||||
return serializer.NewError(serializer.CodeNotFound, "version not found", nil)
|
||||
}
|
||||
|
||||
if targetEntity.Size() > dep.SettingProvider().MaxOnlineEditSize(c) {
|
||||
return fs.ErrFileSizeTooBig
|
||||
}
|
||||
|
||||
entitySource, err := m.GetEntitySource(c, targetEntity.ID(), fs.WithEntity(targetEntity))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get entity source: %w", err)
|
||||
}
|
||||
|
||||
defer entitySource.Close()
|
||||
|
||||
entitySource.Serve(c.Writer, c.Request,
|
||||
entitysource.WithContext(c),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *WopiService) FileInfo(c *gin.Context) (*WopiFileInfo, error) {
|
||||
uri, m, user, viewerSession, dep, err := prepareFs(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hasher := dep.HashIDEncoder()
|
||||
settings := dep.SettingProvider()
|
||||
|
||||
opts := []fs.Option{
|
||||
dbfs.WithFilePublicMetadata(),
|
||||
dbfs.WithExtendedInfo(),
|
||||
dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityDownloadFile, dbfs.NavigatorCapabilityInfo, dbfs.NavigatorCapabilityUploadFile),
|
||||
}
|
||||
file, err := m.Get(c, uri, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get file: %w", err)
|
||||
}
|
||||
|
||||
if file == nil {
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "file not found", nil)
|
||||
}
|
||||
|
||||
versionType := types.EntityTypeVersion
|
||||
find, targetEntity := fs.FindDesiredEntity(file, viewerSession.Version, hasher, &versionType)
|
||||
if !find {
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "version not found", nil)
|
||||
}
|
||||
|
||||
canEdit := file.PrimaryEntityID() == targetEntity.ID() && file.OwnerID() == user.ID
|
||||
siteUrl := settings.SiteURL(c)
|
||||
info := &WopiFileInfo{
|
||||
BaseFileName: file.DisplayName(),
|
||||
Version: hashid.EncodeEntityID(hasher, targetEntity.ID()),
|
||||
BreadcrumbBrandName: settings.SiteBasic(c).Name,
|
||||
BreadcrumbBrandUrl: siteUrl.String(),
|
||||
FileSharingPostMessage: file.OwnerID() == user.ID,
|
||||
EnableShare: file.OwnerID() == user.ID,
|
||||
FileVersionPostMessage: true,
|
||||
ClosePostMessage: true,
|
||||
PostMessageOrigin: "*",
|
||||
FileNameMaxLength: dbfs.MaxFileNameLength,
|
||||
LastModifiedTime: file.UpdatedAt().Format(time.RFC3339),
|
||||
IsAnonymousUser: inventory.IsAnonymousUser(user),
|
||||
UserFriendlyName: user.Nick,
|
||||
UserId: hashid.EncodeUserID(hasher, user.ID),
|
||||
ReadOnly: !canEdit,
|
||||
Size: targetEntity.Size(),
|
||||
OwnerId: hashid.EncodeUserID(hasher, file.OwnerID()),
|
||||
SupportsRename: true,
|
||||
SupportsReviewing: true,
|
||||
SupportsLocks: true,
|
||||
UserCanReview: canEdit,
|
||||
UserCanWrite: canEdit,
|
||||
BreadcrumbFolderName: uri.Dir(),
|
||||
BreadcrumbFolderUrl: routes.FrontendHomeUrl(siteUrl, uri.DirUri().String()).String(),
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
type (
|
||||
CreateViewerSessionService struct {
|
||||
Uri string `json:"uri" form:"uri" binding:"required"`
|
||||
Version string `json:"version" form:"version"`
|
||||
ViewerID string `json:"viewer_id" form:"viewer_id" binding:"required"`
|
||||
PreferredAction setting.ViewerAction `json:"preferred_action" form:"preferred_action" binding:"required"`
|
||||
}
|
||||
CreateViewerSessionParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *CreateViewerSessionService) Create(c *gin.Context) (*ViewerSessionResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
m := manager.NewFileManager(dep, user)
|
||||
defer m.Recycle()
|
||||
|
||||
uri, err := fs.NewUriFromString(s.Uri)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
|
||||
}
|
||||
|
||||
// Find the given viewer
|
||||
viewers := dep.SettingProvider().FileViewers(c)
|
||||
var targetViewer *setting.Viewer
|
||||
for _, group := range viewers {
|
||||
for _, viewer := range group.Viewers {
|
||||
if viewer.ID == s.ViewerID && !viewer.Disabled {
|
||||
targetViewer = &viewer
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetViewer != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetViewer == nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "unknown viewer id", err)
|
||||
}
|
||||
|
||||
viewerSession, err := m.CreateViewerSession(c, uri, s.Version, targetViewer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := &ViewerSessionResponse{Session: viewerSession}
|
||||
if targetViewer.Type == setting.ViewerTypeWopi {
|
||||
// For WOPI viewer, generate WOPI src
|
||||
wopiSrc, err := wopi.GenerateWopiSrc(c, s.PreferredAction, targetViewer, viewerSession)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "failed to generate wopi src", err)
|
||||
}
|
||||
res.WopiSrc = wopiSrc.String()
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
package explorer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cloudreve/Cloudreve/v3/middleware"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/wopi"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type WopiService struct {
|
||||
}
|
||||
|
||||
func (service *WopiService) Rename(c *gin.Context) error {
|
||||
fs, _, err := service.prepareFs(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer fs.Recycle()
|
||||
|
||||
return fs.Rename(c, []uint{}, []uint{c.MustGet("object_id").(uint)}, c.GetHeader(wopi.RenameRequestHeader))
|
||||
}
|
||||
|
||||
func (service *WopiService) GetFile(c *gin.Context) error {
|
||||
fs, _, err := service.prepareFs(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer fs.Recycle()
|
||||
|
||||
resp, err := fs.Preview(c, fs.FileTarget[0].ID, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to pull file content: %w", err)
|
||||
}
|
||||
|
||||
// 重定向到文件源
|
||||
if resp.Redirect {
|
||||
return fmt.Errorf("redirect not supported in WOPI")
|
||||
}
|
||||
|
||||
// 直接返回文件内容
|
||||
defer resp.Content.Close()
|
||||
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
http.ServeContent(c.Writer, c.Request, fs.FileTarget[0].Name, fs.FileTarget[0].UpdatedAt, resp.Content)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *WopiService) FileInfo(c *gin.Context) (*serializer.WopiFileInfo, error) {
|
||||
fs, session, err := service.prepareFs(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer fs.Recycle()
|
||||
|
||||
parent, err := model.GetFoldersByIDs([]uint{fs.FileTarget[0].FolderID}, fs.User.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(parent) == 0 {
|
||||
return nil, fmt.Errorf("failed to find parent folder")
|
||||
}
|
||||
|
||||
parent[0].TraceRoot()
|
||||
siteUrl := model.GetSiteURL()
|
||||
|
||||
// Generate url for parent folder
|
||||
parentUrl := model.GetSiteURL()
|
||||
parentUrl.Path = "/home"
|
||||
query := parentUrl.Query()
|
||||
query.Set("path", parent[0].Position)
|
||||
parentUrl.RawQuery = query.Encode()
|
||||
|
||||
info := &serializer.WopiFileInfo{
|
||||
BaseFileName: fs.FileTarget[0].Name,
|
||||
Version: fs.FileTarget[0].Model.UpdatedAt.String(),
|
||||
BreadcrumbBrandName: model.GetSettingByName("siteName"),
|
||||
BreadcrumbBrandUrl: siteUrl.String(),
|
||||
FileSharingPostMessage: false,
|
||||
PostMessageOrigin: "*",
|
||||
FileNameMaxLength: 256,
|
||||
LastModifiedTime: fs.FileTarget[0].Model.UpdatedAt.Format(time.RFC3339),
|
||||
IsAnonymousUser: true,
|
||||
ReadOnly: true,
|
||||
ClosePostMessage: true,
|
||||
Size: int64(fs.FileTarget[0].Size),
|
||||
OwnerId: hashid.HashID(fs.FileTarget[0].UserID, hashid.UserID),
|
||||
}
|
||||
|
||||
if session.Action == wopi.ActionEdit {
|
||||
info.FileSharingPostMessage = true
|
||||
info.IsAnonymousUser = false
|
||||
info.SupportsRename = true
|
||||
info.SupportsReviewing = true
|
||||
info.SupportsUpdate = true
|
||||
info.UserFriendlyName = fs.User.Nick
|
||||
info.UserId = hashid.HashID(fs.User.ID, hashid.UserID)
|
||||
info.UserCanRename = true
|
||||
info.UserCanReview = true
|
||||
info.UserCanWrite = true
|
||||
info.ReadOnly = false
|
||||
info.BreadcrumbFolderName = parent[0].Name
|
||||
info.BreadcrumbFolderUrl = parentUrl.String()
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (service *WopiService) prepareFs(c *gin.Context) (*filesystem.FileSystem, *wopi.SessionCache, error) {
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystemFromContext(c)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
session := c.MustGet(middleware.WopiSessionCtx).(*wopi.SessionCache)
|
||||
if err := fs.SetTargetFileByIDs([]uint{session.FileID}); err != nil {
|
||||
fs.Recycle()
|
||||
return nil, nil, fmt.Errorf("failed to find file: %w", err)
|
||||
}
|
||||
|
||||
maxSize := model.GetIntSetting("maxEditSize", 0)
|
||||
if maxSize > 0 && fs.FileTarget[0].Size > uint64(maxSize) {
|
||||
return nil, nil, errors.New("file too large")
|
||||
}
|
||||
|
||||
return fs, session, nil
|
||||
}
|
||||
398
service/explorer/workflows.go
Normal file
398
service/explorer/workflows.go
Normal file
@@ -0,0 +1,398 @@
|
||||
package explorer
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/task"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/downloader"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/workflows"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/queue"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/tools/container/intsets"
|
||||
)
|
||||
|
||||
// ItemMoveService 处理多文件/目录移动
|
||||
type ItemMoveService struct {
|
||||
SrcDir string `json:"src_dir" binding:"required,min=1,max=65535"`
|
||||
Src ItemIDService `json:"src"`
|
||||
Dst string `json:"dst" binding:"required,min=1,max=65535"`
|
||||
}
|
||||
|
||||
// ItemRenameService 处理多文件/目录重命名
|
||||
type ItemRenameService struct {
|
||||
Src ItemIDService `json:"src"`
|
||||
NewName string `json:"new_name" binding:"required,min=1,max=255"`
|
||||
}
|
||||
|
||||
// ItemService 处理多文件/目录相关服务
|
||||
type ItemService struct {
|
||||
Items []uint `json:"items"`
|
||||
Dirs []uint `json:"dirs"`
|
||||
}
|
||||
|
||||
// ItemIDService 处理多文件/目录相关服务,字段值为HashID,可通过Raw()方法获取原始ID
|
||||
type ItemIDService struct {
|
||||
Items []string `json:"items"`
|
||||
Dirs []string `json:"dirs"`
|
||||
Source *ItemService
|
||||
Force bool `json:"force"`
|
||||
UnlinkOnly bool `json:"unlink"`
|
||||
}
|
||||
|
||||
// ItemDecompressService 文件解压缩任务服务
|
||||
type ItemDecompressService struct {
|
||||
Src string `json:"src"`
|
||||
Dst string `json:"dst" binding:"required,min=1,max=65535"`
|
||||
Encoding string `json:"encoding"`
|
||||
}
|
||||
|
||||
// ItemPropertyService 获取对象属性服务
|
||||
type ItemPropertyService struct {
|
||||
ID string `binding:"required"`
|
||||
TraceRoot bool `form:"trace_root"`
|
||||
IsFolder bool `form:"is_folder"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register(ItemIDService{})
|
||||
}
|
||||
|
||||
type (
|
||||
DownloadWorkflowService struct {
|
||||
Src []string `json:"src"`
|
||||
SrcFile string `json:"src_file"`
|
||||
Dst string `json:"dst" binding:"required"`
|
||||
}
|
||||
CreateDownloadParamCtx struct{}
|
||||
)
|
||||
|
||||
func (service *DownloadWorkflowService) CreateDownloadTask(c *gin.Context) ([]*TaskResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
hasher := dep.HashIDEncoder()
|
||||
m := manager.NewFileManager(dep, user)
|
||||
defer m.Recycle()
|
||||
|
||||
if !user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionRemoteDownload)) {
|
||||
return nil, serializer.NewError(serializer.CodeGroupNotAllowed, "Group not allowed to download files", nil)
|
||||
}
|
||||
|
||||
// Src must be set
|
||||
if service.SrcFile == "" && len(service.Src) == 0 {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "No source files", nil)
|
||||
}
|
||||
|
||||
// Only one of src and src_file can be set
|
||||
if service.SrcFile != "" && len(service.Src) > 0 {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid source files", nil)
|
||||
}
|
||||
|
||||
dst, err := fs.NewUriFromString(service.Dst)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid destination", err)
|
||||
}
|
||||
|
||||
// Validate dst
|
||||
_, err = m.Get(c, dst, dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityCreateFile))
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid destination", err)
|
||||
}
|
||||
|
||||
// 检查批量任务数量
|
||||
limit := user.Edges.Group.Settings.Aria2BatchSize
|
||||
if limit > 0 && len(service.Src) > limit {
|
||||
return nil, serializer.NewError(serializer.CodeBatchAria2Size, "", nil)
|
||||
}
|
||||
|
||||
// Validate src file
|
||||
if service.SrcFile != "" {
|
||||
src, err := fs.NewUriFromString(service.SrcFile)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid source file uri", err)
|
||||
}
|
||||
|
||||
_, err = m.Get(c, src, dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityDownloadFile))
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid source file", err)
|
||||
}
|
||||
}
|
||||
|
||||
// batch creating tasks
|
||||
ae := serializer.NewAggregateError()
|
||||
tasks := make([]queue.Task, 0, len(service.Src))
|
||||
for _, src := range service.Src {
|
||||
if src == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
t, err := workflows.NewRemoteDownloadTask(c, src, service.SrcFile, service.Dst)
|
||||
if err != nil {
|
||||
ae.Add(src, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := dep.RemoteDownloadQueue(c).QueueTask(c, t); err != nil {
|
||||
ae.Add(src, err)
|
||||
}
|
||||
|
||||
tasks = append(tasks, t)
|
||||
}
|
||||
|
||||
if service.SrcFile != "" {
|
||||
t, err := workflows.NewRemoteDownloadTask(c, "", service.SrcFile, service.Dst)
|
||||
if err != nil {
|
||||
ae.Add(service.SrcFile, err)
|
||||
}
|
||||
|
||||
if err := dep.RemoteDownloadQueue(c).QueueTask(c, t); err != nil {
|
||||
ae.Add(service.SrcFile, err)
|
||||
}
|
||||
|
||||
tasks = append(tasks, t)
|
||||
}
|
||||
|
||||
return lo.Map(tasks, func(item queue.Task, index int) *TaskResponse {
|
||||
return BuildTaskResponse(item, nil, hasher)
|
||||
}), ae.Aggregate()
|
||||
}
|
||||
|
||||
type (
|
||||
ArchiveWorkflowService struct {
|
||||
Src []string `json:"src" binding:"required"`
|
||||
Dst string `json:"dst" binding:"required"`
|
||||
Encoding string `json:"encoding"`
|
||||
}
|
||||
CreateArchiveParamCtx struct{}
|
||||
)
|
||||
|
||||
func (service *ArchiveWorkflowService) CreateExtractTask(c *gin.Context) (*TaskResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
hasher := dep.HashIDEncoder()
|
||||
m := manager.NewFileManager(dep, user)
|
||||
defer m.Recycle()
|
||||
|
||||
if !user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionArchiveTask)) {
|
||||
return nil, serializer.NewError(serializer.CodeGroupNotAllowed, "Group not allowed to compress files", nil)
|
||||
}
|
||||
|
||||
dst, err := fs.NewUriFromString(service.Dst)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid destination", err)
|
||||
}
|
||||
|
||||
if len(service.Src) == 0 {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "No source files", nil)
|
||||
}
|
||||
|
||||
// Validate destination
|
||||
if _, err := m.Get(c, dst, dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityCreateFile)); err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid destination", err)
|
||||
}
|
||||
|
||||
// Create task
|
||||
t, err := workflows.NewExtractArchiveTask(c, service.Src[0], service.Dst, service.Encoding)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeCreateTaskError, "Failed to create task", err)
|
||||
}
|
||||
|
||||
if err := dep.IoIntenseQueue(c).QueueTask(c, t); err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeCreateTaskError, "Failed to queue task", err)
|
||||
}
|
||||
|
||||
return BuildTaskResponse(t, nil, hasher), nil
|
||||
}
|
||||
|
||||
// CreateCompressTask Create task to create an archive file
|
||||
func (service *ArchiveWorkflowService) CreateCompressTask(c *gin.Context) (*TaskResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
hasher := dep.HashIDEncoder()
|
||||
m := manager.NewFileManager(dep, user)
|
||||
defer m.Recycle()
|
||||
|
||||
if !user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionArchiveTask)) {
|
||||
return nil, serializer.NewError(serializer.CodeGroupNotAllowed, "Group not allowed to compress files", nil)
|
||||
}
|
||||
|
||||
dst, err := fs.NewUriFromString(service.Dst)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid destination", err)
|
||||
}
|
||||
|
||||
// Create a placeholder file then delete it to validate the destination
|
||||
session, err := m.PrepareUpload(c, &fs.UploadRequest{
|
||||
Props: &fs.UploadProps{
|
||||
Uri: dst,
|
||||
Size: 0,
|
||||
UploadSessionID: uuid.Must(uuid.NewV4()).String(),
|
||||
ExpireAt: time.Now().Add(time.Second * 3600),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.OnUploadFailed(c, session)
|
||||
|
||||
// Create task
|
||||
t, err := workflows.NewCreateArchiveTask(c, service.Src, service.Dst)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeCreateTaskError, "Failed to create task", err)
|
||||
}
|
||||
|
||||
if err := dep.IoIntenseQueue(c).QueueTask(c, t); err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeCreateTaskError, "Failed to queue task", err)
|
||||
}
|
||||
|
||||
return BuildTaskResponse(t, nil, hasher), nil
|
||||
}
|
||||
|
||||
type (
|
||||
ListTaskService struct {
|
||||
PageSize int `form:"page_size" binding:"required,min=10,max=100"`
|
||||
Category string `form:"category" binding:"required,eq=general|eq=downloading|eq=downloaded"`
|
||||
NextPageToken string `form:"next_page_token"`
|
||||
}
|
||||
ListTaskParamCtx struct{}
|
||||
)
|
||||
|
||||
func (service *ListTaskService) ListTasks(c *gin.Context) (*TaskListResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
hasher := dep.HashIDEncoder()
|
||||
taskClient := dep.TaskClient()
|
||||
|
||||
args := &inventory.ListTaskArgs{
|
||||
PaginationArgs: &inventory.PaginationArgs{
|
||||
UseCursorPagination: true,
|
||||
PageToken: service.NextPageToken,
|
||||
PageSize: service.PageSize,
|
||||
},
|
||||
Types: []string{queue.CreateArchiveTaskType, queue.ExtractArchiveTaskType, queue.RelocateTaskType},
|
||||
UserID: user.ID,
|
||||
}
|
||||
|
||||
if service.Category != "general" {
|
||||
args.Types = []string{queue.RemoteDownloadTaskType}
|
||||
if service.Category == "downloading" {
|
||||
args.PageSize = intsets.MaxInt
|
||||
args.Status = []task.Status{task.StatusSuspending, task.StatusProcessing, task.StatusQueued}
|
||||
} else if service.Category == "downloaded" {
|
||||
args.Status = []task.Status{task.StatusCanceled, task.StatusError, task.StatusCompleted}
|
||||
}
|
||||
}
|
||||
|
||||
// Get tasks
|
||||
res, err := taskClient.List(c, args)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to query tasks", err)
|
||||
}
|
||||
|
||||
tasks := make([]queue.Task, 0, len(res.Tasks))
|
||||
nodeMap := make(map[int]*ent.Node)
|
||||
for _, t := range res.Tasks {
|
||||
task, err := queue.NewTaskFromModel(t)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to parse task", err)
|
||||
}
|
||||
|
||||
summary := task.Summarize(hasher)
|
||||
if summary != nil && summary.NodeID > 0 {
|
||||
if _, ok := nodeMap[summary.NodeID]; !ok {
|
||||
nodeMap[summary.NodeID] = nil
|
||||
}
|
||||
}
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
|
||||
// Get nodes
|
||||
nodes, err := dep.NodeClient().ListActiveNodes(c, lo.Keys(nodeMap))
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to query nodes", err)
|
||||
}
|
||||
for _, n := range nodes {
|
||||
nodeMap[n.ID] = n
|
||||
}
|
||||
|
||||
// Build response
|
||||
return BuildTaskListResponse(tasks, res, nodeMap, hasher), nil
|
||||
}
|
||||
|
||||
func TaskPhaseProgress(c *gin.Context, taskID int) (queue.Progresses, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
u := inventory.UserFromContext(c)
|
||||
r := dep.TaskRegistry()
|
||||
t, found := r.Get(taskID)
|
||||
if !found || t.Owner().ID != u.ID {
|
||||
return queue.Progresses{}, nil
|
||||
}
|
||||
|
||||
return t.Progress(c), nil
|
||||
}
|
||||
|
||||
func CancelDownloadTask(c *gin.Context, taskID int) error {
|
||||
dep := dependency.FromContext(c)
|
||||
u := inventory.UserFromContext(c)
|
||||
r := dep.TaskRegistry()
|
||||
t, found := r.Get(taskID)
|
||||
if !found || t.Owner().ID != u.ID {
|
||||
return serializer.NewError(serializer.CodeNotFound, "Task not found", nil)
|
||||
}
|
||||
|
||||
if downloadTask, ok := t.(*workflows.RemoteDownloadTask); ok {
|
||||
if err := downloadTask.CancelDownload(c); err != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to cancel download task", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
SetDownloadFilesService struct {
|
||||
Files []*downloader.SetFileToDownloadArgs `json:"files" binding:"required"`
|
||||
}
|
||||
SetDownloadFilesParamCtx struct{}
|
||||
)
|
||||
|
||||
func (service *SetDownloadFilesService) SetDownloadFiles(c *gin.Context, taskID int) error {
|
||||
dep := dependency.FromContext(c)
|
||||
u := inventory.UserFromContext(c)
|
||||
r := dep.TaskRegistry()
|
||||
|
||||
t, found := r.Get(taskID)
|
||||
if !found || t.Owner().ID != u.ID {
|
||||
return serializer.NewError(serializer.CodeNotFound, "Task not found", nil)
|
||||
}
|
||||
|
||||
status := t.Status()
|
||||
summary := t.Summarize(dep.HashIDEncoder())
|
||||
// Task must be in processing state
|
||||
if status != task.StatusSuspending && status != task.StatusProcessing {
|
||||
return serializer.NewError(serializer.CodeNotFound, "Task not in processing state", nil)
|
||||
}
|
||||
|
||||
// Task must in monitoring loop
|
||||
if summary.Phase != workflows.RemoteDownloadTaskPhaseMonitor {
|
||||
return serializer.NewError(serializer.CodeNotFound, "Task not in monitoring loop", nil)
|
||||
}
|
||||
|
||||
if downloadTask, ok := t.(*workflows.RemoteDownloadTask); ok {
|
||||
if err := downloadTask.SetDownloadTarget(c, service.Files...); err != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to set download files", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/googledrive"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/onedrive"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/oauth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/mq"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SlaveNotificationService struct {
|
||||
Subject string `uri:"subject" binding:"required"`
|
||||
}
|
||||
|
||||
type OauthCredentialService struct {
|
||||
PolicyID uint `uri:"id" binding:"required"`
|
||||
}
|
||||
|
||||
func HandleMasterHeartbeat(req *serializer.NodePingReq) serializer.Response {
|
||||
res, err := cluster.DefaultController.HandleHeartBeat(req)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Cannot initialize slave controller", err)
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Code: 0,
|
||||
Data: res,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleSlaveNotificationPush 转发从机的消息通知到本机消息队列
|
||||
func (s *SlaveNotificationService) HandleSlaveNotificationPush(c *gin.Context) serializer.Response {
|
||||
var msg mq.Message
|
||||
dec := gob.NewDecoder(c.Request.Body)
|
||||
if err := dec.Decode(&msg); err != nil {
|
||||
return serializer.ParamErr("Cannot parse notification message", err)
|
||||
}
|
||||
|
||||
mq.GlobalMQ.Publish(s.Subject, msg)
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Get 获取主机Oauth策略的AccessToken
|
||||
func (s *OauthCredentialService) Get(c *gin.Context) serializer.Response {
|
||||
policy, err := model.GetPolicyByID(s.PolicyID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodePolicyNotExist, "", err)
|
||||
}
|
||||
|
||||
var client oauth.TokenProvider
|
||||
switch policy.Type {
|
||||
case "onedrive":
|
||||
client, err = onedrive.NewClient(&policy)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Cannot initialize OneDrive client", err)
|
||||
}
|
||||
case "googledrive":
|
||||
client, err = googledrive.NewClient(&policy)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Cannot initialize Google Drive client", err)
|
||||
}
|
||||
default:
|
||||
return serializer.Err(serializer.CodePolicyNotExist, "", nil)
|
||||
}
|
||||
|
||||
if err := client.UpdateCredential(c, conf.SystemConfig.Mode == "slave"); err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Cannot refresh OneDrive credential", err)
|
||||
}
|
||||
|
||||
return serializer.Response{Data: client.AccessToken()}
|
||||
}
|
||||
1
service/node/response.go
Normal file
1
service/node/response.go
Normal file
@@ -0,0 +1 @@
|
||||
package node
|
||||
120
service/node/rpc.go
Normal file
120
service/node/rpc.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/credmanager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SlaveNotificationService struct {
|
||||
Subject string `uri:"subject" binding:"required"`
|
||||
}
|
||||
|
||||
type (
|
||||
OauthCredentialParamCtx struct{}
|
||||
OauthCredentialService struct {
|
||||
ID string `uri:"id" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
// Get 获取主机Oauth策略的AccessToken
|
||||
func (s *OauthCredentialService) Get(c *gin.Context) (*credmanager.CredentialResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
credManager := dep.CredManager()
|
||||
|
||||
cred, err := credManager.Obtain(c, s.ID)
|
||||
if cred == nil || err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "Credential not found", err)
|
||||
}
|
||||
|
||||
return &credmanager.CredentialResponse{
|
||||
Token: cred.String(),
|
||||
ExpireAt: cred.Expiry(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
StatelessPrepareUploadParamCtx struct{}
|
||||
)
|
||||
|
||||
func StatelessPrepareUpload(s *fs.StatelessPrepareUploadService, c *gin.Context) (*fs.StatelessPrepareUploadResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
user, err := userClient.GetLoginUserByID(c, s.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := context.WithValue(c.Request.Context(), inventory.UserCtx{}, user)
|
||||
fm := manager.NewFileManager(dep, user)
|
||||
uploadSession, err := fm.PrepareUpload(ctx, s.UploadRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fs.StatelessPrepareUploadResponse{
|
||||
Session: uploadSession,
|
||||
Req: s.UploadRequest,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
StatelessCompleteUploadParamCtx struct{}
|
||||
)
|
||||
|
||||
func StatelessCompleteUpload(s *fs.StatelessCompleteUploadService, c *gin.Context) (fs.File, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
user, err := userClient.GetLoginUserByID(c, s.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
util.WithValue(c, inventory.UserCtx{}, user)
|
||||
fm := manager.NewFileManager(dep, user)
|
||||
return fm.CompleteUpload(c, s.UploadSession)
|
||||
}
|
||||
|
||||
type (
|
||||
StatelessOnUploadFailedParamCtx struct{}
|
||||
)
|
||||
|
||||
func StatelessOnUploadFailed(s *fs.StatelessOnUploadFailedService, c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
user, err := userClient.GetLoginUserByID(c, s.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
util.WithValue(c, inventory.UserCtx{}, user)
|
||||
fm := manager.NewFileManager(dep, user)
|
||||
fm.OnUploadFailed(c, s.UploadSession)
|
||||
return nil
|
||||
}
|
||||
|
||||
type StatelessCreateFileParamCtx struct{}
|
||||
|
||||
func StatelessCreateFile(s *fs.StatelessCreateFileService, c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
user, err := userClient.GetLoginUserByID(c, s.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri, err := fs.NewUriFromString(s.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
util.WithValue(c, inventory.UserCtx{}, user)
|
||||
fm := manager.NewFileManager(dep, user)
|
||||
_, err = fm.Create(c, uri, s.Type)
|
||||
return err
|
||||
}
|
||||
150
service/node/task.go
Normal file
150
service/node/task.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/task"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/workflows"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/queue"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type (
|
||||
CreateSlaveTaskParamCtx struct{}
|
||||
)
|
||||
|
||||
func CreateTaskInSlave(s *cluster.CreateSlaveTask, c *gin.Context) (int, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
registry := dep.TaskRegistry()
|
||||
|
||||
props, err := slaveTaskPropsFromContext(c)
|
||||
if err != nil {
|
||||
return 0, serializer.NewError(serializer.CodeParamErr, "failed to get master props from header", err)
|
||||
}
|
||||
|
||||
var t queue.Task
|
||||
switch s.Type {
|
||||
case queue.SlaveUploadTaskType:
|
||||
t = workflows.NewSlaveUploadTask(c, props, registry.NextID(), s.State)
|
||||
case queue.SlaveCreateArchiveTaskType:
|
||||
t = workflows.NewSlaveCreateArchiveTask(c, props, registry.NextID(), s.State)
|
||||
case queue.SlaveExtractArchiveType:
|
||||
t = workflows.NewSlaveExtractArchiveTask(c, props, registry.NextID(), s.State)
|
||||
default:
|
||||
return 0, serializer.NewError(serializer.CodeParamErr, "type not supported", nil)
|
||||
}
|
||||
|
||||
if err := dep.SlaveQueue(c).QueueTask(c, t); err != nil {
|
||||
return 0, serializer.NewError(serializer.CodeInternalSetting, "failed to queue task", err)
|
||||
}
|
||||
|
||||
registry.Set(t.ID(), t)
|
||||
return t.ID(), nil
|
||||
}
|
||||
|
||||
type (
|
||||
GetSlaveTaskParamCtx struct{}
|
||||
GetSlaveTaskService struct {
|
||||
ID int `uri:"id" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
func (s *GetSlaveTaskService) Get(c *gin.Context) (*cluster.SlaveTaskSummary, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
registry := dep.TaskRegistry()
|
||||
|
||||
t, ok := registry.Get(s.ID)
|
||||
if !ok {
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "task not found", nil)
|
||||
}
|
||||
status := t.Status()
|
||||
_, clearOnComplete := c.GetQuery(routes.SlaveClearTaskRegistryQuery)
|
||||
if clearOnComplete && status == task.StatusCompleted ||
|
||||
status == task.StatusError ||
|
||||
status == task.StatusCanceled {
|
||||
registry.Delete(s.ID)
|
||||
}
|
||||
|
||||
res := &cluster.SlaveTaskSummary{
|
||||
Status: status,
|
||||
PrivateState: t.State(),
|
||||
Progress: t.Progress(c),
|
||||
}
|
||||
err := t.Error()
|
||||
if err != nil {
|
||||
res.Error = err.Error()
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func slaveTaskPropsFromContext(ctx context.Context) (*types.SlaveTaskProps, error) {
|
||||
nodeIdStr, ok := ctx.Value(cluster.SlaveNodeIDCtx{}).(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to get node ID from context")
|
||||
}
|
||||
|
||||
nodeId, err := strconv.Atoi(nodeIdStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert node ID to int: %w", err)
|
||||
}
|
||||
|
||||
masterSiteUrl := cluster.MasterSiteUrlFromContext(ctx)
|
||||
if masterSiteUrl == "" {
|
||||
return nil, fmt.Errorf("failed to get master site URL from context")
|
||||
}
|
||||
|
||||
masterSiteVersion, ok := ctx.Value(cluster.MasterSiteVersionCtx{}).(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to get master site version from context")
|
||||
}
|
||||
|
||||
masterSiteId, ok := ctx.Value(cluster.MasterSiteIDCtx{}).(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert master site ID to int: %w", err)
|
||||
}
|
||||
|
||||
props := &types.SlaveTaskProps{
|
||||
NodeID: nodeId,
|
||||
MasterSiteID: masterSiteId,
|
||||
MasterSiteURl: masterSiteUrl,
|
||||
MasterSiteVersion: masterSiteVersion,
|
||||
}
|
||||
|
||||
return props, nil
|
||||
}
|
||||
|
||||
type (
|
||||
FolderCleanupParamCtx struct{}
|
||||
)
|
||||
|
||||
func Cleanup(args *cluster.FolderCleanup, c *gin.Context) error {
|
||||
l := logging.FromContext(c)
|
||||
ae := serializer.NewAggregateError()
|
||||
for _, p := range args.Path {
|
||||
l.Info("Cleaning up folder %q", p)
|
||||
if err := os.RemoveAll(p); err != nil {
|
||||
l.Warning("Failed to clean up folder %q: %s", p, err)
|
||||
ae.Add(p, err)
|
||||
}
|
||||
}
|
||||
|
||||
return ae.Aggregate()
|
||||
}
|
||||
|
||||
type (
|
||||
CreateSlaveDownloadTaskParamCtx struct{}
|
||||
GetSlaveDownloadTaskParamCtx struct{}
|
||||
CancelSlaveDownloadTaskParamCtx struct{}
|
||||
SelectSlaveDownloadFilesParamCtx struct{}
|
||||
TestSlaveDownloadParamCtx struct{}
|
||||
)
|
||||
44
service/setting/response.go
Normal file
44
service/setting/response.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/samber/lo"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ListDavAccountResponse struct {
|
||||
Accounts []DavAccount `json:"accounts"`
|
||||
Pagination *inventory.PaginationResults `json:"pagination"`
|
||||
}
|
||||
|
||||
func BuildListDavAccountResponse(res *inventory.ListDavAccountResult, hasher hashid.Encoder) *ListDavAccountResponse {
|
||||
return &ListDavAccountResponse{
|
||||
Accounts: lo.Map(res.Accounts, func(item *ent.DavAccount, index int) DavAccount {
|
||||
return BuildDavAccount(item, hasher)
|
||||
}),
|
||||
Pagination: res.PaginationResults,
|
||||
}
|
||||
}
|
||||
|
||||
type DavAccount struct {
|
||||
ID string `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Name string `json:"name"`
|
||||
Uri string `json:"uri"`
|
||||
Password string `json:"password"`
|
||||
Options *boolset.BooleanSet `json:"options"`
|
||||
}
|
||||
|
||||
func BuildDavAccount(account *ent.DavAccount, hasher hashid.Encoder) DavAccount {
|
||||
return DavAccount{
|
||||
ID: hashid.EncodeDavAccountID(hasher, account.ID),
|
||||
CreatedAt: account.CreatedAt,
|
||||
Name: account.Name,
|
||||
Uri: account.URI,
|
||||
Password: account.Password,
|
||||
Options: account.Options,
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/constants"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// WebDAVListService WebDAV 列表服务
|
||||
type WebDAVListService struct {
|
||||
}
|
||||
|
||||
// WebDAVAccountService WebDAV 账号管理服务
|
||||
type WebDAVAccountService struct {
|
||||
ID uint `uri:"id" binding:"required,min=1"`
|
||||
@@ -22,11 +25,10 @@ type WebDAVAccountCreateService struct {
|
||||
Name string `json:"name" binding:"required,min=1,max=255"`
|
||||
}
|
||||
|
||||
// WebDAVAccountUpdateService WebDAV 修改只读性和是否使用代理服务
|
||||
type WebDAVAccountUpdateService struct {
|
||||
ID uint `json:"id" binding:"required,min=1"`
|
||||
Readonly *bool `json:"readonly" binding:"required_without=UseProxy"`
|
||||
UseProxy *bool `json:"use_proxy" binding:"required_without=Readonly"`
|
||||
// WebDAVAccountUpdateReadonlyService WebDAV 修改只读性服务
|
||||
type WebDAVAccountUpdateReadonlyService struct {
|
||||
ID uint `json:"id" binding:"required,min=1"`
|
||||
Readonly bool `json:"readonly"`
|
||||
}
|
||||
|
||||
// WebDAVMountCreateService WebDAV 挂载创建服务
|
||||
@@ -35,52 +37,163 @@ type WebDAVMountCreateService struct {
|
||||
Policy string `json:"policy" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
// Create 创建WebDAV账户
|
||||
func (service *WebDAVAccountCreateService) Create(c *gin.Context, user *model.User) serializer.Response {
|
||||
account := model.Webdav{
|
||||
Name: service.Name,
|
||||
Password: util.RandStringRunes(32),
|
||||
UserID: user.ID,
|
||||
Root: service.Path,
|
||||
}
|
||||
//// Unmount 取消目录挂载
|
||||
//func (service *WebDAVListService) Unmount(c *gin.Context, user *model.User) serializer.Response {
|
||||
// folderID, _ := c.Get("object_id")
|
||||
// folder, err := model.GetFoldersByIDs([]uint{folderID.(uint)}, user.ID)
|
||||
// if err != nil || len(folder) == 0 {
|
||||
// return serializer.ErrDeprecated(serializer.CodeParentNotExist, "", err)
|
||||
// }
|
||||
//
|
||||
// if err := folder[0].Mount(0); err != nil {
|
||||
// return serializer.DBErrDeprecated("Failed to update folder record", err)
|
||||
// }
|
||||
//
|
||||
// return serializer.Response{}
|
||||
//}
|
||||
|
||||
if _, err := account.Create(); err != nil {
|
||||
return serializer.Err(serializer.CodeDBError, "创建失败", err)
|
||||
type (
|
||||
ListDavAccountsService struct {
|
||||
PageSize int `form:"page_size" binding:"required,min=10,max=100"`
|
||||
NextPageToken string `form:"next_page_token"`
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Data: map[string]interface{}{
|
||||
"id": account.ID,
|
||||
"password": account.Password,
|
||||
"created_at": account.CreatedAt,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Delete 删除WebDAV账户
|
||||
func (service *WebDAVAccountService) Delete(c *gin.Context, user *model.User) serializer.Response {
|
||||
model.DeleteWebDAVAccountByID(service.ID, user.ID)
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Update 修改WebDAV账户只读性和是否使用代理服务
|
||||
func (service *WebDAVAccountUpdateService) Update(c *gin.Context, user *model.User) serializer.Response {
|
||||
var updates = make(map[string]interface{})
|
||||
if service.Readonly != nil {
|
||||
updates["readonly"] = *service.Readonly
|
||||
}
|
||||
if service.UseProxy != nil {
|
||||
updates["use_proxy"] = *service.UseProxy
|
||||
}
|
||||
model.UpdateWebDAVAccountByID(service.ID, user.ID, updates)
|
||||
return serializer.Response{Data: updates}
|
||||
}
|
||||
ListDavAccountParamCtx struct{}
|
||||
)
|
||||
|
||||
// Accounts 列出WebDAV账号
|
||||
func (service *WebDAVListService) Accounts(c *gin.Context, user *model.User) serializer.Response {
|
||||
accounts := model.ListWebDAVAccounts(user.ID)
|
||||
func (service *ListDavAccountsService) List(c *gin.Context) (*ListDavAccountResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
hasher := dep.HashIDEncoder()
|
||||
davAccountClient := dep.DavAccountClient()
|
||||
|
||||
return serializer.Response{Data: map[string]interface{}{
|
||||
"accounts": accounts,
|
||||
}}
|
||||
args := &inventory.ListDavAccountArgs{
|
||||
UserID: user.ID,
|
||||
PaginationArgs: &inventory.PaginationArgs{
|
||||
UseCursorPagination: true,
|
||||
PageSize: service.PageSize,
|
||||
PageToken: service.NextPageToken,
|
||||
},
|
||||
}
|
||||
|
||||
res, err := davAccountClient.List(c, args)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list dav accounts", err)
|
||||
}
|
||||
|
||||
return BuildListDavAccountResponse(res, hasher), nil
|
||||
}
|
||||
|
||||
type (
|
||||
CreateDavAccountService struct {
|
||||
Uri string `json:"uri" binding:"required"`
|
||||
Name string `json:"name" binding:"required,min=1,max=255"`
|
||||
Readonly bool `json:"readonly"`
|
||||
Proxy bool `json:"proxy"`
|
||||
}
|
||||
CreateDavAccountParamCtx struct{}
|
||||
)
|
||||
|
||||
// Create 创建WebDAV账号
|
||||
func (service *CreateDavAccountService) Create(c *gin.Context) (*DavAccount, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
|
||||
bs, err := service.validateAndGetBs(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
davAccountClient := dep.DavAccountClient()
|
||||
account, err := davAccountClient.Create(c, &inventory.CreateDavAccountParams{
|
||||
UserID: user.ID,
|
||||
Name: service.Name,
|
||||
URI: service.Uri,
|
||||
Password: util.RandString(32, util.RandomLowerCases),
|
||||
Options: bs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to create dav account", err)
|
||||
}
|
||||
|
||||
accountRes := BuildDavAccount(account, dep.HashIDEncoder())
|
||||
return &accountRes, nil
|
||||
}
|
||||
|
||||
// Update updates an existing account
|
||||
func (service *CreateDavAccountService) Update(c *gin.Context) (*DavAccount, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
accountId := hashid.FromContext(c)
|
||||
|
||||
// Find existing account
|
||||
davAccountClient := dep.DavAccountClient()
|
||||
account, err := davAccountClient.GetByIDAndUserID(c, accountId, user.ID)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "Account not exist", err)
|
||||
}
|
||||
|
||||
bs, err := service.validateAndGetBs(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Update account
|
||||
account, err = davAccountClient.Update(c, accountId, &inventory.CreateDavAccountParams{
|
||||
Name: service.Name,
|
||||
URI: service.Uri,
|
||||
Options: bs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to update dav account", err)
|
||||
}
|
||||
|
||||
accountRes := BuildDavAccount(account, dep.HashIDEncoder())
|
||||
return &accountRes, nil
|
||||
}
|
||||
|
||||
func (service *CreateDavAccountService) validateAndGetBs(user *ent.User) (*boolset.BooleanSet, error) {
|
||||
if !user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionWebDAV)) {
|
||||
return nil, serializer.NewError(serializer.CodeGroupNotAllowed, "WebDAV is not enabled for this user group", nil)
|
||||
}
|
||||
|
||||
uri, err := fs.NewUriFromString(service.Uri)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid URI", err)
|
||||
}
|
||||
|
||||
// Only "my" and "share" fs is allowed in WebDAV
|
||||
if uriFs := uri.FileSystem(); uri.SearchParameters() != nil ||
|
||||
(uriFs != constants.FileSystemMy && uriFs != constants.FileSystemShare) {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid URI", nil)
|
||||
}
|
||||
|
||||
bs := boolset.BooleanSet{}
|
||||
if service.Readonly {
|
||||
boolset.Set(types.DavAccountReadOnly, true, &bs)
|
||||
}
|
||||
|
||||
if service.Proxy && user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionWebDAVProxy)) {
|
||||
boolset.Set(types.DavAccountProxy, true, &bs)
|
||||
}
|
||||
return &bs, nil
|
||||
}
|
||||
|
||||
func DeleteDavAccount(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
accountId := hashid.FromContext(c)
|
||||
|
||||
// Find existing account
|
||||
davAccountClient := dep.DavAccountClient()
|
||||
_, err := davAccountClient.GetByIDAndUserID(c, accountId, user.ID)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeNotFound, "Account not exist", err)
|
||||
}
|
||||
|
||||
if err := davAccountClient.Delete(c, accountId); err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to delete dav account", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,150 +1,90 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/service/explorer"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ShareCreateService 创建新分享服务
|
||||
type ShareCreateService struct {
|
||||
SourceID string `json:"id" binding:"required"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
Password string `json:"password" binding:"max=255"`
|
||||
RemainDownloads int `json:"downloads"`
|
||||
Expire int `json:"expire"`
|
||||
Preview bool `json:"preview"`
|
||||
type (
|
||||
// ShareCreateService 创建新分享服务
|
||||
ShareCreateService struct {
|
||||
Uri string `json:"uri" binding:"required"`
|
||||
IsPrivate bool `json:"is_private"`
|
||||
RemainDownloads int `json:"downloads"`
|
||||
Expire int `json:"expire"`
|
||||
}
|
||||
ShareCreateParamCtx struct{}
|
||||
)
|
||||
|
||||
// Upsert 创建或更新分享
|
||||
func (service *ShareCreateService) Upsert(c *gin.Context, existed int) (string, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
m := manager.NewFileManager(dep, user)
|
||||
defer m.Recycle()
|
||||
|
||||
// Check group permission for creating share link
|
||||
if !user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionShare)) {
|
||||
return "", serializer.NewError(serializer.CodeGroupNotAllowed, "Group permission denied", nil)
|
||||
}
|
||||
|
||||
uri, err := fs.NewUriFromString(service.Uri)
|
||||
if err != nil {
|
||||
return "", serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
|
||||
}
|
||||
|
||||
var expires *time.Time
|
||||
if service.Expire > 0 {
|
||||
expires = new(time.Time)
|
||||
*expires = time.Now().Add(time.Duration(service.Expire) * time.Second)
|
||||
}
|
||||
|
||||
share, err := m.CreateOrUpdateShare(c, uri, &manager.CreateShareArgs{
|
||||
IsPrivate: service.IsPrivate,
|
||||
RemainDownloads: service.RemainDownloads,
|
||||
Expire: expires,
|
||||
ExistedShareID: existed,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
base := dep.SettingProvider().SiteURL(c)
|
||||
return explorer.BuildShareLink(share, dep.HashIDEncoder(), base), nil
|
||||
}
|
||||
|
||||
// ShareUpdateService 分享更新服务
|
||||
type ShareUpdateService struct {
|
||||
Prop string `json:"prop" binding:"required,eq=password|eq=preview_enabled"`
|
||||
Value string `json:"value" binding:"max=255"`
|
||||
}
|
||||
func DeleteShare(c *gin.Context, shareId int) error {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
shareClient := dep.ShareClient()
|
||||
|
||||
// Delete 删除分享
|
||||
func (service *Service) Delete(c *gin.Context, user *model.User) serializer.Response {
|
||||
share := model.GetShareByHashID(c.Param("id"))
|
||||
if share == nil || share.Creator().ID != user.ID {
|
||||
return serializer.Err(serializer.CodeShareLinkNotFound, "", nil)
|
||||
}
|
||||
|
||||
if err := share.Delete(); err != nil {
|
||||
return serializer.DBErr("Failed to delete share record", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Update 更新分享属性
|
||||
func (service *ShareUpdateService) Update(c *gin.Context) serializer.Response {
|
||||
shareCtx, _ := c.Get("share")
|
||||
share := shareCtx.(*model.Share)
|
||||
|
||||
switch service.Prop {
|
||||
case "password":
|
||||
err := share.Update(map[string]interface{}{"password": service.Value})
|
||||
if err != nil {
|
||||
return serializer.DBErr("Failed to update share record", err)
|
||||
}
|
||||
case "preview_enabled":
|
||||
value := service.Value == "true"
|
||||
err := share.Update(map[string]interface{}{"preview_enabled": value})
|
||||
if err != nil {
|
||||
return serializer.DBErr("Failed to update share record", err)
|
||||
}
|
||||
return serializer.Response{
|
||||
Data: value,
|
||||
}
|
||||
}
|
||||
return serializer.Response{
|
||||
Data: service.Value,
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建新分享
|
||||
func (service *ShareCreateService) Create(c *gin.Context) serializer.Response {
|
||||
userCtx, _ := c.Get("user")
|
||||
user := userCtx.(*model.User)
|
||||
|
||||
// 是否拥有权限
|
||||
if !user.Group.ShareEnabled {
|
||||
return serializer.Err(serializer.CodeGroupNotAllowed, "", nil)
|
||||
}
|
||||
|
||||
// 源对象真实ID
|
||||
ctx := context.WithValue(c, inventory.LoadShareFile{}, true)
|
||||
var (
|
||||
sourceID uint
|
||||
sourceName string
|
||||
err error
|
||||
share *ent.Share
|
||||
err error
|
||||
)
|
||||
if service.IsDir {
|
||||
sourceID, err = hashid.DecodeHashID(service.SourceID, hashid.FolderID)
|
||||
if user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionIsAdmin)) {
|
||||
share, err = shareClient.GetByID(ctx, shareId)
|
||||
} else {
|
||||
sourceID, err = hashid.DecodeHashID(service.SourceID, hashid.FileID)
|
||||
share, err = shareClient.GetByIDUser(ctx, shareId, user.ID)
|
||||
}
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotFound, "", nil)
|
||||
return serializer.NewError(serializer.CodeNotFound, "share not found", err)
|
||||
}
|
||||
|
||||
// 对象是否存在
|
||||
exist := true
|
||||
if service.IsDir {
|
||||
folder, err := model.GetFoldersByIDs([]uint{sourceID}, user.ID)
|
||||
if err != nil || len(folder) == 0 {
|
||||
exist = false
|
||||
} else {
|
||||
sourceName = folder[0].Name
|
||||
}
|
||||
} else {
|
||||
file, err := model.GetFilesByIDs([]uint{sourceID}, user.ID)
|
||||
if err != nil || len(file) == 0 {
|
||||
exist = false
|
||||
} else {
|
||||
sourceName = file[0].Name
|
||||
}
|
||||
}
|
||||
if !exist {
|
||||
return serializer.Err(serializer.CodeNotFound, "", nil)
|
||||
}
|
||||
|
||||
newShare := model.Share{
|
||||
Password: service.Password,
|
||||
IsDir: service.IsDir,
|
||||
UserID: user.ID,
|
||||
SourceID: sourceID,
|
||||
RemainDownloads: -1,
|
||||
PreviewEnabled: service.Preview,
|
||||
SourceName: sourceName,
|
||||
}
|
||||
|
||||
// 如果开启了自动过期
|
||||
if service.RemainDownloads > 0 {
|
||||
expires := time.Now().Add(time.Duration(service.Expire) * time.Second)
|
||||
newShare.RemainDownloads = service.RemainDownloads
|
||||
newShare.Expires = &expires
|
||||
}
|
||||
|
||||
// 创建分享
|
||||
id, err := newShare.Create()
|
||||
if err != nil {
|
||||
return serializer.DBErr("Failed to create share link record", err)
|
||||
}
|
||||
|
||||
// 获取分享的唯一id
|
||||
uid := hashid.HashID(id, hashid.ShareID)
|
||||
// 最终得到分享链接
|
||||
siteURL := model.GetSiteURL()
|
||||
sharePath, _ := url.Parse("/s/" + uid)
|
||||
shareURL := siteURL.ResolveReference(sharePath)
|
||||
|
||||
return serializer.Response{
|
||||
Code: 0,
|
||||
Data: shareURL.String(),
|
||||
if err := shareClient.Delete(c, share.ID); err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to delete share", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
28
service/share/response.go
Normal file
28
service/share/response.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/service/explorer"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type ListShareResponse struct {
|
||||
Shares []explorer.Share `json:"shares"`
|
||||
Pagination *inventory.PaginationResults `json:"pagination"`
|
||||
}
|
||||
|
||||
func BuildListShareResponse(res *inventory.ListShareResult, hasher hashid.Encoder, base *url.URL, requester *ent.User, unlocked bool) *ListShareResponse {
|
||||
var infos []explorer.Share
|
||||
for _, share := range res.Shares {
|
||||
infos = append(infos, *explorer.BuildShare(share, base, hasher, requester, share.Edges.User, share.Edges.File.Name,
|
||||
types.FileType(share.Edges.File.Type), unlocked))
|
||||
}
|
||||
|
||||
return &ListShareResponse{
|
||||
Shares: infos,
|
||||
Pagination: res.PaginationResults,
|
||||
}
|
||||
}
|
||||
@@ -2,414 +2,159 @@ package share
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/cloudreve/Cloudreve/v3/service/explorer"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/service/explorer"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ShareUserGetService 获取用户的分享服务
|
||||
type ShareUserGetService struct {
|
||||
Type string `form:"type" binding:"required,eq=hot|eq=default"`
|
||||
Page uint `form:"page" binding:"required,min=1"`
|
||||
type (
|
||||
ShortLinkRedirectService struct {
|
||||
ID string `uri:"id" binding:"required"`
|
||||
Password string `uri:"password"`
|
||||
}
|
||||
ShortLinkRedirectParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *ShortLinkRedirectService) RedirectTo(c *gin.Context) string {
|
||||
return routes.MasterShareLongUrl(s.ID, s.Password).String()
|
||||
}
|
||||
|
||||
// ShareGetService 获取分享服务
|
||||
type ShareGetService struct {
|
||||
Password string `form:"password" binding:"max=255"`
|
||||
}
|
||||
type (
|
||||
ShareInfoService struct {
|
||||
Password string `form:"password"`
|
||||
CountViews bool `form:"count_views"`
|
||||
OwnerExtended bool `form:"owner_extended"`
|
||||
}
|
||||
ShareInfoParamCtx struct{}
|
||||
)
|
||||
|
||||
// Service 对分享进行操作的服务,
|
||||
// path 为可选文件完整路径,在目录分享下有效
|
||||
type Service struct {
|
||||
Path string `form:"path" uri:"path" binding:"max=65535"`
|
||||
}
|
||||
func (s *ShareInfoService) Get(c *gin.Context) (*explorer.Share, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
u := inventory.UserFromContext(c)
|
||||
shareClient := dep.ShareClient()
|
||||
|
||||
// ArchiveService 分享归档下载服务
|
||||
type ArchiveService struct {
|
||||
Path string `json:"path" binding:"required,max=65535"`
|
||||
Items []string `json:"items"`
|
||||
Dirs []string `json:"dirs"`
|
||||
}
|
||||
|
||||
// ShareListService 列出分享
|
||||
type ShareListService struct {
|
||||
Page uint `form:"page" binding:"required,min=1"`
|
||||
OrderBy string `form:"order_by" binding:"required,eq=created_at|eq=downloads|eq=views"`
|
||||
Order string `form:"order" binding:"required,eq=DESC|eq=ASC"`
|
||||
Keywords string `form:"keywords"`
|
||||
}
|
||||
|
||||
// Get 获取给定用户的分享
|
||||
func (service *ShareUserGetService) Get(c *gin.Context) serializer.Response {
|
||||
// 取得用户
|
||||
userID, _ := c.Get("object_id")
|
||||
user, err := model.GetActiveUserByID(userID.(uint))
|
||||
if err != nil || user.OptionsSerialized.ProfileOff {
|
||||
return serializer.Err(serializer.CodeNotFound, "", err)
|
||||
ctx := context.WithValue(c, inventory.LoadShareUser{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadShareFile{}, true)
|
||||
share, err := shareClient.GetByID(ctx, hashid.FromContext(c))
|
||||
if err != nil {
|
||||
if ent.IsNotFound(err) {
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "Share not found", nil)
|
||||
}
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get share", err)
|
||||
}
|
||||
|
||||
// 列出分享
|
||||
hotNum := model.GetIntSetting("hot_share_num", 10)
|
||||
if service.Type == "default" {
|
||||
hotNum = 10
|
||||
}
|
||||
orderBy := "created_at desc"
|
||||
if service.Type == "hot" {
|
||||
orderBy = "views desc"
|
||||
}
|
||||
shares, total := model.ListShares(user.ID, int(service.Page), hotNum, orderBy, true)
|
||||
// 列出分享对应的文件
|
||||
for i := 0; i < len(shares); i++ {
|
||||
shares[i].Source()
|
||||
if err := inventory.IsValidShare(share); err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "Share link expired", err)
|
||||
}
|
||||
|
||||
res := serializer.BuildShareList(shares, total)
|
||||
res.Data.(map[string]interface{})["user"] = struct {
|
||||
ID string `json:"id"`
|
||||
Nick string `json:"nick"`
|
||||
Group string `json:"group"`
|
||||
Date string `json:"date"`
|
||||
}{
|
||||
hashid.HashID(user.ID, hashid.UserID),
|
||||
user.Nick,
|
||||
user.Group.Name,
|
||||
user.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
if s.CountViews {
|
||||
_ = shareClient.Viewed(c, share)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// Search 搜索公共分享
|
||||
func (service *ShareListService) Search(c *gin.Context) serializer.Response {
|
||||
// 列出分享
|
||||
shares, total := model.SearchShares(int(service.Page), 18, service.OrderBy+" "+
|
||||
service.Order, service.Keywords)
|
||||
// 列出分享对应的文件
|
||||
for i := 0; i < len(shares); i++ {
|
||||
shares[i].Source()
|
||||
}
|
||||
|
||||
return serializer.BuildShareList(shares, total)
|
||||
}
|
||||
|
||||
// List 列出用户分享
|
||||
func (service *ShareListService) List(c *gin.Context, user *model.User) serializer.Response {
|
||||
// 列出分享
|
||||
shares, total := model.ListShares(user.ID, int(service.Page), 18, service.OrderBy+" "+
|
||||
service.Order, false)
|
||||
// 列出分享对应的文件
|
||||
for i := 0; i < len(shares); i++ {
|
||||
shares[i].Source()
|
||||
}
|
||||
|
||||
return serializer.BuildShareList(shares, total)
|
||||
}
|
||||
|
||||
// Get 获取分享内容
|
||||
func (service *ShareGetService) Get(c *gin.Context) serializer.Response {
|
||||
shareCtx, _ := c.Get("share")
|
||||
share := shareCtx.(*model.Share)
|
||||
|
||||
// 是否已解锁
|
||||
unlocked := true
|
||||
if share.Password != "" {
|
||||
sessionKey := fmt.Sprintf("share_unlock_%d", share.ID)
|
||||
unlocked = util.GetSession(c, sessionKey) != nil
|
||||
if !unlocked && service.Password != "" {
|
||||
// 如果未解锁,且指定了密码,则尝试解锁
|
||||
if service.Password == share.Password {
|
||||
unlocked = true
|
||||
util.SetSession(c, map[string]interface{}{sessionKey: true})
|
||||
}
|
||||
}
|
||||
// Share requires password
|
||||
if share.Password != "" && s.Password != share.Password && share.Edges.User.ID != u.ID {
|
||||
unlocked = false
|
||||
}
|
||||
|
||||
if unlocked {
|
||||
share.Viewed()
|
||||
}
|
||||
base := dep.SettingProvider().SiteURL(c)
|
||||
res := explorer.BuildShare(share, base, dep.HashIDEncoder(), u, share.Edges.User, share.Edges.File.Name,
|
||||
types.FileType(share.Edges.File.Type), unlocked)
|
||||
|
||||
return serializer.Response{
|
||||
Code: 0,
|
||||
Data: serializer.BuildShareResponse(share, unlocked),
|
||||
}
|
||||
}
|
||||
if s.OwnerExtended && share.Edges.User.ID == u.ID {
|
||||
// Add more information about the shared file
|
||||
m := manager.NewFileManager(dep, u)
|
||||
defer m.Recycle()
|
||||
|
||||
// CreateDownloadSession 创建下载会话
|
||||
func (service *Service) CreateDownloadSession(c *gin.Context) serializer.Response {
|
||||
shareCtx, _ := c.Get("share")
|
||||
share := shareCtx.(*model.Share)
|
||||
userCtx, _ := c.Get("user")
|
||||
user := userCtx.(*model.User)
|
||||
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystem(user)
|
||||
if err != nil {
|
||||
return serializer.DBErr("Failed to update share record", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 重设文件系统处理目标为源文件
|
||||
err = fs.SetTargetByInterface(share.Source())
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeFileNotFound, "", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 重设根目录
|
||||
if share.IsDir {
|
||||
fs.Root = &fs.DirTarget[0]
|
||||
|
||||
// 找到目标文件
|
||||
err = fs.ResetFileIfNotExist(ctx, service.Path)
|
||||
shareUri, err := fs.NewUriFromString(fs.NewShareUri(res.ID, s.Password))
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// 取得下载地址
|
||||
downloadURL, err := fs.GetDownloadURL(ctx, 0, "download_timeout")
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Code: 0,
|
||||
Data: downloadURL,
|
||||
}
|
||||
}
|
||||
|
||||
// PreviewContent 预览文件,需要登录会话, isText - 是否为文本文件,文本文件会
|
||||
// 强制经由服务端中转
|
||||
func (service *Service) PreviewContent(ctx context.Context, c *gin.Context, isText bool) serializer.Response {
|
||||
shareCtx, _ := c.Get("share")
|
||||
share := shareCtx.(*model.Share)
|
||||
|
||||
// 用于调下层service
|
||||
if share.IsDir {
|
||||
ctx = context.WithValue(ctx, fsctx.FolderModelCtx, share.Source())
|
||||
ctx = context.WithValue(ctx, fsctx.PathCtx, service.Path)
|
||||
} else {
|
||||
ctx = context.WithValue(ctx, fsctx.FileModelCtx, share.Source())
|
||||
}
|
||||
subService := explorer.FileIDService{}
|
||||
|
||||
return subService.PreviewContent(ctx, c, isText)
|
||||
}
|
||||
|
||||
// CreateDocPreviewSession 创建Office预览会话,返回预览地址
|
||||
func (service *Service) CreateDocPreviewSession(c *gin.Context) serializer.Response {
|
||||
shareCtx, _ := c.Get("share")
|
||||
share := shareCtx.(*model.Share)
|
||||
|
||||
// 用于调下层service
|
||||
ctx := context.Background()
|
||||
if share.IsDir {
|
||||
ctx = context.WithValue(ctx, fsctx.FolderModelCtx, share.Source())
|
||||
ctx = context.WithValue(ctx, fsctx.PathCtx, service.Path)
|
||||
} else {
|
||||
ctx = context.WithValue(ctx, fsctx.FileModelCtx, share.Source())
|
||||
}
|
||||
subService := explorer.FileIDService{}
|
||||
|
||||
return subService.CreateDocPreviewSession(ctx, c, false)
|
||||
}
|
||||
|
||||
// List 列出分享的目录下的对象
|
||||
func (service *Service) List(c *gin.Context) serializer.Response {
|
||||
shareCtx, _ := c.Get("share")
|
||||
share := shareCtx.(*model.Share)
|
||||
|
||||
if !share.IsDir {
|
||||
return serializer.ParamErr("This is not a shared folder", nil)
|
||||
}
|
||||
|
||||
if !path.IsAbs(service.Path) {
|
||||
return serializer.ParamErr("Invalid path", nil)
|
||||
}
|
||||
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystem(share.Creator())
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 上下文
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// 重设根目录
|
||||
fs.Root = share.Source().(*model.Folder)
|
||||
fs.Root.Name = "/"
|
||||
|
||||
// 分享Key上下文
|
||||
ctx = context.WithValue(ctx, fsctx.ShareKeyCtx, hashid.HashID(share.ID, hashid.ShareID))
|
||||
|
||||
// 获取子项目
|
||||
objects, err := fs.List(ctx, service.Path, nil)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Code: 0,
|
||||
Data: serializer.BuildObjectList(0, objects, nil),
|
||||
}
|
||||
}
|
||||
|
||||
// Thumb 获取被分享文件的缩略图
|
||||
func (service *Service) Thumb(c *gin.Context) serializer.Response {
|
||||
shareCtx, _ := c.Get("share")
|
||||
share := shareCtx.(*model.Share)
|
||||
|
||||
if !share.IsDir {
|
||||
return serializer.ParamErr("This share has no thumb", nil)
|
||||
}
|
||||
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystem(share.Creator())
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 重设根目录
|
||||
fs.Root = share.Source().(*model.Folder)
|
||||
|
||||
// 找到缩略图的父目录
|
||||
exist, parent := fs.IsPathExist(service.Path)
|
||||
if !exist {
|
||||
return serializer.Err(serializer.CodeParentNotExist, "", nil)
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), fsctx.LimitParentCtx, parent)
|
||||
|
||||
// 获取文件ID
|
||||
fileID, err := hashid.DecodeHashID(c.Param("file"), hashid.FileID)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotFound, "", err)
|
||||
}
|
||||
|
||||
// 获取缩略图
|
||||
resp, err := fs.GetThumb(ctx, uint(fileID))
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, "Failed to get thumb", err)
|
||||
}
|
||||
|
||||
if resp.Redirect {
|
||||
c.Header("Cache-Control", fmt.Sprintf("max-age=%d", resp.MaxAge))
|
||||
c.Redirect(http.StatusMovedPermanently, resp.URL)
|
||||
return serializer.Response{Code: -1}
|
||||
}
|
||||
|
||||
defer resp.Content.Close()
|
||||
http.ServeContent(c.Writer, c.Request, "thumb.png", fs.FileTarget[0].UpdatedAt, resp.Content)
|
||||
|
||||
return serializer.Response{Code: -1}
|
||||
|
||||
}
|
||||
|
||||
// Archive 创建批量下载归档
|
||||
func (service *ArchiveService) Archive(c *gin.Context) serializer.Response {
|
||||
shareCtx, _ := c.Get("share")
|
||||
share := shareCtx.(*model.Share)
|
||||
userCtx, _ := c.Get("user")
|
||||
user := userCtx.(*model.User)
|
||||
|
||||
// 是否有权限
|
||||
if !user.Group.OptionsSerialized.ArchiveDownload {
|
||||
return serializer.Err(serializer.CodeGroupNotAllowed, "", nil)
|
||||
}
|
||||
|
||||
if !share.IsDir {
|
||||
return serializer.ParamErr("This share cannot be batch downloaded", nil)
|
||||
}
|
||||
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystem(user)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 重设根目录
|
||||
fs.Root = share.Source().(*model.Folder)
|
||||
|
||||
// 找到要打包文件的父目录
|
||||
exist, parent := fs.IsPathExist(service.Path)
|
||||
if !exist {
|
||||
return serializer.Err(serializer.CodeParentNotExist, "", nil)
|
||||
}
|
||||
|
||||
// 限制操作范围为父目录下
|
||||
ctx := context.WithValue(context.Background(), fsctx.LimitParentCtx, parent)
|
||||
|
||||
// 用于调下层service
|
||||
tempUser := share.Creator()
|
||||
tempUser.Group.OptionsSerialized.ArchiveDownload = true
|
||||
c.Set("user", tempUser)
|
||||
|
||||
subService := explorer.ItemIDService{
|
||||
Dirs: service.Dirs,
|
||||
Items: service.Items,
|
||||
}
|
||||
|
||||
return subService.Archive(ctx, c)
|
||||
}
|
||||
|
||||
// SearchService 对分享的目录进行搜索
|
||||
type SearchService struct {
|
||||
explorer.ItemSearchService
|
||||
}
|
||||
|
||||
// Search 执行搜索
|
||||
func (service *SearchService) Search(c *gin.Context) serializer.Response {
|
||||
shareCtx, _ := c.Get("share")
|
||||
share := shareCtx.(*model.Share)
|
||||
|
||||
if !share.IsDir {
|
||||
return serializer.ParamErr("此分享无法列目录", nil)
|
||||
}
|
||||
|
||||
if service.Path != "" && !path.IsAbs(service.Path) {
|
||||
return serializer.ParamErr("路径无效", nil)
|
||||
}
|
||||
|
||||
// 创建文件系统
|
||||
fs, err := filesystem.NewFileSystem(share.Creator())
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCreateFSError, "", err)
|
||||
}
|
||||
defer fs.Recycle()
|
||||
|
||||
// 上下文
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// 重设根目录
|
||||
fs.Root = share.Source().(*model.Folder)
|
||||
fs.Root.Name = "/"
|
||||
if service.Path != "" {
|
||||
ok, parent := fs.IsPathExist(service.Path)
|
||||
if !ok {
|
||||
return serializer.Err(serializer.CodeParentNotExist, "Cannot find parent folder", nil)
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Invalid share url", err)
|
||||
}
|
||||
|
||||
fs.Root = parent
|
||||
root, err := m.Get(c, shareUri)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "File not found", err)
|
||||
}
|
||||
|
||||
res.SourceUri = root.Uri(true).String()
|
||||
}
|
||||
|
||||
// 分享Key上下文
|
||||
ctx = context.WithValue(ctx, fsctx.ShareKeyCtx, hashid.HashID(share.ID, hashid.ShareID))
|
||||
return res, nil
|
||||
|
||||
return service.SearchKeywords(c, fs, "%"+service.Keywords+"%")
|
||||
}
|
||||
|
||||
type (
|
||||
ListShareService struct {
|
||||
PageSize int `form:"page_size" binding:"required,min=10,max=100"`
|
||||
OrderBy string `uri:"order_by" form:"order_by" json:"order_by"`
|
||||
OrderDirection string `uri:"order_direction" form:"order_direction" json:"order_direction"`
|
||||
NextPageToken string `form:"next_page_token"`
|
||||
}
|
||||
ListShareParamCtx struct{}
|
||||
)
|
||||
|
||||
func (s *ListShareService) List(c *gin.Context) (*ListShareResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
hasher := dep.HashIDEncoder()
|
||||
shareClient := dep.ShareClient()
|
||||
|
||||
args := &inventory.ListShareArgs{
|
||||
PaginationArgs: &inventory.PaginationArgs{
|
||||
UseCursorPagination: true,
|
||||
PageToken: s.NextPageToken,
|
||||
PageSize: s.PageSize,
|
||||
Order: inventory.OrderDirection(s.OrderDirection),
|
||||
OrderBy: s.OrderBy,
|
||||
},
|
||||
UserID: user.ID,
|
||||
}
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadShareUser{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadShareFile{}, true)
|
||||
res, err := shareClient.List(ctx, args)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list shares", err)
|
||||
}
|
||||
|
||||
base := dep.SettingProvider().SiteURL(ctx)
|
||||
return BuildListShareResponse(res, hasher, base, user, true), nil
|
||||
}
|
||||
|
||||
func (s *ListShareService) ListInUserProfile(c *gin.Context, uid int) (*ListShareResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
hasher := dep.HashIDEncoder()
|
||||
shareClient := dep.ShareClient()
|
||||
|
||||
args := &inventory.ListShareArgs{
|
||||
PaginationArgs: &inventory.PaginationArgs{
|
||||
UseCursorPagination: true,
|
||||
PageToken: s.NextPageToken,
|
||||
PageSize: s.PageSize,
|
||||
Order: inventory.OrderDirection(s.OrderDirection),
|
||||
OrderBy: s.OrderBy,
|
||||
},
|
||||
UserID: uid,
|
||||
PublicOnly: true,
|
||||
}
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadShareUser{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadShareFile{}, true)
|
||||
res, err := shareClient.List(ctx, args)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list shares", err)
|
||||
}
|
||||
|
||||
base := dep.SettingProvider().SiteURL(ctx)
|
||||
return BuildListShareResponse(res, hasher, base, user, false), nil
|
||||
}
|
||||
|
||||
67
service/user/info.go
Normal file
67
service/user/info.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func GetUser(c *gin.Context) (*ent.User, error) {
|
||||
uid := hashid.FromContext(c)
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
ctx := context.WithValue(c, inventory.LoadUserGroup{}, true)
|
||||
return userClient.GetByID(ctx, uid)
|
||||
}
|
||||
|
||||
func GetUserCapacity(c *gin.Context) (*fs.Capacity, error) {
|
||||
user := inventory.UserFromContext(c)
|
||||
dep := dependency.FromContext(c)
|
||||
m := manager.NewFileManager(dep, user)
|
||||
defer m.Recycle()
|
||||
|
||||
return m.Capacity(c)
|
||||
}
|
||||
|
||||
type (
|
||||
SearchUserService struct {
|
||||
Keyword string `form:"keyword" binding:"required,min=2"`
|
||||
}
|
||||
SearchUserParamCtx struct{}
|
||||
)
|
||||
|
||||
const resultLimit = 10
|
||||
|
||||
func (s *SearchUserService) Search(c *gin.Context) ([]*ent.User, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
res, err := userClient.SearchActive(c, resultLimit, s.Keyword)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to search user", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ListAllGroups lists all groups.
|
||||
func ListAllGroups(c *gin.Context) ([]*ent.Group, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
groupClient := dep.GroupClient()
|
||||
res, err := groupClient.ListAll(c)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list all groups", err)
|
||||
}
|
||||
|
||||
res = lo.Filter(res, func(g *ent.Group, index int) bool {
|
||||
return g.ID != inventory.AnonymousGroupID
|
||||
})
|
||||
|
||||
return res, nil
|
||||
}
|
||||
@@ -1,203 +1,245 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/cache"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/email"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/user"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/email"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// LoginParameterCtx define key fore UserLoginService
|
||||
type LoginParameterCtx struct{}
|
||||
|
||||
// UserLoginService 管理用户登录的服务
|
||||
type UserLoginService struct {
|
||||
//TODO 细致调整验证规则
|
||||
UserName string `form:"userName" json:"userName" binding:"required,email"`
|
||||
Password string `form:"Password" json:"Password" binding:"required,min=4,max=64"`
|
||||
UserName string `form:"email" json:"email" binding:"required,email"`
|
||||
Password string `form:"password" json:"password" binding:"required,min=4,max=64"`
|
||||
}
|
||||
|
||||
// UserResetEmailService 发送密码重设邮件服务
|
||||
type UserResetEmailService struct {
|
||||
UserName string `form:"userName" json:"userName" binding:"required,email"`
|
||||
}
|
||||
|
||||
// UserResetService 密码重设服务
|
||||
type UserResetService struct {
|
||||
Password string `form:"Password" json:"Password" binding:"required,min=4,max=64"`
|
||||
ID string `json:"id" binding:"required"`
|
||||
Secret string `json:"secret" binding:"required"`
|
||||
}
|
||||
type (
|
||||
// UserResetService 密码重设服务
|
||||
UserResetService struct {
|
||||
Password string `form:"password" json:"password" binding:"required,min=6,max=64"`
|
||||
Secret string `json:"secret" binding:"required"`
|
||||
}
|
||||
UserResetParameterCtx struct{}
|
||||
)
|
||||
|
||||
// Reset 重设密码
|
||||
func (service *UserResetService) Reset(c *gin.Context) serializer.Response {
|
||||
// 取得原始用户ID
|
||||
uid, err := hashid.DecodeHashID(service.ID, hashid.UserID)
|
||||
func (service *UserResetService) Reset(c *gin.Context) (*User, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
kv := dep.KV()
|
||||
uid := hashid.FromContext(c)
|
||||
|
||||
resetSession, ok := kv.Get(fmt.Sprintf("user_reset_%d", uid))
|
||||
if !ok || resetSession.(string) != service.Secret {
|
||||
return nil, serializer.NewError(serializer.CodeTempLinkExpired, "Link is expired", nil)
|
||||
}
|
||||
|
||||
if err := kv.Delete(fmt.Sprintf("user_reset_%d", uid)); err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to delete reset session", err)
|
||||
}
|
||||
|
||||
u, err := userClient.GetActiveByID(c, uid)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInvalidTempLink, "Invalid link", err)
|
||||
return nil, serializer.NewError(serializer.CodeUserNotFound, "User not found", err)
|
||||
}
|
||||
|
||||
// 检查重设会话
|
||||
resetSession, exist := cache.Get(fmt.Sprintf("user_reset_%d", uid))
|
||||
if !exist || resetSession.(string) != service.Secret {
|
||||
return serializer.Err(serializer.CodeTempLinkExpired, "Link is expired", err)
|
||||
}
|
||||
|
||||
// 重设用户密码
|
||||
user, err := model.GetActiveUserByID(uid)
|
||||
u, err = userClient.UpdatePassword(c, u, service.Password)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeUserNotFound, "User not found", nil)
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to update password", err)
|
||||
}
|
||||
|
||||
user.SetPassword(service.Password)
|
||||
if err := user.Update(map[string]interface{}{"password": user.Password}); err != nil {
|
||||
return serializer.DBErr("Failed to reset password", err)
|
||||
}
|
||||
|
||||
cache.Deletes([]string{fmt.Sprintf("%d", uid)}, "user_reset_")
|
||||
return serializer.Response{}
|
||||
userRes := BuildUser(u, dep.HashIDEncoder())
|
||||
return &userRes, nil
|
||||
}
|
||||
|
||||
type (
|
||||
// UserResetEmailService 发送密码重设邮件服务
|
||||
UserResetEmailService struct {
|
||||
UserName string `form:"email" json:"email" binding:"required,email"`
|
||||
}
|
||||
UserResetEmailParameterCtx struct{}
|
||||
)
|
||||
|
||||
const userResetPrefix = "user_reset_"
|
||||
|
||||
// Reset 发送密码重设邮件
|
||||
func (service *UserResetEmailService) Reset(c *gin.Context) serializer.Response {
|
||||
// 查找用户
|
||||
if user, err := model.GetUserByEmail(service.UserName); err == nil {
|
||||
|
||||
if user.Status == model.Baned || user.Status == model.OveruseBaned {
|
||||
return serializer.Err(serializer.CodeUserBaned, "This user is banned", nil)
|
||||
}
|
||||
if user.Status == model.NotActivicated {
|
||||
return serializer.Err(serializer.CodeUserNotActivated, "This user is not activated", nil)
|
||||
}
|
||||
// 创建密码重设会话
|
||||
secret := util.RandStringRunes(32)
|
||||
cache.Set(fmt.Sprintf("user_reset_%d", user.ID), secret, 3600)
|
||||
|
||||
// 生成用户访问的重设链接
|
||||
controller, _ := url.Parse("/reset")
|
||||
finalURL := model.GetSiteURL().ResolveReference(controller)
|
||||
queries := finalURL.Query()
|
||||
queries.Add("id", hashid.HashID(user.ID, hashid.UserID))
|
||||
queries.Add("sign", secret)
|
||||
finalURL.RawQuery = queries.Encode()
|
||||
|
||||
// 发送密码重设邮件
|
||||
title, body := email.NewResetEmail(user.Nick, finalURL.String())
|
||||
if err := email.Send(user.Email, title, body); err != nil {
|
||||
return serializer.Err(serializer.CodeFailedSendEmail, "Failed to send email", err)
|
||||
}
|
||||
func (service *UserResetEmailService) Reset(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
|
||||
u, err := userClient.GetByEmail(c, service.UserName)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeUserNotFound, "User not found", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Login 二步验证继续登录
|
||||
func (service *Enable2FA) Login(c *gin.Context) serializer.Response {
|
||||
if uid, ok := util.GetSession(c, "2fa_user_id").(uint); ok {
|
||||
// 查找用户
|
||||
expectedUser, err := model.GetActiveUserByID(uid)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeUserNotFound, "User not found", nil)
|
||||
}
|
||||
|
||||
// 验证二步验证代码
|
||||
if !totp.Validate(service.Code, expectedUser.TwoFactor) {
|
||||
return serializer.Err(serializer.Code2FACodeErr, "2FA code not correct", nil)
|
||||
}
|
||||
|
||||
//登陆成功,清空并设置session
|
||||
util.DeleteSession(c, "2fa_user_id")
|
||||
util.SetSession(c, map[string]interface{}{
|
||||
"user_id": expectedUser.ID,
|
||||
})
|
||||
|
||||
return serializer.BuildUserResponse(expectedUser)
|
||||
if u.Status == user.StatusManualBanned || u.Status == user.StatusSysBanned {
|
||||
return serializer.NewError(serializer.CodeUserBaned, "This user is banned", nil)
|
||||
}
|
||||
|
||||
return serializer.Err(serializer.CodeLoginSessionNotExist, "Login session not exist", nil)
|
||||
if u.Status == user.StatusInactive {
|
||||
return serializer.NewError(serializer.CodeUserNotActivated, "This user is not activated", nil)
|
||||
}
|
||||
|
||||
secret := util.RandStringRunes(32)
|
||||
if err := dep.KV().Set(fmt.Sprintf("%s%d", userResetPrefix, u.ID), secret, 3600); err != nil {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to create reset session", err)
|
||||
}
|
||||
|
||||
base := dep.SettingProvider().SiteURL(c)
|
||||
resetUrl := routes.MasterUserResetUrl(base)
|
||||
queries := resetUrl.Query()
|
||||
queries.Add("id", hashid.EncodeUserID(dep.HashIDEncoder(), u.ID))
|
||||
queries.Add("secret", secret)
|
||||
resetUrl.RawQuery = queries.Encode()
|
||||
|
||||
title, body, err := email.NewResetEmail(c, dep.SettingProvider(), u, resetUrl.String())
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeFailedSendEmail, "Failed to send activation email", err)
|
||||
}
|
||||
|
||||
if err := dep.EmailClient(c).Send(c, u.Email, title, body); err != nil {
|
||||
return serializer.NewError(serializer.CodeFailedSendEmail, "Failed to send activation email", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Login 用户登录函数
|
||||
func (service *UserLoginService) Login(c *gin.Context) serializer.Response {
|
||||
expectedUser, err := model.GetUserByEmail(service.UserName)
|
||||
func (service *UserLoginService) Login(c *gin.Context) (*ent.User, string, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadUserGroup{}, true)
|
||||
expectedUser, err := userClient.GetByEmail(ctx, service.UserName)
|
||||
|
||||
// 一系列校验
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeCredentialInvalid, "Wrong password or email address", err)
|
||||
}
|
||||
if authOK, _ := expectedUser.CheckPassword(service.Password); !authOK {
|
||||
return serializer.Err(serializer.CodeCredentialInvalid, "Wrong password or email address", nil)
|
||||
}
|
||||
if expectedUser.Status == model.Baned || expectedUser.Status == model.OveruseBaned {
|
||||
return serializer.Err(serializer.CodeUserBaned, "This account has been blocked", nil)
|
||||
}
|
||||
if expectedUser.Status == model.NotActivicated {
|
||||
return serializer.Err(serializer.CodeUserNotActivated, "This account is not activated", nil)
|
||||
err = serializer.NewError(serializer.CodeInvalidPassword, "Incorrect password or email address", err)
|
||||
} else if checkErr := inventory.CheckPassword(expectedUser, service.Password); checkErr != nil {
|
||||
err = serializer.NewError(serializer.CodeInvalidPassword, "Incorrect password or email address", err)
|
||||
} else if expectedUser.Status == user.StatusManualBanned || expectedUser.Status == user.StatusSysBanned {
|
||||
err = serializer.NewError(serializer.CodeUserBaned, "This account has been blocked", nil)
|
||||
} else if expectedUser.Status == user.StatusInactive {
|
||||
err = serializer.NewError(serializer.CodeUserNotActivated, "This account is not activated", nil)
|
||||
}
|
||||
|
||||
if expectedUser.TwoFactor != "" {
|
||||
// 需要二步验证
|
||||
util.SetSession(c, map[string]interface{}{
|
||||
"2fa_user_id": expectedUser.ID,
|
||||
})
|
||||
return serializer.Response{Code: 203}
|
||||
}
|
||||
|
||||
//登陆成功,清空并设置session
|
||||
util.SetSession(c, map[string]interface{}{
|
||||
"user_id": expectedUser.ID,
|
||||
})
|
||||
|
||||
return serializer.BuildUserResponse(expectedUser)
|
||||
|
||||
}
|
||||
|
||||
// CopySessionService service for copy user session
|
||||
type CopySessionService struct {
|
||||
ID string `uri:"id" binding:"required,uuid4"`
|
||||
}
|
||||
|
||||
const CopySessionTTL = 60
|
||||
|
||||
// Prepare generates the URL with short expiration duration
|
||||
func (s *CopySessionService) Prepare(c *gin.Context, user *model.User) serializer.Response {
|
||||
// 用户组有效期
|
||||
urlID := uuid.Must(uuid.NewV4())
|
||||
if err := cache.Set(fmt.Sprintf("copy_session_%s", urlID.String()), user.ID, CopySessionTTL); err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to create copy session", err)
|
||||
}
|
||||
|
||||
base := model.GetSiteURL()
|
||||
apiBaseURI, _ := url.Parse("/api/v3/user/session/copy/" + urlID.String())
|
||||
apiURL := base.ResolveReference(apiBaseURI)
|
||||
res, err := auth.SignURI(auth.General, apiURL.String(), CopySessionTTL)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to sign temp URL", err)
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Data: res.String(),
|
||||
if expectedUser.TwoFactorSecret != "" {
|
||||
twoFaSessionID := uuid.Must(uuid.NewV4())
|
||||
dep.KV().Set(fmt.Sprintf("user_2fa_%s", twoFaSessionID), expectedUser.ID, 600)
|
||||
return expectedUser, twoFaSessionID.String(), nil
|
||||
}
|
||||
|
||||
return expectedUser, "", nil
|
||||
}
|
||||
|
||||
// Copy a new session from active session, refresh max-age
|
||||
func (s *CopySessionService) Copy(c *gin.Context) serializer.Response {
|
||||
// 用户组有效期
|
||||
cacheKey := fmt.Sprintf("copy_session_%s", s.ID)
|
||||
uid, ok := cache.Get(cacheKey)
|
||||
type (
|
||||
LoginLogCtx struct{}
|
||||
)
|
||||
|
||||
func IssueToken(c *gin.Context) (*BuiltinLoginResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
u := inventory.UserFromContext(c)
|
||||
token, err := dep.TokenAuth().Issue(c, u)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeEncryptError, "Failed to issue token pair", err)
|
||||
}
|
||||
|
||||
return &BuiltinLoginResponse{
|
||||
User: BuildUser(u, dep.HashIDEncoder()),
|
||||
Token: *token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RefreshTokenParameterCtx define key fore RefreshTokenService
|
||||
type RefreshTokenParameterCtx struct{}
|
||||
|
||||
// RefreshTokenService refresh token service
|
||||
type RefreshTokenService struct {
|
||||
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||
}
|
||||
|
||||
func (s *RefreshTokenService) Refresh(c *gin.Context) (*auth.Token, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
token, err := dep.TokenAuth().Refresh(c, s.RefreshToken)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeCredentialInvalid, "Failed to issue token pair", err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
type (
|
||||
OtpValidationParameterCtx struct{}
|
||||
OtpValidationService struct {
|
||||
OTP string `json:"otp" binding:"required"`
|
||||
SessionID string `json:"session_id" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
// Login 用户登录函数
|
||||
func (service *OtpValidationService) Verify2FA(c *gin.Context) (*ent.User, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
kv := dep.KV()
|
||||
|
||||
sessionRaw, ok := kv.Get(fmt.Sprintf("user_2fa_%s", service.SessionID))
|
||||
if !ok {
|
||||
return serializer.Err(serializer.CodeNotFound, "", nil)
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "Session not found", nil)
|
||||
}
|
||||
|
||||
cache.Deletes([]string{cacheKey}, "")
|
||||
util.SetSession(c, map[string]interface{}{
|
||||
"user_id": uid.(uint),
|
||||
})
|
||||
uid := sessionRaw.(int)
|
||||
ctx := context.WithValue(c, inventory.LoadUserGroup{}, true)
|
||||
expectedUser, err := dep.UserClient().GetByID(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "User not found", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
if expectedUser.TwoFactorSecret != "" {
|
||||
if !totp.Validate(service.OTP, expectedUser.TwoFactorSecret) {
|
||||
err := serializer.NewError(serializer.Code2FACodeErr, "Incorrect 2FA code", nil)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
kv.Delete("user_2fa_", service.SessionID)
|
||||
return expectedUser, nil
|
||||
}
|
||||
|
||||
type (
|
||||
PrepareLoginParameterCtx struct{}
|
||||
PrepareLoginService struct {
|
||||
Email string `form:"email" binding:"required,email"`
|
||||
}
|
||||
)
|
||||
|
||||
func (service *PrepareLoginService) Prepare(c *gin.Context) (*PrepareLoginResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
ctx := context.WithValue(c, inventory.LoadUserPasskey{}, true)
|
||||
expectedUser, err := dep.UserClient().GetByEmail(ctx, service.Email)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "User not found", err)
|
||||
}
|
||||
|
||||
return &PrepareLoginResponse{
|
||||
WebAuthnEnabled: len(expectedUser.Edges.Passkey) > 0,
|
||||
PasswordEnabled: expectedUser.Password != "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
288
service/user/passkey.go
Normal file
288
service/user/passkey.go
Normal file
@@ -0,0 +1,288 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/samber/lo"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register(webauthn.SessionData{})
|
||||
}
|
||||
|
||||
type authnUser struct {
|
||||
hasher hashid.Encoder
|
||||
u *ent.User
|
||||
credentials []*ent.Passkey
|
||||
}
|
||||
|
||||
func (a *authnUser) WebAuthnID() []byte {
|
||||
return []byte(hashid.EncodeUserID(a.hasher, a.u.ID))
|
||||
}
|
||||
|
||||
func (a *authnUser) WebAuthnName() string {
|
||||
return a.u.Email
|
||||
}
|
||||
|
||||
func (a *authnUser) WebAuthnDisplayName() string {
|
||||
return a.u.Nick
|
||||
}
|
||||
|
||||
func (a *authnUser) WebAuthnCredentials() []webauthn.Credential {
|
||||
if a.credentials == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return lo.Map(a.credentials, func(item *ent.Passkey, index int) webauthn.Credential {
|
||||
return *item.Credential
|
||||
})
|
||||
}
|
||||
|
||||
const (
|
||||
authnSessionKey = "authn_session_"
|
||||
)
|
||||
|
||||
func PreparePasskeyLogin(c *gin.Context) (*PreparePasskeyLoginResponse, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
webAuthn, err := dep.WebAuthn(c)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to initialize WebAuthn", err)
|
||||
}
|
||||
|
||||
options, sessionData, err := webAuthn.BeginDiscoverableLogin()
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInitializeAuthn, "Failed to begin registration", err)
|
||||
}
|
||||
|
||||
sessionID := uuid.Must(uuid.NewV4()).String()
|
||||
if err := dep.KV().Set(fmt.Sprint("%s%s", authnSessionKey, sessionID), *sessionData, 300); err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to store session data", err)
|
||||
}
|
||||
|
||||
return &PreparePasskeyLoginResponse{
|
||||
Options: options,
|
||||
SessionID: sessionID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type (
|
||||
FinishPasskeyLoginParameterCtx struct{}
|
||||
FinishPasskeyLoginService struct {
|
||||
Response string `json:"response" binding:"required"`
|
||||
SessionID string `json:"session_id" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
func (s *FinishPasskeyLoginService) FinishPasskeyLogin(c *gin.Context) (*ent.User, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
kv := dep.KV()
|
||||
userClient := dep.UserClient()
|
||||
|
||||
sessionDataRaw, ok := kv.Get(fmt.Sprint("%s%s", authnSessionKey, s.SessionID))
|
||||
if !ok {
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "Session not found", nil)
|
||||
}
|
||||
|
||||
_ = kv.Delete(authnSessionKey, s.Response)
|
||||
|
||||
webAuthn, err := dep.WebAuthn(c)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to initialize WebAuthn", err)
|
||||
}
|
||||
|
||||
sessionData := sessionDataRaw.(webauthn.SessionData)
|
||||
pcc, err := protocol.ParseCredentialRequestResponseBody(strings.NewReader(s.Response))
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Failed to parse request", err)
|
||||
}
|
||||
|
||||
var loginedUser *ent.User
|
||||
discoverUserHandle := func(rawID, userHandle []byte) (user webauthn.User, err error) {
|
||||
uid, err := dep.HashIDEncoder().Decode(string(userHandle), hashid.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := context.WithValue(c, inventory.LoadUserPasskey{}, true)
|
||||
ctx = context.WithValue(ctx, inventory.LoadUserGroup{}, true)
|
||||
u, err := userClient.GetLoginUserByID(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get user", err)
|
||||
}
|
||||
|
||||
if inventory.IsAnonymousUser(u) {
|
||||
return nil, errors.New("anonymous user")
|
||||
}
|
||||
|
||||
loginedUser = u
|
||||
return &authnUser{u: u, hasher: dep.HashIDEncoder(), credentials: u.Edges.Passkey}, nil
|
||||
}
|
||||
|
||||
credential, err := webAuthn.ValidateDiscoverableLogin(discoverUserHandle, sessionData, pcc)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeWebAuthnCredentialError, "Failed to validate login", err)
|
||||
}
|
||||
|
||||
// Find the credential just used
|
||||
usedCredentialId := base64.StdEncoding.EncodeToString(credential.ID)
|
||||
usedCredential, found := lo.Find(loginedUser.Edges.Passkey, func(item *ent.Passkey) bool {
|
||||
return item.CredentialID == usedCredentialId
|
||||
})
|
||||
|
||||
if !found {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Passkey login passed but credential used is unknown", nil)
|
||||
}
|
||||
|
||||
// Update used at
|
||||
if err := userClient.MarkPasskeyUsed(c, loginedUser.ID, usedCredential.CredentialID); err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to update passkey", err)
|
||||
}
|
||||
|
||||
return loginedUser, nil
|
||||
}
|
||||
|
||||
func PreparePasskeyRegister(c *gin.Context) (*protocol.CredentialCreation, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
u := inventory.UserFromContext(c)
|
||||
|
||||
existingKeys, err := userClient.ListPasskeys(c, u.ID)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list passkeys", err)
|
||||
}
|
||||
|
||||
webAuthn, err := dep.WebAuthn(c)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to initialize WebAuthn", err)
|
||||
}
|
||||
|
||||
authSelect := protocol.AuthenticatorSelection{
|
||||
RequireResidentKey: protocol.ResidentKeyRequired(),
|
||||
UserVerification: protocol.VerificationPreferred,
|
||||
}
|
||||
|
||||
options, sessionData, err := webAuthn.BeginRegistration(
|
||||
&authnUser{u: u, hasher: dep.HashIDEncoder()},
|
||||
webauthn.WithAuthenticatorSelection(authSelect),
|
||||
webauthn.WithExclusions(lo.Map(existingKeys, func(item *ent.Passkey, index int) protocol.CredentialDescriptor {
|
||||
return protocol.CredentialDescriptor{
|
||||
Type: protocol.PublicKeyCredentialType,
|
||||
CredentialID: item.Credential.ID,
|
||||
Transport: item.Credential.Transport,
|
||||
AttestationType: item.Credential.AttestationType,
|
||||
}
|
||||
})),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInitializeAuthn, "Failed to begin registration", err)
|
||||
}
|
||||
|
||||
if err := dep.KV().Set(fmt.Sprint("%s%d", authnSessionKey, u.ID), *sessionData, 300); err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to store session data", err)
|
||||
}
|
||||
|
||||
return options, nil
|
||||
}
|
||||
|
||||
type (
|
||||
FinishPasskeyRegisterParameterCtx struct{}
|
||||
FinishPasskeyRegisterService struct {
|
||||
Response string `json:"response" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
UA string `json:"ua" binding:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
func (s *FinishPasskeyRegisterService) FinishPasskeyRegister(c *gin.Context) (*Passkey, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
kv := dep.KV()
|
||||
u := inventory.UserFromContext(c)
|
||||
|
||||
sessionDataRaw, ok := kv.Get(fmt.Sprint("%s%d", authnSessionKey, u.ID))
|
||||
if !ok {
|
||||
return nil, serializer.NewError(serializer.CodeNotFound, "Session not found", nil)
|
||||
}
|
||||
|
||||
_ = kv.Delete(authnSessionKey, strconv.Itoa(u.ID))
|
||||
|
||||
webAuthn, err := dep.WebAuthn(c)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to initialize WebAuthn", err)
|
||||
}
|
||||
|
||||
sessionData := sessionDataRaw.(webauthn.SessionData)
|
||||
pcc, err := protocol.ParseCredentialCreationResponseBody(strings.NewReader(s.Response))
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeParamErr, "Failed to parse request", err)
|
||||
}
|
||||
|
||||
credential, err := webAuthn.CreateCredential(&authnUser{u: u, hasher: dep.HashIDEncoder()}, sessionData, pcc)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeWebAuthnCredentialError, "Failed to finish registration", err)
|
||||
}
|
||||
|
||||
client := dep.UAParser().Parse(s.UA)
|
||||
name := util.Replace(map[string]string{
|
||||
"{os}": client.Os.Family,
|
||||
"{browser}": client.UserAgent.Family,
|
||||
}, s.Name)
|
||||
|
||||
passkey, err := dep.UserClient().AddPasskey(c, u.ID, name, credential)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to add passkey", err)
|
||||
}
|
||||
|
||||
res := BuildPasskey(passkey)
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
type (
|
||||
DeletePasskeyService struct {
|
||||
ID string `form:"id" binding:"required"`
|
||||
}
|
||||
DeletePasskeyParameterCtx struct{}
|
||||
)
|
||||
|
||||
func (s *DeletePasskeyService) DeletePasskey(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
u := inventory.UserFromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
|
||||
existingKeys, err := userClient.ListPasskeys(c, u.ID)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to list passkeys", err)
|
||||
}
|
||||
|
||||
var existing *ent.Passkey
|
||||
for _, key := range existingKeys {
|
||||
if key.CredentialID == s.ID {
|
||||
existing = key
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if existing == nil {
|
||||
return serializer.NewError(serializer.CodeNotFound, "Passkey not found", nil)
|
||||
}
|
||||
|
||||
if err := userClient.RemovePasskey(c, u.ID, s.ID); err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to delete passkey", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,113 +1,148 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/email"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/user"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/email"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RegisterParameterCtx define key fore UserRegisterService
|
||||
type RegisterParameterCtx struct{}
|
||||
|
||||
// UserRegisterService 管理用户注册的服务
|
||||
type UserRegisterService struct {
|
||||
//TODO 细致调整验证规则
|
||||
UserName string `form:"userName" json:"userName" binding:"required,email"`
|
||||
Password string `form:"Password" json:"Password" binding:"required,min=4,max=64"`
|
||||
UserName string `form:"email" json:"email" binding:"required,email"`
|
||||
Password string `form:"password" json:"password" binding:"required,min=6,max=64"`
|
||||
Language string `form:"language" json:"language"`
|
||||
}
|
||||
|
||||
// Register 新用户注册
|
||||
func (service *UserRegisterService) Register(c *gin.Context) serializer.Response {
|
||||
// 相关设定
|
||||
options := model.GetSettingByNames("email_active")
|
||||
dep := dependency.FromContext(c)
|
||||
settings := dep.SettingProvider()
|
||||
|
||||
// 相关设定
|
||||
isEmailRequired := model.IsTrueVal(options["email_active"])
|
||||
defaultGroup := model.GetIntSetting("default_group", 2)
|
||||
|
||||
// 创建新的用户对象
|
||||
user := model.NewUser()
|
||||
user.Email = service.UserName
|
||||
user.Nick = strings.Split(service.UserName, "@")[0]
|
||||
user.SetPassword(service.Password)
|
||||
user.Status = model.Active
|
||||
isEmailRequired := settings.EmailActivationEnabled(c)
|
||||
args := &inventory.NewUserArgs{
|
||||
Email: strings.ToLower(service.UserName),
|
||||
PlainPassword: service.Password,
|
||||
Status: user.StatusActive,
|
||||
GroupID: settings.DefaultGroup(c),
|
||||
Language: service.Language,
|
||||
}
|
||||
if isEmailRequired {
|
||||
user.Status = model.NotActivicated
|
||||
}
|
||||
user.GroupID = uint(defaultGroup)
|
||||
userNotActivated := false
|
||||
// 创建用户
|
||||
if err := model.DB.Create(&user).Error; err != nil {
|
||||
//检查已存在使用者是否尚未激活
|
||||
expectedUser, err := model.GetUserByEmail(service.UserName)
|
||||
if expectedUser.Status == model.NotActivicated {
|
||||
userNotActivated = true
|
||||
user = expectedUser
|
||||
} else {
|
||||
return serializer.Err(serializer.CodeEmailExisted, "Email already in use", err)
|
||||
}
|
||||
args.Status = user.StatusInactive
|
||||
}
|
||||
|
||||
userClient := dep.UserClient()
|
||||
uc, tx, _, err := inventory.WithTx(c, userClient)
|
||||
if err != nil {
|
||||
return serializer.DBErr(c, "Failed to start transaction", err)
|
||||
}
|
||||
|
||||
expectedUser, err := uc.Create(c, args)
|
||||
if expectedUser != nil {
|
||||
util.WithValue(c, inventory.UserCtx{}, expectedUser)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
_ = inventory.Rollback(tx)
|
||||
if errors.Is(err, inventory.ErrUserEmailExisted) {
|
||||
return serializer.ErrWithDetails(c, serializer.CodeEmailExisted, "Email already in use", err)
|
||||
}
|
||||
|
||||
if errors.Is(err, inventory.ErrInactiveUserExisted) {
|
||||
if err := sendActivationEmail(c, dep, expectedUser); err != nil {
|
||||
return serializer.ErrWithDetails(c, serializer.CodeNotSet, "", err)
|
||||
}
|
||||
|
||||
return serializer.ErrWithDetails(c, serializer.CodeEmailSent, "User is not activated, activation email has been resent", nil)
|
||||
}
|
||||
|
||||
return serializer.DBErr(c, "Failed to insert user row", err)
|
||||
}
|
||||
|
||||
if err := inventory.Commit(tx); err != nil {
|
||||
return serializer.DBErr(c, "Failed to commit user row", err)
|
||||
}
|
||||
|
||||
// 发送激活邮件
|
||||
if isEmailRequired {
|
||||
|
||||
// 签名激活请求API
|
||||
base := model.GetSiteURL()
|
||||
userID := hashid.HashID(user.ID, hashid.UserID)
|
||||
controller, _ := url.Parse("/api/v3/user/activate/" + userID)
|
||||
activateURL, err := auth.SignURI(auth.General, base.ResolveReference(controller).String(), 86400)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeEncryptError, "Failed to sign the activation link", err)
|
||||
}
|
||||
|
||||
// 取得签名
|
||||
credential := activateURL.Query().Get("sign")
|
||||
|
||||
// 生成对用户访问的激活地址
|
||||
controller, _ = url.Parse("/activate")
|
||||
finalURL := base.ResolveReference(controller)
|
||||
queries := finalURL.Query()
|
||||
queries.Add("id", userID)
|
||||
queries.Add("sign", credential)
|
||||
finalURL.RawQuery = queries.Encode()
|
||||
|
||||
// 返送激活邮件
|
||||
title, body := email.NewActivationEmail(user.Email,
|
||||
finalURL.String(),
|
||||
)
|
||||
if err := email.Send(user.Email, title, body); err != nil {
|
||||
return serializer.Err(serializer.CodeFailedSendEmail, "Failed to send activation email", err)
|
||||
}
|
||||
if userNotActivated == true {
|
||||
//原本在上面要抛出的DBErr,放来这边抛出
|
||||
return serializer.Err(serializer.CodeEmailSent, "User is not activated, activation email has been resent", nil)
|
||||
} else {
|
||||
return serializer.Response{Code: 203}
|
||||
if err := sendActivationEmail(c, dep, expectedUser); err != nil {
|
||||
return serializer.ErrWithDetails(c, serializer.CodeNotSet, "", err)
|
||||
}
|
||||
return serializer.Response{Code: serializer.CodeNotFullySuccess}
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
return serializer.Response{Data: BuildUser(expectedUser, dep.HashIDEncoder())}
|
||||
}
|
||||
|
||||
// Activate 激活用户
|
||||
func (service *SettingService) Activate(c *gin.Context) serializer.Response {
|
||||
// 查找待激活用户
|
||||
uid, _ := c.Get("object_id")
|
||||
user, err := model.GetUserByID(uid.(uint))
|
||||
func sendActivationEmail(ctx context.Context, dep dependency.Dep, newUser *ent.User) error {
|
||||
base := dep.SettingProvider().SiteURL(ctx)
|
||||
userID := hashid.EncodeUserID(dep.HashIDEncoder(), newUser.ID)
|
||||
ttl := time.Now().Add(time.Duration(24) * time.Hour)
|
||||
activateURL, err := auth.SignURI(ctx, dep.GeneralAuth(), routes.MasterUserActivateAPIUrl(base, userID).String(), &ttl)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeUserNotFound, "User not fount", err)
|
||||
return serializer.NewError(serializer.CodeEncryptError, "Failed to sign the activation link", err)
|
||||
}
|
||||
|
||||
// 取得签名
|
||||
credential := activateURL.Query().Get("sign")
|
||||
|
||||
// 生成对用户访问的激活地址
|
||||
finalURL := routes.MasterUserActivateUrl(base)
|
||||
queries := finalURL.Query()
|
||||
queries.Add("id", userID)
|
||||
queries.Add("sign", credential)
|
||||
finalURL.RawQuery = queries.Encode()
|
||||
|
||||
// 返送激活邮件
|
||||
title, body, err := email.NewActivationEmail(ctx, dep.SettingProvider(), newUser, finalURL.String())
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeFailedSendEmail, "Failed to send activation email", err)
|
||||
}
|
||||
|
||||
if err := dep.EmailClient(ctx).Send(ctx, newUser.Email, title, body); err != nil {
|
||||
return serializer.NewError(serializer.CodeFailedSendEmail, "Failed to send activation email", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ActivateUser 激活用户
|
||||
func ActivateUser(c *gin.Context) serializer.Response {
|
||||
uid := hashid.FromContext(c)
|
||||
dep := dependency.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
|
||||
// 查找待激活用户
|
||||
inactiveUser, err := userClient.GetByID(c, uid)
|
||||
if err != nil {
|
||||
return serializer.ErrWithDetails(c, serializer.CodeUserNotFound, "User not fount", err)
|
||||
}
|
||||
|
||||
// 检查状态
|
||||
if user.Status != model.NotActivicated {
|
||||
return serializer.Err(serializer.CodeUserCannotActivate, "This user cannot be activated", nil)
|
||||
if inactiveUser.Status != user.StatusInactive {
|
||||
return serializer.ErrWithDetails(c, serializer.CodeUserCannotActivate, "This user cannot be activated", nil)
|
||||
}
|
||||
|
||||
// 激活用户
|
||||
user.SetStatus(model.Active)
|
||||
activeUser, err := userClient.SetStatus(c, inactiveUser, user.StatusActive)
|
||||
if err != nil {
|
||||
return serializer.DBErr(c, "Failed to update user", err)
|
||||
}
|
||||
|
||||
return serializer.Response{Data: user.Email}
|
||||
util.WithValue(c, inventory.UserCtx{}, activeUser)
|
||||
return serializer.Response{Data: BuildUser(activeUser, dep.HashIDEncoder())}
|
||||
}
|
||||
|
||||
219
service/user/response.go
Normal file
219
service/user/response.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent/user"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/samber/lo"
|
||||
"github.com/ua-parser/uap-go/uaparser"
|
||||
)
|
||||
|
||||
type PreparePasskeyLoginResponse struct {
|
||||
Options *protocol.CredentialAssertion `json:"options"`
|
||||
SessionID string `json:"session_id"`
|
||||
}
|
||||
|
||||
type UserSettings struct {
|
||||
VersionRetentionEnabled bool `json:"version_retention_enabled"`
|
||||
VersionRetentionExt []string `json:"version_retention_ext,omitempty"`
|
||||
VersionRetentionMax int `json:"version_retention_max,omitempty"`
|
||||
Paswordless bool `json:"passwordless"`
|
||||
TwoFAEnabled bool `json:"two_fa_enabled"`
|
||||
Passkeys []Passkey `json:"passkeys,omitempty"`
|
||||
}
|
||||
|
||||
func BuildUserSettings(u *ent.User, passkeys []*ent.Passkey, parser *uaparser.Parser) *UserSettings {
|
||||
return &UserSettings{
|
||||
VersionRetentionEnabled: u.Settings.VersionRetention,
|
||||
VersionRetentionExt: u.Settings.VersionRetentionExt,
|
||||
VersionRetentionMax: u.Settings.VersionRetentionMax,
|
||||
TwoFAEnabled: u.TwoFactorSecret != "",
|
||||
Paswordless: u.Password == "",
|
||||
Passkeys: lo.Map(passkeys, func(item *ent.Passkey, index int) Passkey {
|
||||
return BuildPasskey(item)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
type Passkey struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
UsedAt *time.Time `json:"used_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func BuildPasskey(passkey *ent.Passkey) Passkey {
|
||||
return Passkey{
|
||||
ID: passkey.CredentialID,
|
||||
Name: passkey.Name,
|
||||
UsedAt: passkey.UsedAt,
|
||||
CreatedAt: passkey.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// Node option for handling workflows.
|
||||
type Node struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Capabilities *boolset.BooleanSet `json:"capabilities"`
|
||||
}
|
||||
|
||||
// BuildNodes serialize a list of nodes.
|
||||
func BuildNodes(nodes []*ent.Node, idEncoder hashid.Encoder) []*Node {
|
||||
res := make([]*Node, 0, len(nodes))
|
||||
for _, v := range nodes {
|
||||
res = append(res, BuildNode(v, idEncoder))
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// BuildNode serialize a node.
|
||||
func BuildNode(node *ent.Node, idEncoder hashid.Encoder) *Node {
|
||||
return &Node{
|
||||
ID: hashid.EncodeNodeID(idEncoder, node.ID),
|
||||
Name: node.Name,
|
||||
Type: string(node.Type),
|
||||
Capabilities: node.Capabilities,
|
||||
}
|
||||
}
|
||||
|
||||
// BuiltinLoginResponse response for a successful login for builtin auth provider.
|
||||
type BuiltinLoginResponse struct {
|
||||
User User `json:"user"`
|
||||
Token auth.Token `json:"token"`
|
||||
}
|
||||
|
||||
// User 用户序列化器
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Nickname string `json:"nickname"`
|
||||
Status user.Status `json:"status,omitempty"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
PreferredTheme string `json:"preferred_theme,omitempty"`
|
||||
Anonymous bool `json:"anonymous,omitempty"`
|
||||
Group *Group `json:"group,omitempty"`
|
||||
Pined []types.PinedFile `json:"pined,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Permission *boolset.BooleanSet `json:"permission,omitempty"`
|
||||
DirectLinkBatchSize int `json:"direct_link_batch_size,omitempty"`
|
||||
TrashRetention int `json:"trash_retention,omitempty"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type PrepareLoginResponse struct {
|
||||
WebAuthnEnabled bool `json:"webauthn_enabled"`
|
||||
PasswordEnabled bool `json:"password_enabled"`
|
||||
}
|
||||
|
||||
// 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 *ent.User, idEncoder hashid.Encoder) User {
|
||||
return User{
|
||||
ID: hashid.EncodeUserID(idEncoder, user.ID),
|
||||
Email: user.Email,
|
||||
Nickname: user.Nick,
|
||||
Status: user.Status,
|
||||
Avatar: user.Avatar,
|
||||
CreatedAt: user.CreatedAt,
|
||||
PreferredTheme: user.Settings.PreferredTheme,
|
||||
Anonymous: user.ID == 0,
|
||||
Group: BuildGroup(user.Edges.Group, idEncoder),
|
||||
Pined: user.Settings.Pined,
|
||||
Language: user.Settings.Language,
|
||||
}
|
||||
}
|
||||
|
||||
func BuildGroup(group *ent.Group, idEncoder hashid.Encoder) *Group {
|
||||
if group == nil {
|
||||
return nil
|
||||
}
|
||||
return &Group{
|
||||
ID: hashid.EncodeGroupID(idEncoder, group.ID),
|
||||
Name: group.Name,
|
||||
Permission: group.Permissions,
|
||||
DirectLinkBatchSize: group.Settings.SourceBatchSize,
|
||||
TrashRetention: group.Settings.TrashRetention,
|
||||
}
|
||||
}
|
||||
|
||||
const sensitiveTag = "redacted"
|
||||
|
||||
const (
|
||||
RedactLevelAnonymous = iota
|
||||
RedactLevelUser
|
||||
)
|
||||
|
||||
// BuildUserRedacted Serialize a user without sensitive information.
|
||||
func BuildUserRedacted(u *ent.User, level int, idEncoder hashid.Encoder) User {
|
||||
userRaw := BuildUser(u, idEncoder)
|
||||
|
||||
user := User{
|
||||
ID: userRaw.ID,
|
||||
Nickname: userRaw.Nickname,
|
||||
Avatar: userRaw.Avatar,
|
||||
CreatedAt: userRaw.CreatedAt,
|
||||
}
|
||||
|
||||
if userRaw.Group != nil {
|
||||
user.Group = RedactedGroup(userRaw.Group)
|
||||
}
|
||||
|
||||
if level == RedactLevelUser {
|
||||
user.Email = userRaw.Email
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// BuildGroupRedacted Serialize a group without sensitive information.
|
||||
func RedactedGroup(g *Group) *Group {
|
||||
if g == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &Group{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
}
|
||||
}
|
||||
@@ -1,256 +1,308 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/request"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/thumb"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
model "github.com/cloudreve/Cloudreve/v3/models"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
|
||||
"github.com/cloudreve/Cloudreve/v3/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pquerna/otp/totp"
|
||||
)
|
||||
|
||||
// SettingService 通用设置服务
|
||||
type SettingService struct {
|
||||
}
|
||||
|
||||
// SettingListService 通用设置列表服务
|
||||
type SettingListService struct {
|
||||
Page int `form:"page" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
// AvatarService 头像服务
|
||||
type AvatarService struct {
|
||||
Size string `uri:"size" binding:"required,eq=l|eq=m|eq=s"`
|
||||
}
|
||||
|
||||
// SettingUpdateService 设定更改服务
|
||||
type SettingUpdateService struct {
|
||||
Option string `uri:"option" binding:"required,eq=nick|eq=theme|eq=homepage|eq=vip|eq=qq|eq=policy|eq=password|eq=2fa|eq=authn"`
|
||||
}
|
||||
|
||||
// OptionsChangeHandler 属性更改接口
|
||||
type OptionsChangeHandler interface {
|
||||
Update(*gin.Context, *model.User) serializer.Response
|
||||
}
|
||||
|
||||
// ChangerNick 昵称更改服务
|
||||
type ChangerNick struct {
|
||||
Nick string `json:"nick" binding:"required,min=1,max=255"`
|
||||
}
|
||||
|
||||
// PolicyChange 更改存储策略
|
||||
type PolicyChange struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
}
|
||||
|
||||
// HomePage 更改个人主页开关
|
||||
type HomePage struct {
|
||||
Enabled bool `json:"status"`
|
||||
}
|
||||
|
||||
// PasswordChange 更改密码
|
||||
type PasswordChange struct {
|
||||
Old string `json:"old" binding:"required,min=4,max=64"`
|
||||
New string `json:"new" binding:"required,min=4,max=64"`
|
||||
}
|
||||
|
||||
// Enable2FA 开启二步验证
|
||||
type Enable2FA struct {
|
||||
Code string `json:"code" binding:"required"`
|
||||
}
|
||||
|
||||
// DeleteWebAuthn 删除WebAuthn凭证
|
||||
type DeleteWebAuthn struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
}
|
||||
|
||||
// ThemeChose 主题选择
|
||||
type ThemeChose struct {
|
||||
Theme string `json:"theme" binding:"required,hexcolor|rgb|rgba|hsl"`
|
||||
}
|
||||
|
||||
// Update 更新主题设定
|
||||
func (service *ThemeChose) Update(c *gin.Context, user *model.User) serializer.Response {
|
||||
user.OptionsSerialized.PreferredTheme = service.Theme
|
||||
if err := user.UpdateOptions(); err != nil {
|
||||
return serializer.DBErr("Failed to update user preferences", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Update 删除凭证
|
||||
func (service *DeleteWebAuthn) Update(c *gin.Context, user *model.User) serializer.Response {
|
||||
user.RemoveAuthn(service.ID)
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Update 更改二步验证设定
|
||||
func (service *Enable2FA) Update(c *gin.Context, user *model.User) serializer.Response {
|
||||
if user.TwoFactor == "" {
|
||||
// 开启2FA
|
||||
secret, ok := util.GetSession(c, "2fa_init").(string)
|
||||
if !ok {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "You have not initiated 2FA session", nil)
|
||||
}
|
||||
|
||||
if !totp.Validate(service.Code, secret) {
|
||||
return serializer.ParamErr("Incorrect 2FA code", nil)
|
||||
}
|
||||
|
||||
if err := user.Update(map[string]interface{}{"two_factor": secret}); err != nil {
|
||||
return serializer.DBErr("Failed to update user preferences", err)
|
||||
}
|
||||
|
||||
} else {
|
||||
// 关闭2FA
|
||||
if !totp.Validate(service.Code, user.TwoFactor) {
|
||||
return serializer.ParamErr("Incorrect 2FA code", nil)
|
||||
}
|
||||
|
||||
if err := user.Update(map[string]interface{}{"two_factor": ""}); err != nil {
|
||||
return serializer.DBErr("Failed to update user preferences", err)
|
||||
}
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
const (
|
||||
twoFaEnableSessionKey = "2fa_init_"
|
||||
)
|
||||
|
||||
// Init2FA 初始化二步验证
|
||||
func (service *SettingService) Init2FA(c *gin.Context, user *model.User) serializer.Response {
|
||||
func Init2FA(c *gin.Context) (string, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
user := inventory.UserFromContext(c)
|
||||
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: "Cloudreve",
|
||||
AccountName: user.Email,
|
||||
})
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to generate TOTP secret", err)
|
||||
return "", serializer.NewError(serializer.CodeInternalSetting, "Failed to generate TOTP secret", err)
|
||||
}
|
||||
|
||||
util.SetSession(c, map[string]interface{}{"2fa_init": key.Secret()})
|
||||
return serializer.Response{Data: key.Secret()}
|
||||
if err := dep.KV().Set(fmt.Sprintf("%s%d", twoFaEnableSessionKey, user.ID), key.Secret(), 600); err != nil {
|
||||
return "", serializer.NewError(serializer.CodeInternalSetting, "Failed to store TOTP session", err)
|
||||
}
|
||||
|
||||
return key.Secret(), nil
|
||||
}
|
||||
|
||||
// Update 更改密码
|
||||
func (service *PasswordChange) Update(c *gin.Context, user *model.User) serializer.Response {
|
||||
// 验证老密码
|
||||
if ok, _ := user.CheckPassword(service.Old); !ok {
|
||||
return serializer.Err(serializer.CodeIncorrectPassword, "", nil)
|
||||
type (
|
||||
// AvatarService Service to get avatar
|
||||
GetAvatarService struct {
|
||||
NoCache bool `form:"nocache"`
|
||||
}
|
||||
GetAvatarServiceParamsCtx struct{}
|
||||
)
|
||||
|
||||
// 更改为新密码
|
||||
user.SetPassword(service.New)
|
||||
if err := user.Update(map[string]interface{}{"password": user.Password}); err != nil {
|
||||
return serializer.DBErr("Failed to update password", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Update 切换个人主页开关
|
||||
func (service *HomePage) Update(c *gin.Context, user *model.User) serializer.Response {
|
||||
user.OptionsSerialized.ProfileOff = !service.Enabled
|
||||
if err := user.UpdateOptions(); err != nil {
|
||||
return serializer.DBErr("Failed to update user preferences", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Update 更改昵称
|
||||
func (service *ChangerNick) Update(c *gin.Context, user *model.User) serializer.Response {
|
||||
if err := user.Update(map[string]interface{}{"nick": service.Nick}); err != nil {
|
||||
return serializer.DBErr("Failed to update user", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
const (
|
||||
GravatarAvatar = "gravatar"
|
||||
FileAvatar = "file"
|
||||
)
|
||||
|
||||
// Get 获取用户头像
|
||||
func (service *AvatarService) Get(c *gin.Context) serializer.Response {
|
||||
func (service *GetAvatarService) Get(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
settings := dep.SettingProvider()
|
||||
// 查找目标用户
|
||||
uid, _ := c.Get("object_id")
|
||||
user, err := model.GetActiveUserByID(uid.(uint))
|
||||
uid := hashid.FromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
user, err := userClient.GetByID(c, uid)
|
||||
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeUserNotFound, "", err)
|
||||
return serializer.NewError(serializer.CodeUserNotFound, "", err)
|
||||
}
|
||||
|
||||
if !service.NoCache {
|
||||
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", settings.PublicResourceMaxAge(c)))
|
||||
}
|
||||
|
||||
// 未设定头像时,返回404错误
|
||||
if user.Avatar == "" {
|
||||
c.Status(404)
|
||||
return serializer.Response{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取头像设置
|
||||
sizes := map[string]string{
|
||||
"s": model.GetSettingByName("avatar_size_s"),
|
||||
"m": model.GetSettingByName("avatar_size_m"),
|
||||
"l": model.GetSettingByName("avatar_size_l"),
|
||||
}
|
||||
avatarSettings := settings.Avatar(c)
|
||||
|
||||
// Gravatar 头像重定向
|
||||
if user.Avatar == "gravatar" {
|
||||
server := model.GetSettingByName("gravatar_server")
|
||||
gravatarRoot, err := url.Parse(server)
|
||||
if user.Avatar == GravatarAvatar {
|
||||
gravatarRoot, err := url.Parse(avatarSettings.Gravatar)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "Failed to parse Gravatar server", err)
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "Failed to parse Gravatar server", err)
|
||||
}
|
||||
email_lowered := strings.ToLower(user.Email)
|
||||
has := md5.Sum([]byte(email_lowered))
|
||||
avatar, _ := url.Parse(fmt.Sprintf("/avatar/%x?d=mm&s=%s", has, sizes[service.Size]))
|
||||
avatar, _ := url.Parse(fmt.Sprintf("/avatar/%x?d=mm&s=200", has))
|
||||
|
||||
return serializer.Response{
|
||||
Code: -301,
|
||||
Data: gravatarRoot.ResolveReference(avatar).String(),
|
||||
}
|
||||
c.Redirect(http.StatusFound, gravatarRoot.ResolveReference(avatar).String())
|
||||
return nil
|
||||
}
|
||||
|
||||
// 本地文件头像
|
||||
if user.Avatar == "file" {
|
||||
avatarRoot := util.RelativePath(model.GetSettingByName("avatar_path"))
|
||||
sizeToInt := map[string]string{
|
||||
"s": "0",
|
||||
"m": "1",
|
||||
"l": "2",
|
||||
}
|
||||
if user.Avatar == FileAvatar {
|
||||
avatarRoot := util.DataPath(avatarSettings.Path)
|
||||
|
||||
avatar, err := os.Open(filepath.Join(avatarRoot, fmt.Sprintf("avatar_%d_%s.png", user.ID, sizeToInt[service.Size])))
|
||||
avatar, err := os.Open(filepath.Join(avatarRoot, fmt.Sprintf("avatar_%d.png", user.ID)))
|
||||
if err != nil {
|
||||
dep.Logger().Warning("Failed to open avatar file", err)
|
||||
c.Status(404)
|
||||
return serializer.Response{}
|
||||
}
|
||||
defer avatar.Close()
|
||||
|
||||
http.ServeContent(c.Writer, c.Request, "avatar.png", user.UpdatedAt, avatar)
|
||||
return serializer.Response{}
|
||||
return nil
|
||||
}
|
||||
|
||||
c.Status(404)
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// ListTasks 列出任务
|
||||
func (service *SettingListService) ListTasks(c *gin.Context, user *model.User) serializer.Response {
|
||||
tasks, total := model.ListTasks(user.ID, service.Page, 10, "updated_at desc")
|
||||
return serializer.BuildTaskList(tasks, total)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Settings 获取用户设定
|
||||
func (service *SettingService) Settings(c *gin.Context, user *model.User) serializer.Response {
|
||||
return serializer.Response{
|
||||
Data: map[string]interface{}{
|
||||
"uid": user.ID,
|
||||
"homepage": !user.OptionsSerialized.ProfileOff,
|
||||
"two_factor": user.TwoFactor != "",
|
||||
"prefer_theme": user.OptionsSerialized.PreferredTheme,
|
||||
"themes": model.GetSettingByName("themes"),
|
||||
"authn": serializer.BuildWebAuthnList(user.WebAuthnCredentials()),
|
||||
},
|
||||
func GetUserSettings(c *gin.Context) (*UserSettings, error) {
|
||||
dep := dependency.FromContext(c)
|
||||
u := inventory.UserFromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
passkeys, err := userClient.ListPasskeys(c, u.ID)
|
||||
if err != nil {
|
||||
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get user passkey", err)
|
||||
}
|
||||
|
||||
return BuildUserSettings(u, passkeys, dep.UAParser()), nil
|
||||
|
||||
// 用户组有效期
|
||||
|
||||
//return serializer.Response{
|
||||
// Data: map[string]interface{}{
|
||||
// "uid": user.ID,
|
||||
// "qq": user.OpenID != "",
|
||||
// "homepage": !user.OptionsSerialized.ProfileOff,
|
||||
// "two_factor": user.TwoFactor != "",
|
||||
// "prefer_theme": user.OptionsSerialized.PreferredTheme,
|
||||
// "themes": model.GetSettingByName("themes"),
|
||||
// "group_expires": groupExpires,
|
||||
// "authn": serializer.BuildWebAuthnList(user.WebAuthnCredentials()),
|
||||
// },
|
||||
//}
|
||||
}
|
||||
|
||||
func UpdateUserAvatar(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
u := inventory.UserFromContext(c)
|
||||
settings := dep.SettingProvider()
|
||||
|
||||
avatarSettings := settings.AvatarProcess(c)
|
||||
if c.Request.ContentLength == -1 || c.Request.ContentLength > avatarSettings.MaxFileSize {
|
||||
request.BlackHole(c.Request.Body)
|
||||
return serializer.NewError(serializer.CodeFileTooLarge, "", nil)
|
||||
}
|
||||
|
||||
if c.Request.ContentLength == 0 {
|
||||
// Use Gravatar for empty body
|
||||
if _, err := dep.UserClient().UpdateAvatar(c, u, GravatarAvatar); err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to update user avatar", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return updateAvatarFile(c, u, c.GetHeader("Content-Type"), c.Request.Body, avatarSettings)
|
||||
}
|
||||
|
||||
func updateAvatarFile(ctx context.Context, u *ent.User, contentType string, file io.Reader, avatarSettings *setting.AvatarProcess) error {
|
||||
dep := dependency.FromContext(ctx)
|
||||
// Detect ext from content type
|
||||
ext := "png"
|
||||
switch contentType {
|
||||
case "image/jpeg", "image/jpg":
|
||||
ext = "jpg"
|
||||
case "image/gif":
|
||||
ext = "gif"
|
||||
}
|
||||
avatar, err := thumb.NewThumbFromFile(file, ext)
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeParamErr, "Invalid image", err)
|
||||
}
|
||||
|
||||
// Resize and save avatar
|
||||
avatar.CreateAvatar(avatarSettings.MaxWidth)
|
||||
avatarRoot := util.DataPath(avatarSettings.Path)
|
||||
f, err := util.CreatNestedFile(filepath.Join(avatarRoot, fmt.Sprintf("avatar_%d.png", u.ID)))
|
||||
if err != nil {
|
||||
return serializer.NewError(serializer.CodeIOFailed, "Failed to create avatar file", err)
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
if err := avatar.Save(f, &setting.ThumbEncode{
|
||||
Quality: 100,
|
||||
Format: "png",
|
||||
}); err != nil {
|
||||
return serializer.NewError(serializer.CodeIOFailed, "Failed to save avatar file", err)
|
||||
}
|
||||
|
||||
if _, err := dep.UserClient().UpdateAvatar(ctx, u, FileAvatar); err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to update user avatar", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
PatchUserSetting struct {
|
||||
Nick *string `json:"nick" binding:"omitempty,min=1,max=255"`
|
||||
Language *string `json:"language" binding:"omitempty,min=1,max=255"`
|
||||
PreferredTheme *string `json:"preferred_theme" binding:"omitempty,hexcolor|rgb|rgba|hsl"`
|
||||
VersionRetentionEnabled *bool `json:"version_retention_enabled" binding:"omitempty"`
|
||||
VersionRetentionExt *[]string `json:"version_retention_ext" binding:"omitempty"`
|
||||
VersionRetentionMax *int `json:"version_retention_max" binding:"omitempty,min=0"`
|
||||
CurrentPassword *string `json:"current_password" binding:"omitempty,min=4,max=64"`
|
||||
NewPassword *string `json:"new_password" binding:"omitempty,min=6,max=64"`
|
||||
TwoFAEnabled *bool `json:"two_fa_enabled" binding:"omitempty"`
|
||||
TwoFACode *string `json:"two_fa_code" binding:"omitempty"`
|
||||
}
|
||||
PatchUserSettingParamsCtx struct{}
|
||||
)
|
||||
|
||||
func (s *PatchUserSetting) Patch(c *gin.Context) error {
|
||||
dep := dependency.FromContext(c)
|
||||
u := inventory.UserFromContext(c)
|
||||
userClient := dep.UserClient()
|
||||
saveSetting := false
|
||||
|
||||
if s.Nick != nil {
|
||||
if _, err := userClient.UpdateNickname(c, u, *s.Nick); err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to update user nick", err)
|
||||
}
|
||||
}
|
||||
|
||||
if s.Language != nil {
|
||||
u.Settings.Language = *s.Language
|
||||
saveSetting = true
|
||||
}
|
||||
|
||||
if s.PreferredTheme != nil {
|
||||
u.Settings.PreferredTheme = *s.PreferredTheme
|
||||
saveSetting = true
|
||||
}
|
||||
|
||||
if s.VersionRetentionEnabled != nil {
|
||||
u.Settings.VersionRetention = *s.VersionRetentionEnabled
|
||||
saveSetting = true
|
||||
}
|
||||
|
||||
if s.VersionRetentionExt != nil {
|
||||
u.Settings.VersionRetentionExt = *s.VersionRetentionExt
|
||||
saveSetting = true
|
||||
}
|
||||
|
||||
if s.VersionRetentionMax != nil {
|
||||
u.Settings.VersionRetentionMax = *s.VersionRetentionMax
|
||||
saveSetting = true
|
||||
}
|
||||
|
||||
if s.CurrentPassword != nil && s.NewPassword != nil {
|
||||
if err := inventory.CheckPassword(u, *s.CurrentPassword); err != nil {
|
||||
return serializer.NewError(serializer.CodeIncorrectPassword, "Incorrect password", err)
|
||||
}
|
||||
|
||||
if _, err := userClient.UpdatePassword(c, u, *s.NewPassword); err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to update user password", err)
|
||||
}
|
||||
}
|
||||
|
||||
if s.TwoFAEnabled != nil {
|
||||
if *s.TwoFAEnabled {
|
||||
kv := dep.KV()
|
||||
secret, ok := kv.Get(fmt.Sprintf("%s%d", twoFaEnableSessionKey, u.ID))
|
||||
if !ok {
|
||||
return serializer.NewError(serializer.CodeInternalSetting, "You have not initiated 2FA session", nil)
|
||||
}
|
||||
|
||||
if !totp.Validate(*s.TwoFACode, secret.(string)) {
|
||||
return serializer.NewError(serializer.Code2FACodeErr, "Incorrect 2FA code", nil)
|
||||
}
|
||||
|
||||
if _, err := userClient.UpdateTwoFASecret(c, u, secret.(string)); err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to update user 2FA", err)
|
||||
}
|
||||
|
||||
} else {
|
||||
if !totp.Validate(*s.TwoFACode, u.TwoFactorSecret) {
|
||||
return serializer.NewError(serializer.Code2FACodeErr, "Incorrect 2FA code", nil)
|
||||
}
|
||||
|
||||
if _, err := userClient.UpdateTwoFASecret(c, u, ""); err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to update user 2FA", err)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if saveSetting {
|
||||
if err := userClient.SaveSettings(c, u); err != nil {
|
||||
return serializer.NewError(serializer.CodeDBError, "Failed to update user settings", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user