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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user