chore: 初始化 Cloudreve 源码仓库

This commit is contained in:
2026-09-13 11:28:59 +08:00
commit 2a7b03ab1d
489 changed files with 163867 additions and 0 deletions
+559
View File
@@ -0,0 +1,559 @@
package admin
import (
"context"
"path"
"strconv"
"time"
"github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/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/cloudreve/Cloudreve/v4/pkg/setting"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
)
// FileService 文件ID服务
type FileService struct {
ID uint `uri:"id" json:"id" binding:"required"`
}
// FileBatchService 文件批量操作服务
type FileBatchService struct {
ID []uint `json:"id" binding:"min=1"`
Force bool `json:"force"`
UnlinkOnly bool `json:"unlink"`
}
// ListFolderService 列目录结构
type ListFolderService struct {
Path string `uri:"path" binding:"required,max=65535"`
ID uint `uri:"id" binding:"required"`
Type string `uri:"type" binding:"eq=policy|eq=user"`
}
// 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.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)
//}
//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.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{}
}
const (
fileNameCondition = "file_name"
fileUserCondition = "file_user"
filePolicyCondition = "file_policy"
fileMetadataCondition = "file_metadata"
fileSharedCondition = "file_shared"
fileDirectLinkCondition = "file_direct_link"
)
func (service *AdminListService) Files(c *gin.Context) (*ListFileResponse, 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.LoadFileDirectLink{}, true)
var (
err error
userID int
policyID int
metadata string
shared bool
directLink bool
)
if service.Conditions[fileUserCondition] != "" {
userID, err = strconv.Atoi(service.Conditions[fileUserCondition])
if err != nil {
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid user ID", err)
}
}
if service.Conditions[filePolicyCondition] != "" {
policyID, err = strconv.Atoi(service.Conditions[filePolicyCondition])
if err != nil {
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid policy ID", err)
}
}
if service.Conditions[fileMetadataCondition] != "" {
metadata = service.Conditions[fileMetadataCondition]
}
if service.Conditions[fileSharedCondition] != "" && setting.IsTrueValue(service.Conditions[fileSharedCondition]) {
shared = true
}
if service.Conditions[fileDirectLinkCondition] != "" && setting.IsTrueValue(service.Conditions[fileDirectLinkCondition]) {
directLink = true
}
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],
HasMetadata: metadata,
Shared: shared,
HasDirectLink: directLink,
})
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list files", err)
}
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),
FileHashID: hashid.EncodeFileID(hasher, file.ID),
}
}),
}, 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),
FileHashID: hashid.EncodeFileID(hasher, file.ID),
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), dep.EncryptorFactory(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), dep.EncryptorFactory(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
}
+192
View File
@@ -0,0 +1,192 @@
package admin
import (
"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"`
}
// GroupService 用户组ID服务
type GroupService struct {
ID uint `uri:"id" json:"id" binding:"required"`
}
// Get 获取用户组详情
func (service *GroupService) Get() serializer.Response {
//group, err := model.GetGroupByID(service.ID)
//if err != nil {
// return serializer.ErrDeprecated(serializer.CodeGroupNotFound, "", err)
//}
//
//return serializer.Response{Data: group}
return serializer.Response{}
}
// 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{}
}
func (service *SingleGroupService) Delete(c *gin.Context) error {
if service.ID <= 3 {
return serializer.NewError(serializer.CodeInvalidActionOnSystemGroup, "", nil)
}
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)
}
if users > 0 {
return serializer.NewError(serializer.CodeGroupUsedByUser, strconv.Itoa(users), nil)
}
err = groupClient.Delete(c, service.ID)
if err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to delete group", err)
}
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)
}
+26
View File
@@ -0,0 +1,26 @@
package admin
import (
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
)
// AdminListService 仪表盘列条目服务
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}
return serializer.Response{}
}
+256
View File
@@ -0,0 +1,256 @@
package admin
import (
"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"
)
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
}
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 {
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 err != nil {
return "", serializer.NewError(serializer.CodeParamErr, "Failed to create downloader", err)
}
version, err := dl.Test(c)
if err != nil {
return "", serializer.NewError(serializer.CodeParamErr, "Failed to test downloader: "+err.Error(), nil)
}
return version, nil
}
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 nil, serializer.NewError(serializer.CodeDBError, "Failed to update 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)
// 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)
}
+219
View File
@@ -0,0 +1,219 @@
package admin
import (
"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"
"github.com/samber/lo"
)
// System OAuth client GUIDs that cannot be deleted
var systemOAuthClientGUIDs = []string{
inventory.OAuthClientDesktopGUID,
inventory.OAuthClientiOSGUID,
}
type (
SingleOAuthClientService struct {
ID int `uri:"id" json:"id" binding:"required"`
}
SingleOAuthClientParamCtx struct{}
)
type (
// UpsertOAuthClientRequest expands ent.OAuthClient fields to avoid json:"-"
// redaction on Secret field, which prevents it from being bound from JSON input.
UpsertOAuthClientRequest struct {
ID int `json:"id,omitempty"`
GUID string `json:"guid,omitempty"`
Secret string `json:"secret,omitempty"`
Name string `json:"name,omitempty"`
HomepageURL string `json:"homepage_url,omitempty"`
RedirectUris []string `json:"redirect_uris,omitempty"`
Scopes []string `json:"scopes,omitempty"`
Props *types.OAuthClientProps `json:"props,omitempty"`
IsEnabled bool `json:"is_enabled"`
}
UpsertOAuthClientService struct {
Client *UpsertOAuthClientRequest `json:"client" binding:"required"`
}
UpsertOAuthClientParamCtx struct{}
)
type (
BatchOAuthClientService struct {
IDs []int `json:"ids" binding:"required"`
}
BatchOAuthClientParamCtx struct{}
)
// OAuthClients lists OAuth clients with pagination
func (s *AdminListService) OAuthClients(c *gin.Context) (*ListOAuthClientResponse, error) {
dep := dependency.FromContext(c)
oauthClient := dep.OAuthClientClient()
var isEnabled *bool
if enabledStr, ok := s.Conditions["is_enabled"]; ok {
enabled := enabledStr == "true"
isEnabled = &enabled
}
res, err := oauthClient.List(c, &inventory.ListOAuthClientArgs{
PaginationArgs: &inventory.PaginationArgs{
Page: s.Page - 1,
PageSize: s.PageSize,
OrderBy: s.OrderBy,
Order: inventory.OrderDirection(s.OrderDirection),
},
Name: s.Searches["name"],
IsEnabled: isEnabled,
})
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list OAuth clients", err)
}
clients := lo.Map(res.Clients, func(client *ent.OAuthClient, _ int) GetOAuthClientResponse {
return GetOAuthClientResponse{
OAuthClient: client,
IsSystem: lo.Contains(systemOAuthClientGUIDs, client.GUID),
}
})
return &ListOAuthClientResponse{
Pagination: res.PaginationResults,
Clients: clients,
}, nil
}
func (s *SingleOAuthClientService) Get(c *gin.Context) (*GetOAuthClientResponse, error) {
dep := dependency.FromContext(c)
oauthClient := dep.OAuthClientClient()
client, err := oauthClient.GetByID(c, s.ID)
if err != nil {
return nil, serializer.NewError(serializer.CodeNotFound, "OAuth client not found", err)
}
res := &GetOAuthClientResponse{
OAuthClient: client,
IsSystem: lo.Contains(systemOAuthClientGUIDs, client.GUID),
}
// Count grants
grants, err := oauthClient.CountGrants(c, s.ID)
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to count grants", err)
}
res.TotalGrants = grants
return res, nil
}
func (r *UpsertOAuthClientRequest) toEnt() *ent.OAuthClient {
return &ent.OAuthClient{
ID: r.ID,
GUID: r.GUID,
Secret: r.Secret,
Name: r.Name,
HomepageURL: r.HomepageURL,
RedirectUris: r.RedirectUris,
Scopes: r.Scopes,
Props: r.Props,
IsEnabled: r.IsEnabled,
}
}
func (s *UpsertOAuthClientService) Create(c *gin.Context) (*GetOAuthClientResponse, error) {
dep := dependency.FromContext(c)
oauthClient := dep.OAuthClientClient()
if s.Client.ID > 0 {
return nil, serializer.NewError(serializer.CodeParamErr, "ID must be 0 for creating new OAuth client", nil)
}
client, err := oauthClient.Create(c, s.Client.toEnt())
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to create OAuth client", err)
}
service := &SingleOAuthClientService{ID: client.ID}
return service.Get(c)
}
func (s *UpsertOAuthClientService) Update(c *gin.Context) (*GetOAuthClientResponse, error) {
dep := dependency.FromContext(c)
oauthClient := dep.OAuthClientClient()
if s.Client.ID == 0 {
return nil, serializer.NewError(serializer.CodeParamErr, "ID is required", nil)
}
// Check if this is a system client
existing, err := oauthClient.GetByID(c, s.Client.ID)
if err != nil {
return nil, serializer.NewError(serializer.CodeNotFound, "OAuth client not found", err)
}
// System clients cannot change GUID
if lo.Contains(systemOAuthClientGUIDs, existing.GUID) {
s.Client.GUID = existing.GUID
}
_, err = oauthClient.Update(c, s.Client.toEnt())
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to update OAuth client", err)
}
service := &SingleOAuthClientService{ID: s.Client.ID}
return service.Get(c)
}
func (s *SingleOAuthClientService) Delete(c *gin.Context) error {
dep := dependency.FromContext(c)
oauthClient := dep.OAuthClientClient()
// Check if client exists
client, err := oauthClient.GetByID(c, s.ID)
if err != nil {
return serializer.NewError(serializer.CodeNotFound, "OAuth client not found", err)
}
// Check if this is a system client
if lo.Contains(systemOAuthClientGUIDs, client.GUID) {
return serializer.NewError(serializer.CodeInvalidActionOnSystemGroup, "Cannot delete system OAuth client", nil)
}
err = oauthClient.Delete(c, s.ID)
if err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to delete OAuth client", err)
}
return nil
}
func (s *BatchOAuthClientService) Delete(c *gin.Context) error {
dep := dependency.FromContext(c)
oauthClient := dep.OAuthClientClient()
for _, id := range s.IDs {
// Check if client exists
client, err := oauthClient.GetByID(c, id)
if err != nil {
continue // Skip non-existent clients
}
// Check if this is a system client
if lo.Contains(systemOAuthClientGUIDs, client.GUID) {
continue // Skip system clients
}
// Delete the client (including grants)
oauthClient.Delete(c, id)
}
return nil
}
+527
View File
@@ -0,0 +1,527 @@
package admin
import (
"context"
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"time"
"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/ks3"
"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"
)
// PathTestService 本地路径测试服务
type PathTestService struct {
Path string `json:"path" binding:"required"`
}
// SlaveTestService 从机测试服务
type SlaveTestService struct {
Secret string `json:"secret" binding:"required"`
Server string `json:"server" 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"`
}
// PolicyService 存储策略ID服务
type PolicyService struct {
ID uint `uri:"id" json:"id" binding:"required"`
Region string `json:"region"`
}
// Delete 删除存储策略
func (service *SingleStoragePolicyService) Delete(c *gin.Context) error {
// 禁止删除默认策略
if service.ID == 1 {
return serializer.NewError(serializer.CodeDeleteDefaultPolicy, "", nil)
}
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.NewError(serializer.CodePolicyNotExist, "", err)
}
// 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)
}
used, err := dep.FileClient().IsStoragePolicyUsedByEntities(ctx, service.ID)
if err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to check if policy is used by entities", err)
}
if used {
return serializer.NewError(serializer.CodePolicyUsedByFiles, "", nil)
}
err = storagePolicyClient.Delete(ctx, policy)
if err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to delete policy", err)
}
return nil
}
// Test 从机响应ping
func (service *SlavePingService) Test(c *gin.Context) error {
master, err := url.Parse(service.Callback)
if err != nil {
return serializer.NewError(serializer.CodeParamErr, "Failed to parse callback url", err)
}
dep := dependency.FromContext(c)
r := dep.RequestClient()
res, err := r.Request(
"GET",
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.NewError(serializer.CodeSlavePingMaster, err.Error(), 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 nil
}
// Test 测试从机通信
func (service *SlaveTestService) Test() serializer.Response {
//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{}
}
// 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.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(c *gin.Context) (*ListPolicyResponse, error) {
dep := dependency.FromContext(c)
storagePolicyClient := dep.StoragePolicyClient()
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)
}
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
sc, tx, ctx, err := inventory.WithTx(c, storagePolicyClient)
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to create transaction", err)
}
_, err = sc.Upsert(ctx, service.Policy)
if err != nil {
_ = inventory.Rollback(tx)
return nil, serializer.NewError(serializer.CodeDBError, "Failed to update policy", err)
}
if err := inventory.Commit(tx); err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to commit transaction", 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.PolicyTypeKs3:
handler, err := ks3.New(c, service.Policy, dep.SettingProvider(), dep.ConfigProvider(), dep.Logger(), dep.MimeDetector(c))
if err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to create ks3 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.CodeParamErr, "Failed to obtain token: "+err.Error(), 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
}
+154
View File
@@ -0,0 +1,154 @@
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"`
TaskHashID string `json:"task_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"`
FileHashID string `json:"file_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 ListOAuthClientResponse struct {
Clients []GetOAuthClientResponse `json:"clients"`
Pagination *inventory.PaginationResults `json:"pagination"`
}
type GetOAuthClientResponse struct {
*ent.OAuthClient
IsSystem bool `json:"is_system"`
TotalGrants int `json:"total_grants,omitempty"`
}
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{})
}
+161
View File
@@ -0,0 +1,161 @@
package admin
import (
"context"
"strconv"
"strings"
"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"
)
const (
shareUserIDCondition = "share_user_id"
shareFileIDCondition = "share_file_id"
shareIDCondition = "share_id"
)
func (s *AdminListService) Shares(c *gin.Context) (*ListShareResponse, error) {
dep := dependency.FromContext(c)
shareClient := dep.ShareClient()
hasher := dep.HashIDEncoder()
var (
err error
userID int
fileID int
shareIDs []int
)
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)
}
}
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)
}
}
if s.Conditions[shareIDCondition] != "" {
shareIdStrs := strings.Split(s.Conditions[shareIDCondition], ",")
for _, shareIdStr := range shareIdStrs {
shareID, err := strconv.Atoi(shareIdStr)
if err != nil {
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid share ID", err)
}
shareIDs = append(shareIDs, shareID)
}
}
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,
ShareIDs: shareIDs,
})
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list shares", err)
}
siteUrl := dep.SettingProvider().SiteURL(c)
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
}
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.EncodeUserID(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
}
+433
View File
@@ -0,0 +1,433 @@
package admin
import (
"context"
"encoding/gob"
"encoding/json"
"fmt"
"net/url"
"reflect"
"strings"
"time"
"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() {
gob.Register(map[string]interface{}{})
gob.Register(map[string]string{})
}
// NoParamService 无需参数的服务
type NoParamService struct {
}
// BatchSettingChangeService 设定批量更改服务
type BatchSettingChangeService struct {
Options []SettingChangeService `json:"options"`
}
// SettingChangeService 设定更改服务
type SettingChangeService struct {
Key string `json:"key" binding:"required"`
Value string `json:"value"`
}
// 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.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 (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 summary, ok := kv.Get(MetricCacheKey); ok {
summaryCasted := summary.(MetricsSummary)
res.MetricsSummary = &summaryCasted
return res, nil
}
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 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
}
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)
}
_ = 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"`
}
ThumbGeneratorTestParamCtx struct{}
)
// Test 通过获取生成器版本来测试
func (s *ThumbGeneratorTestService) Test(c *gin.Context) (string, error) {
version, err := thumb.TestGenerator(c, s.Name, s.Executable)
if err != nil {
return "", serializer.NewError(serializer.CodeParamErr, "Failed to invoke generator: "+err.Error(), err)
}
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 {
_, ok := inventory.RedactedSettings[strings.ToLower(item)]
return !ok
}))
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,
"fts_meilisearch_embed_config": meilisearchPostProcessor,
"fts_meilisearch_endpoint": meilisearchPostProcessor,
"fts_meilisearch_api_key": meilisearchPostProcessor,
"fts_meilisearch_embed_enabled": meilisearchPostProcessor,
"fts_meilisearch_page_size": meilisearchPostProcessor,
"fts_tika_endpoint": tikaPostProcessor,
"fts_tika_exts": tikaPostProcessor,
"fts_tika_max_file_size": tikaPostProcessor,
}
)
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.RandStringRunesCrypto(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
}
func meilisearchPostProcessor(ctx context.Context, settings map[string]string) error {
dep := dependency.FromContext(ctx)
dep.SearchIndexer(context.WithValue(ctx, dependency.ReloadCtx{}, true))
return nil
}
func tikaPostProcessor(ctx context.Context, settings map[string]string) error {
dep := dependency.FromContext(ctx)
dep.TextExtractor(context.WithValue(ctx, dependency.ReloadCtx{}, true))
return nil
}
+282
View File
@@ -0,0 +1,282 @@
package admin
import (
"context"
"strconv"
"time"
"github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/ent/task"
"github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/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"
)
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
}
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,
})
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to list tasks", err)
}
tasks := make([]queue.Task, 0, len(res.Tasks))
nodeMap := make(map[int]*ent.Node)
for _, t := range res.Tasks {
task, err := queue.NewTaskFromModel(t)
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to parse task", err)
}
summary := task.Summarize(hasher)
if summary != nil && summary.NodeID > 0 {
if _, ok := nodeMap[summary.NodeID]; !ok {
nodeMap[summary.NodeID] = nil
}
}
tasks = append(tasks, task)
}
// Get nodes
nodes, err := dep.NodeClient().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
}
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
)
if task.Edges.User != nil {
uid = hashid.EncodeUserID(hasher, task.Edges.User.ID)
}
t := tasks[i]
summary = t.Summarize(hasher)
if summary != nil && summary.NodeID > 0 {
node = nodeMap[summary.NodeID]
}
return GetTaskResponse{
Task: task,
TaskHashID: hashid.EncodeTaskID(hasher, task.ID),
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,
TaskHashID: hashid.EncodeTaskID(hasher, task.ID),
}, 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
}
type (
CleanupTaskService struct {
NotAfter time.Time `json:"not_after" binding:"required"`
Types []string `json:"types"`
Status []task.Status `json:"status"`
}
CleanupTaskParameterCtx struct{}
)
func (s *CleanupTaskService) CleanupTask(c *gin.Context) error {
dep := dependency.FromContext(c)
taskClient := dep.TaskClient()
if len(s.Status) == 0 {
s.Status = []task.Status{task.StatusCanceled, task.StatusCompleted, task.StatusError}
}
if err := taskClient.DeleteBy(c, &inventory.DeleteTaskArgs{
NotAfter: s.NotAfter,
Types: s.Types,
Status: s.Status,
}); err != nil {
return serializer.NewError(serializer.CodeDBError, "Failed to cleanup tasks", err)
}
return nil
}
+185
View File
@@ -0,0 +1,185 @@
package admin
import (
"encoding/hex"
"errors"
"net/http"
"strconv"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"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/wneessen/go-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) (*types.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)
}
opts := []mail.Option{
mail.WithPort(port),
mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover), mail.WithTLSPortPolicy(mail.TLSOpportunistic),
mail.WithUsername(s.Settings["smtpUser"]), mail.WithPassword(s.Settings["smtpPass"]),
}
if setting.IsTrueValue(s.Settings["smtpEncryption"]) {
opts = append(opts, mail.WithSSL())
}
d, diaErr := mail.NewClient(s.Settings["smtpHost"], opts...)
if diaErr != nil {
return serializer.NewError(serializer.CodeInternalSetting, "Failed to create SMTP client: "+diaErr.Error(), diaErr)
}
m := mail.NewMsg()
if err := m.FromFormat(s.Settings["fromName"], s.Settings["fromAdress"]); err != nil {
return serializer.NewError(serializer.CodeInternalSetting, "Failed to set FROM address: "+err.Error(), err)
}
m.ReplyToFormat(s.Settings["fromName"], s.Settings["replyTo"])
m.To(s.To)
m.Subject("Cloudreve SMTP Test")
m.SetMessageID()
m.SetBodyString(mail.TypeTextHTML, "This is a test email from Cloudreve.")
err = d.DialAndSendWithContext(c, m)
if err != nil {
// Check if this is an SMTP RESET error after successful delivery
var sendErr *mail.SendError
var errParsed = errors.As(err, &sendErr)
if errParsed && sendErr.Reason == mail.ErrSMTPReset {
return nil // Don't treat this as a delivery failure since mail was sent
}
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)
}
+254
View File
@@ -0,0 +1,254 @@
package admin
import (
"context"
"strconv"
"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"`
}
// UserService 用户ID服务
type UserService struct {
ID uint `uri:"id" json:"id" binding:"required"`
}
// UserBatchService 用户批量操作服务
type UserBatchService struct {
ID []uint `json:"id" binding:"min=1"`
}
const (
userStatusCondition = "user_status"
userGroupCondition = "user_group"
userNickCondition = "user_nick"
userEmailCondition = "user_email"
)
func (service *AdminListService) Users(c *gin.Context) (*ListUserResponse, 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)
var (
err error
groupID int
)
if service.Conditions[userGroupCondition] != "" {
groupID, err = strconv.Atoi(service.Conditions[userGroupCondition])
if err != nil {
return nil, serializer.NewError(serializer.CodeParamErr, "Invalid group ID", err)
}
}
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 nil, serializer.NewError(serializer.CodeDBError, "Failed to list users", err)
}
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 != "",
}
}),
}, nil
}
type (
SingleUserService struct {
ID int `uri:"id" json:"id" binding:"required"`
}
SingleUserParamCtx struct{}
)
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)
}
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)
}
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)
}
}
if s.Password != "" && len(s.Password) > 128 {
return nil, serializer.NewError(serializer.CodeParamErr, "Password too long", 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()
}