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