From 93010e3525948eef7bb92545fc80ef5d736529b6 Mon Sep 17 00:00:00 2001 From: HFO4 <912394456@qq.com> Date: Sat, 30 Nov 2019 15:09:56 +0800 Subject: [PATCH] Feat: delete objects --- conf/conf.ini | 2 +- middleware/session.go | 1 + models/file.go | 66 ++++++++++++++++++- models/folder.go | 14 ++++ models/migration.go | 8 +-- models/policy.go | 25 ++++++- models/policy_test.go | 8 +-- models/user.go | 2 +- models/user_test.go | 10 +-- pkg/filesystem/errors.go | 1 + pkg/filesystem/file.go | 50 +++++++++++++- pkg/filesystem/file_test.go | 2 +- pkg/filesystem/filesystem.go | 26 +++++++- pkg/filesystem/local/handler.go | 11 ++-- pkg/filesystem/path.go | 110 +++++++++++++++++++++++++++++++ pkg/filesystem/validator.go | 2 +- pkg/serializer/error.go | 2 + pkg/util/common.go | 13 ++++ routers/controllers/directory.go | 2 +- routers/controllers/file.go | 2 +- routers/controllers/objects.go | 22 +++++++ routers/router.go | 8 ++- routers/router_test.go | 26 ++------ service/explorer/directory.go | 4 +- service/explorer/file.go | 2 +- service/explorer/objects.go | 34 ++++++++++ 26 files changed, 398 insertions(+), 55 deletions(-) create mode 100644 routers/controllers/objects.go create mode 100644 service/explorer/objects.go diff --git a/conf/conf.ini b/conf/conf.ini index 3f9c0fc..5aac817 100644 --- a/conf/conf.ini +++ b/conf/conf.ini @@ -12,7 +12,7 @@ TablePrefix = v3_ [Redis] Server = 127.0.0.1:6379 -Password = 52121225 +Password = DB = 0 [Captcha] diff --git a/middleware/session.go b/middleware/session.go index 20568f8..3a80e8b 100644 --- a/middleware/session.go +++ b/middleware/session.go @@ -28,6 +28,7 @@ func Session(secret string) gin.HandlerFunc { } // Also set Secure: true if using SSL, you should though + // TODO:same-site policy Store.Options(sessions.Options{HttpOnly: true, MaxAge: 7 * 86400, Path: "/"}) return sessions.Sessions("cloudreve-session", Store) } diff --git a/models/file.go b/models/file.go index 098baeb..3ec428e 100644 --- a/models/file.go +++ b/models/file.go @@ -3,6 +3,7 @@ package model import ( "github.com/HFO4/cloudreve/pkg/util" "github.com/jinzhu/gorm" + "path" ) // File 文件 @@ -31,7 +32,7 @@ func (file *File) Create() (uint, error) { return file.ID, nil } -// GetFileByPathAndName 给定路径、文件名、用户ID,查找文件 +// GetFileByPathAndName 给定路径(s)、文件名、用户ID,查找文件 func GetFileByPathAndName(path string, name string, uid uint) (File, error) { var file File result := DB.Where("user_id = ? AND dir = ? AND name=?", uid, path, name).First(&file) @@ -45,6 +46,20 @@ func (folder *Folder) GetChildFile() ([]File, error) { return files, result.Error } +// GetChildFilesOfFolders 批量检索目录子文件 +func GetChildFilesOfFolders(folders *[]Folder) ([]File, error) { + // 将所有待删除目录ID抽离,以便检索文件 + folderIDs := make([]uint, 0, len(*folders)) + for _, value := range *folders { + folderIDs = append(folderIDs, value.ID) + } + + // 检索文件 + var files []File + result := DB.Where("folder_id in (?)", folderIDs).Find(&files) + return files, result.Error +} + // GetPolicy 获取文件所属策略 // TODO:test func (file *File) GetPolicy() *Policy { @@ -53,3 +68,52 @@ func (file *File) GetPolicy() *Policy { } return &file.Policy } + +// GetFileByPaths 根据给定的文件路径(s)查找文件 +func GetFileByPaths(paths []string, uid uint) ([]File, error) { + var files []File + tx := DB + for _, value := range paths { + base := path.Base(value) + dir := path.Dir(value) + tx = tx.Or("dir = ? and name = ? and user_id = ?", dir, base, uid) + } + result := tx.Find(&files) + return files, result.Error +} + +// RemoveFilesWithSoftLinks 去除给定的文件列表中有软链接的文件 +func RemoveFilesWithSoftLinks(files []File) ([]File, error) { + // 结果值 + filteredFiles := make([]File, 0) + + // 查询软链接的文件 + var filesWithSoftLinks []File + tx := DB + for _, value := range files { + tx = tx.Or("source_name = ? and policy_id = ? and id != ?", value.SourceName, value.GetPolicy().ID, value.ID) + } + result := tx.Find(&filesWithSoftLinks) + if result.Error != nil { + return nil, result.Error + } + + // 过滤具有软连接的文件 + for i := 0; i < len(files); i++ { + for _, value := range filesWithSoftLinks { + if value.PolicyID != files[i].PolicyID || value.SourceName != files[i].SourceName { + filteredFiles = append(filteredFiles, files[i]) + break + } + } + } + + return filteredFiles, nil + +} + +// DeleteFileByIDs 根据给定ID批量删除文件记录 +func DeleteFileByIDs(ids []uint) error { + result := DB.Where("id in (?)", ids).Delete(&File{}) + return result.Error +} diff --git a/models/folder.go b/models/folder.go index 28c56a7..7a7ce3b 100644 --- a/models/folder.go +++ b/models/folder.go @@ -38,3 +38,17 @@ func (folder *Folder) GetChildFolder() ([]Folder, error) { result := DB.Where("parent_id = ?", folder.ID).Find(&folders) return folders, result.Error } + +// GetRecursiveChildFolder 查找所有递归子目录 +func GetRecursiveChildFolder(dirs []string, uid uint) ([]Folder, error) { + folders := make([]Folder, 0, len(dirs)) + search := util.BuildRegexp(dirs, "^", "/", "|") + result := DB.Where("(owner_id = ? and position_absolute REGEXP ?) or position_absolute in (?)", uid, search, dirs).Find(&folders) + return folders, result.Error +} + +// DeleteFolderByIDs 根据给定ID批量删除目录记录 +func DeleteFolderByIDs(ids []uint) error { + result := DB.Where("id in (?)", ids).Delete(&Folder{}) + return result.Error +} diff --git a/models/migration.go b/models/migration.go index 52ba95a..a065fa0 100644 --- a/models/migration.go +++ b/models/migration.go @@ -54,17 +54,17 @@ func migration() { } func addDefaultPolicy() { - _, err := GetPolicyByID(1) + _, err := GetPolicyByID(uint(1)) // 未找到初始存储策略时,则创建 if gorm.IsRecordNotFoundError(err) { defaultPolicy := Policy{ - Name: "默认上传策略", + Name: "默认存储策略", Type: "local", - Server: "/Api/V3/File/Upload", + Server: "/api/v3/file/upload", BaseURL: "http://cloudreve.org/public/uploads/", MaxSize: 10 * 1024 * 1024 * 1024, AutoRename: true, - DirNameRule: "{date}/{uid}", + DirNameRule: "uploads/{uid}/{path}", FileNameRule: "{uid}_{randomkey8}_{originname}", IsOriginLinkEnable: false, } diff --git a/models/policy.go b/models/policy.go index de82f15..00e389e 100644 --- a/models/policy.go +++ b/models/policy.go @@ -6,6 +6,7 @@ import ( "github.com/jinzhu/gorm" "path/filepath" "strconv" + "sync" "time" ) @@ -41,16 +42,36 @@ type PolicyOption struct { RangeTransferEnabled bool `json:"range_transfer_enabled"` } +// 存储策略缓存,部分情况下需要频繁查询存储策略 +var policyCache = make(map[uint]Policy) +var rw sync.RWMutex + // GetPolicyByID 用ID获取存储策略 func GetPolicyByID(ID interface{}) (Policy, error) { + // 尝试读取缓存 + rw.RLock() + if policy, ok := policyCache[ID.(uint)]; ok { + rw.RUnlock() + return policy, nil + } + rw.RUnlock() + var policy Policy result := DB.First(&policy, ID) + + // 写入缓存 + if result.Error == nil { + rw.Lock() + policyCache[policy.ID] = policy + rw.Unlock() + } + return policy, result.Error } -// AfterFind 找到上传策略后的钩子 +// AfterFind 找到存储策略后的钩子 func (policy *Policy) AfterFind() (err error) { - // 解析上传策略设置到OptionsSerialized + // 解析存储策略设置到OptionsSerialized err = json.Unmarshal([]byte(policy.Options), &policy.OptionsSerialized) if policy.OptionsSerialized.FileType == nil { policy.OptionsSerialized.FileType = []string{} diff --git a/models/policy_test.go b/models/policy_test.go index 87efbd6..9ec34f7 100644 --- a/models/policy_test.go +++ b/models/policy_test.go @@ -13,16 +13,16 @@ func TestGetPolicyByID(t *testing.T) { asserts := assert.New(t) rows := sqlmock.NewRows([]string{"name", "type", "options"}). - AddRow("默认上传策略", "local", "{\"op_name\":\"123\"}") + AddRow("默认存储策略", "local", "{\"op_name\":\"123\"}") mock.ExpectQuery("^SELECT \\* FROM `(.+)` WHERE `(.+)`\\.`deleted_at` IS NULL AND \\(\\(`policies`.`id` = 1\\)\\)(.+)$").WillReturnRows(rows) - policy, err := GetPolicyByID(1) + policy, err := GetPolicyByID(uint(1)) asserts.NoError(err) - asserts.Equal("默认上传策略", policy.Name) + asserts.Equal("默认存储策略", policy.Name) asserts.Equal("123", policy.OptionsSerialized.OPName) rows = sqlmock.NewRows([]string{"name", "type", "options"}) mock.ExpectQuery("^SELECT \\* FROM `(.+)` WHERE `(.+)`\\.`deleted_at` IS NULL AND \\(\\(`policies`.`id` = 1\\)\\)(.+)$").WillReturnRows(rows) - policy, err = GetPolicyByID(1) + policy, err = GetPolicyByID(uint(1)) asserts.Error(err) } diff --git a/models/user.go b/models/user.go index f0b28cd..d59c132 100644 --- a/models/user.go +++ b/models/user.go @@ -83,7 +83,7 @@ func (user *User) GetRemainingCapacity() uint64 { return user.Group.MaxStorage - user.Storage } -// GetPolicyID 获取用户当前的上传策略ID +// GetPolicyID 获取用户当前的存储策略ID func (user *User) GetPolicyID() uint { // 用户未指定时,返回可用的第一个 if user.OptionsSerialized.PreferredPolicy == 0 { diff --git a/models/user_test.go b/models/user_test.go index 0dd720c..e15a57a 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -22,7 +22,7 @@ func TestGetUserByID(t *testing.T) { mock.ExpectQuery("^SELECT (.+)").WillReturnRows(groupRows) policyRows := sqlmock.NewRows([]string{"id", "name"}). - AddRow(1, "默认上传策略") + AddRow(1, "默认存储策略") mock.ExpectQuery("^SELECT (.+)").WillReturnRows(policyRows) user, err := GetUserByID(1) @@ -50,7 +50,7 @@ func TestGetUserByID(t *testing.T) { OptionsSerialized: PolicyOption{ FileType: []string{}, }, - Name: "默认上传策略", + Name: "默认存储策略", }, }, user) @@ -105,7 +105,7 @@ func TestUser_AfterFind(t *testing.T) { asserts := assert.New(t) policyRows := sqlmock.NewRows([]string{"id", "name"}). - AddRow(1, "默认上传策略") + AddRow(1, "默认存储策略") mock.ExpectQuery("^SELECT (.+)").WillReturnRows(policyRows) newUser := NewUser() @@ -117,7 +117,7 @@ func TestUser_AfterFind(t *testing.T) { asserts.NoError(err) asserts.NoError(mock.ExpectationsWereMet()) asserts.Equal(expected, newUser.OptionsSerialized) - asserts.Equal("默认上传策略", newUser.Policy.Name) + asserts.Equal("默认存储策略", newUser.Policy.Name) } func TestUser_BeforeSave(t *testing.T) { @@ -199,7 +199,7 @@ func TestUser_DeductionCapacity(t *testing.T) { mock.ExpectQuery("^SELECT (.+)").WillReturnRows(groupRows) policyRows := sqlmock.NewRows([]string{"id", "name"}). - AddRow(1, "默认上传策略") + AddRow(1, "默认存储策略") mock.ExpectQuery("^SELECT (.+)").WillReturnRows(policyRows) newUser, err := GetUserByID(1) diff --git a/pkg/filesystem/errors.go b/pkg/filesystem/errors.go index 078cf5c..dac770b 100644 --- a/pkg/filesystem/errors.go +++ b/pkg/filesystem/errors.go @@ -17,4 +17,5 @@ var ( ErrPathNotExist = serializer.NewError(404, "路径不存在", nil) ErrObjectNotExist = serializer.NewError(404, "文件不存在", nil) ErrIO = serializer.NewError(serializer.CodeIOFailed, "无法读取文件数据", nil) + ErrDBListObjects = serializer.NewError(serializer.CodeDBError, "无法列取对象记录", nil) ) diff --git a/pkg/filesystem/file.go b/pkg/filesystem/file.go index 590af48..ec850f1 100644 --- a/pkg/filesystem/file.go +++ b/pkg/filesystem/file.go @@ -85,7 +85,7 @@ func (fs *FileSystem) GetContent(ctx context.Context, path string) (io.ReadSeeke if !exist { return nil, ErrObjectNotExist } - fs.Target = &file + fs.FileTarget = []model.File{file} // 将当前存储策略重设为文件使用的 fs.Policy = file.GetPolicy() @@ -102,3 +102,51 @@ func (fs *FileSystem) GetContent(ctx context.Context, path string) (io.ReadSeeke return rs, nil } + +// deleteGroupedFile 对分组好的文件执行删除操作, +// 返回每个分组失败的文件列表 +func (fs *FileSystem) deleteGroupedFile(ctx context.Context, files map[uint][]*model.File) map[uint][]string { + // 失败的文件列表 + failed := make(map[uint][]string, len(files)) + + for policyID, toBeDeletedFiles := range files { + // 列举出需要物理删除的文件的物理路径 + sourceNames := make([]string, 0, len(toBeDeletedFiles)) + for i := 0; i < len(toBeDeletedFiles); i++ { + sourceNames = append(sourceNames, toBeDeletedFiles[i].SourceName) + } + + // 切换上传策略 + fs.Policy = toBeDeletedFiles[0].GetPolicy() + err := fs.dispatchHandler() + if err != nil { + failed[policyID] = sourceNames + continue + } + + // 执行删除 + failedFile, _ := fs.Handler.Delete(ctx, sourceNames) + failed[policyID] = failedFile + + } + + return failed +} + +// GroupFileByPolicy 将目标文件按照存储策略分组 +func (fs *FileSystem) GroupFileByPolicy(ctx context.Context, files []model.File) map[uint][]*model.File { + var policyGroup = make(map[uint][]*model.File) + + for key := range files { + if file, ok := policyGroup[files[key].GetPolicy().ID]; ok { + // 如果已存在分组,直接追加 + policyGroup[files[key].GetPolicy().ID] = append(file, &files[key]) + } else { + // 分布不存在,创建 + policyGroup[files[key].GetPolicy().ID] = make([]*model.File, 0) + policyGroup[files[key].GetPolicy().ID] = append(policyGroup[files[key].GetPolicy().ID], &files[key]) + } + } + + return policyGroup +} diff --git a/pkg/filesystem/file_test.go b/pkg/filesystem/file_test.go index df0ec8d..e47bfc4 100644 --- a/pkg/filesystem/file_test.go +++ b/pkg/filesystem/file_test.go @@ -74,7 +74,7 @@ func TestFileSystem_GetContent(t *testing.T) { asserts.Equal(ErrObjectNotExist, err) asserts.Nil(rs) - // 未知上传策略 + // 未知存储策略 file, err := os.Create("TestFileSystem_GetContent.txt") asserts.NoError(err) _ = file.Close() diff --git a/pkg/filesystem/filesystem.go b/pkg/filesystem/filesystem.go index c452dc4..ff4afd2 100644 --- a/pkg/filesystem/filesystem.go +++ b/pkg/filesystem/filesystem.go @@ -33,10 +33,12 @@ type Handler interface { type FileSystem struct { // 文件系统所有者 User *model.User - // 操作文件使用的上传策略 + // 操作文件使用的存储策略 Policy *model.Policy // 当前正在处理的文件对象 - Target *model.File + FileTarget []model.File + // 当前正在处理的目录对象 + DirTarget []model.Folder /* 钩子函数 @@ -100,3 +102,23 @@ func NewFileSystemFromContext(c *gin.Context) (*FileSystem, error) { fs, err := NewFileSystem(user.(*model.User)) return fs, err } + +// SetTargetFile 设置当前处理的目标文件 +func (fs *FileSystem) SetTargetFile(files *[]model.File) { + if len(fs.FileTarget) == 0 { + fs.FileTarget = *files + } else { + fs.FileTarget = append(fs.FileTarget, *files...) + } + +} + +// SetTargetDir 设置当前处理的目标目录 +func (fs *FileSystem) SetTargetDir(dirs *[]model.Folder) { + if len(fs.DirTarget) == 0 { + fs.DirTarget = *dirs + } else { + fs.DirTarget = append(fs.DirTarget, *dirs...) + } + +} diff --git a/pkg/filesystem/local/handler.go b/pkg/filesystem/local/handler.go index 9dab184..23757f4 100644 --- a/pkg/filesystem/local/handler.go +++ b/pkg/filesystem/local/handler.go @@ -63,20 +63,19 @@ func (handler Handler) Put(ctx context.Context, file io.ReadCloser, dst string, } // Delete 删除一个或多个文件, -// 返回已删除的文件,及遇到的最后一个错误 +// 返回未删除的文件,及遇到的最后一个错误 func (handler Handler) Delete(ctx context.Context, files []string) ([]string, error) { - deleted := make([]string, 0, len(files)) + deleteFailed := make([]string, 0, len(files)) var retErr error for _, value := range files { err := os.Remove(value) - if err == nil { - deleted = append(deleted, value) - } else { + if err != nil { util.Log().Warning("无法删除文件,%s", err) retErr = err + deleteFailed = append(deleteFailed, value) } } - return deleted, retErr + return deleteFailed, retErr } diff --git a/pkg/filesystem/path.go b/pkg/filesystem/path.go index 7434a93..ae8892e 100644 --- a/pkg/filesystem/path.go +++ b/pkg/filesystem/path.go @@ -2,7 +2,10 @@ package filesystem import ( "context" + "fmt" model "github.com/HFO4/cloudreve/models" + "github.com/HFO4/cloudreve/pkg/serializer" + "github.com/HFO4/cloudreve/pkg/util" "path" ) @@ -22,6 +25,113 @@ type Object struct { Date string `json:"date"` } +// Delete 递归删除对象 +func (fs *FileSystem) Delete(ctx context.Context, dirs, files []string) error { + // 已删除的总容量,map用于去重 + var deletedStorage = make(map[uint]uint64) + // 已删除的文件ID + var deletedFileIDs = make([]uint, 0, len(fs.FileTarget)) + // 删除失败的文件的父目录ID + + // 所有文件的ID + var allFileIDs = make([]uint, 0, len(fs.FileTarget)) + + // 列出要删除的目录 + if len(dirs) > 0 { + err := fs.ListDeleteDirs(ctx, dirs) + if err != nil { + return err + } + } + + // 列出要删除的文件 + if len(files) > 0 { + err := fs.ListDeleteFiles(ctx, files) + if err != nil { + return err + } + // 去除待删除文件中包含软连接的部分 + filesToBeDelete, err := model.RemoveFilesWithSoftLinks(fs.FileTarget) + if err != nil { + return ErrDBListObjects.WithError(err) + } + + // 根据存储策略将文件分组 + policyGroup := fs.GroupFileByPolicy(ctx, filesToBeDelete) + + // 按照存储策略分组删除对象 + failed := fs.deleteGroupedFile(ctx, policyGroup) + + for i := 0; i < len(fs.FileTarget); i++ { + if util.ContainsString(failed[fs.FileTarget[i].PolicyID], fs.FileTarget[i].SourceName) { + // TODO 删除失败时不删除文件记录及父目录 + } else { + deletedFileIDs = append(deletedFileIDs, fs.FileTarget[i].ID) + deletedStorage[fs.FileTarget[i].ID] = fs.FileTarget[i].Size + } + allFileIDs = append(allFileIDs, fs.FileTarget[i].ID) + } + } + + // 删除文件记录 + err := model.DeleteFileByIDs(allFileIDs) + if err != nil { + return ErrDBListObjects.WithError(err) + } + + // 归还容量 + var total uint64 + for _, value := range deletedStorage { + total += value + } + fs.User.IncreaseStorage(total) + + // 删除目录 + var allFolderIDs = make([]uint, 0, len(fs.DirTarget)) + for _, value := range fs.DirTarget { + allFolderIDs = append(allFolderIDs, value.ID) + } + err = model.DeleteFolderByIDs(allFolderIDs) + if err != nil { + return ErrDBListObjects.WithError(err) + } + + if notDeleted := len(fs.FileTarget) - len(deletedFileIDs); notDeleted > 0 { + return serializer.NewError(serializer.CodeNotFullySuccess, fmt.Sprintf("有 %d 个文件未能成功删除,已删除它们的文件记录", notDeleted), nil) + } + + return nil +} + +// ListDeleteDirs 递归列出要删除目录,及目录下所有文件 +func (fs *FileSystem) ListDeleteDirs(ctx context.Context, dirs []string) error { + // 列出所有递归子目录 + folders, err := model.GetRecursiveChildFolder(dirs, fs.User.ID) + if err != nil { + return ErrDBListObjects.WithError(err) + } + fs.SetTargetDir(&folders) + + // 检索目录下的子文件 + files, err := model.GetChildFilesOfFolders(&folders) + if err != nil { + return ErrDBListObjects.WithError(err) + } + fs.SetTargetFile(&files) + + return nil +} + +// ListDeleteFiles 根据给定的路径列出要删除的文件 +func (fs *FileSystem) ListDeleteFiles(ctx context.Context, paths []string) error { + files, err := model.GetFileByPaths(paths, fs.User.ID) + if err != nil { + return ErrDBListObjects.WithError(err) + } + fs.SetTargetFile(&files) + return nil +} + // List 列出路径下的内容, // pathProcessor为最终对象路径的处理钩子。 // 有些情况下(如在分享页面列对象)时, diff --git a/pkg/filesystem/validator.go b/pkg/filesystem/validator.go index d3751dd..b220b81 100644 --- a/pkg/filesystem/validator.go +++ b/pkg/filesystem/validator.go @@ -13,7 +13,7 @@ import ( */ // 文件/路径名保留字符 -var reservedCharacter = []string{"\\", "?", "*", "<", "\"", ":", ">", "/"} +var reservedCharacter = []string{"\\", "?", "*", "<", "\"", ":", ">", "/", "|"} // ValidateLegalName 验证文件名/文件夹名是否合法 func (fs *FileSystem) ValidateLegalName(ctx context.Context, name string) bool { diff --git a/pkg/serializer/error.go b/pkg/serializer/error.go index c109afd..f91b2f7 100644 --- a/pkg/serializer/error.go +++ b/pkg/serializer/error.go @@ -42,6 +42,8 @@ func (err AppError) Error() string { // 五开头的五位数错误编码为服务器端错误,比如数据库操作失败 // 四开头的五位数错误编码为客户端错误,有时候是客户端代码写错了,有时候是用户操作错误 const ( + // CodeNotFullySuccess 未完全成功 + CodeNotFullySuccess = 203 // CodeCheckLogin 未登录 CodeCheckLogin = 401 // CodeNoRightErr 未授权访问 diff --git a/pkg/util/common.go b/pkg/util/common.go index ff974d8..1f72510 100644 --- a/pkg/util/common.go +++ b/pkg/util/common.go @@ -2,6 +2,7 @@ package util import ( "math/rand" + "regexp" "strings" ) @@ -43,3 +44,15 @@ func Replace(table map[string]string, s string) string { } return s } + +// BuildRegexp 构建用于SQL查询用的多条件正则 +func BuildRegexp(search []string, prefix, suffix, condition string) string { + var res string + for key, value := range search { + res += prefix + regexp.QuoteMeta(value) + suffix + if key < len(search)-1 { + res += condition + } + } + return res +} diff --git a/routers/controllers/directory.go b/routers/controllers/directory.go index 9486840..74f84f1 100644 --- a/routers/controllers/directory.go +++ b/routers/controllers/directory.go @@ -19,7 +19,7 @@ func CreateDirectory(c *gin.Context) { // ListDirectory 列出目录下内容 func ListDirectory(c *gin.Context) { var service explorer.DirectoryService - if err := c.ShouldBindQuery(&service); err == nil { + if err := c.ShouldBindUri(&service); err == nil { res := service.ListDirectory(c) c.JSON(200, res) } else { diff --git a/routers/controllers/file.go b/routers/controllers/file.go index f0f9774..7e92e65 100644 --- a/routers/controllers/file.go +++ b/routers/controllers/file.go @@ -38,7 +38,7 @@ func FileUploadStream(c *gin.Context) { // 非本地策略时拒绝上传 if user, ok := c.Get("user"); ok && user.(*model.User).Policy.Type != "local" { - c.JSON(200, serializer.Err(serializer.CodePolicyNotAllowed, "当前上传策略无法使用", nil)) + c.JSON(200, serializer.Err(serializer.CodePolicyNotAllowed, "当前存储策略无法使用", nil)) return } diff --git a/routers/controllers/objects.go b/routers/controllers/objects.go new file mode 100644 index 0000000..0db6762 --- /dev/null +++ b/routers/controllers/objects.go @@ -0,0 +1,22 @@ +package controllers + +import ( + "context" + "github.com/HFO4/cloudreve/service/explorer" + "github.com/gin-gonic/gin" +) + +// Delete 删除文件或目录 +func Delete(c *gin.Context) { + // 创建上下文 + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var service explorer.ItemService + if err := c.ShouldBindJSON(&service); err == nil { + res := service.Delete(ctx, c) + c.JSON(200, res) + } else { + c.JSON(200, ErrorResponse(err)) + } +} diff --git a/routers/router.go b/routers/router.go index 795a987..ae3f506 100644 --- a/routers/router.go +++ b/routers/router.go @@ -72,7 +72,13 @@ func InitRouter() *gin.Engine { // 创建目录 directory.PUT("", controllers.CreateDirectory) // 列出目录下内容 - directory.GET("", controllers.ListDirectory) + directory.GET("*path", controllers.ListDirectory) + } + + // 对象,文件和目录的抽象 + object := auth.Group("object") + { + object.DELETE("", controllers.Delete) } } diff --git a/routers/router_test.go b/routers/router_test.go index 66f4964..40eff97 100644 --- a/routers/router_test.go +++ b/routers/router_test.go @@ -77,7 +77,7 @@ func TestUserSession(t *testing.T) { AddRow("login_captcha", "0", "login"), userRows: sqlmock.NewRows([]string{"email", "nick", "password", "options"}). AddRow("admin@cloudreve.org", "admin", "CKLmDKa1C9SD64vU:76adadd4fd4bad86959155f6f7bc8993c94e7adf", "{}"), policyRows: sqlmock.NewRows([]string{"name", "type", "options"}). - AddRow("默认上传策略", "local", "{\"op_name\":\"123\"}"), + AddRow("默认存储策略", "local", "{\"op_name\":\"123\"}"), reqBody: `{"userName":"admin@cloudreve.org","captchaCode":"captchaCode","Password":"admin"}`, expected: serializer.BuildUserResponse(model.User{ Email: "admin@cloudreve.org", @@ -95,7 +95,7 @@ func TestUserSession(t *testing.T) { userRows: sqlmock.NewRows([]string{"email", "nick", "password", "options"}). AddRow("admin@cloudreve.org", "admin", "CKLmDKa1C9SD64vU:76adadd4fd4bad86959155f6f7bc8993c94e7adf", "{}"), policyRows: sqlmock.NewRows([]string{"name", "type", "options"}). - AddRow("默认上传策略", "local", "{\"op_name\":\"123\"}"), + AddRow("默认存储策略", "local", "{\"op_name\":\"123\"}"), reqBody: `{"userName":"admin@cloudreve.org","captchaCode":"captchaCode","Password":"admin"}`, expected: serializer.ParamErr("验证码错误", nil), }, @@ -106,7 +106,7 @@ func TestUserSession(t *testing.T) { userRows: sqlmock.NewRows([]string{"email", "nick", "password", "options"}). AddRow("admin@cloudreve.org", "admin", "CKLmDKa1C9SD64vU:76adadd4fd4bad86959155f6f7bc8993c94e7adf", "{}"), policyRows: sqlmock.NewRows([]string{"name", "type", "options"}). - AddRow("默认上传策略", "local", "{\"op_name\":\"123\"}"), + AddRow("默认存储策略", "local", "{\"op_name\":\"123\"}"), reqBody: `{"userName":"admin@cloudreve.org","captchaCode":"captchaCode","Password":"admin123"}`, expected: serializer.Err(401, "用户邮箱或密码错误", nil), }, @@ -122,7 +122,7 @@ func TestUserSession(t *testing.T) { userRows: sqlmock.NewRows([]string{"email", "nick", "password", "options", "status"}). AddRow("admin@cloudreve.org", "admin", "CKLmDKa1C9SD64vU:76adadd4fd4bad86959155f6f7bc8993c94e7adf", "{}", model.Baned), policyRows: sqlmock.NewRows([]string{"name", "type", "options"}). - AddRow("默认上传策略", "local", "{\"op_name\":\"123\"}"), + AddRow("默认存储策略", "local", "{\"op_name\":\"123\"}"), reqBody: `{"userName":"admin@cloudreve.org","captchaCode":"captchaCode","Password":"admin"}`, expected: serializer.Err(403, "该账号已被封禁", nil), }, @@ -133,7 +133,7 @@ func TestUserSession(t *testing.T) { userRows: sqlmock.NewRows([]string{"email", "nick", "password", "options", "status"}). AddRow("admin@cloudreve.org", "admin", "CKLmDKa1C9SD64vU:76adadd4fd4bad86959155f6f7bc8993c94e7adf", "{}", model.NotActivicated), policyRows: sqlmock.NewRows([]string{"name", "type", "options"}). - AddRow("默认上传策略", "local", "{\"op_name\":\"123\"}"), + AddRow("默认存储策略", "local", "{\"op_name\":\"123\"}"), reqBody: `{"userName":"admin@cloudreve.org","captchaCode":"captchaCode","Password":"admin"}`, expected: serializer.Err(403, "该账号未激活", nil), }, @@ -273,7 +273,7 @@ func TestListDirectoryRoute(t *testing.T) { // 成功 req, _ := http.NewRequest( "GET", - "/api/v3/directory?path=/", + "/api/v3/directory/", nil, ) middleware.SessionMock = map[string]interface{}{"user_id": 1} @@ -286,20 +286,6 @@ func TestListDirectoryRoute(t *testing.T) { w.Body.Reset() - // 缺少参数 - req, _ = http.NewRequest( - "GET", - "/api/v3/directory", - nil, - ) - middleware.SessionMock = map[string]interface{}{"user_id": 1} - router.ServeHTTP(w, req) - asserts.Equal(200, w.Code) - resJSON = &serializer.Response{} - err = json.Unmarshal(w.Body.Bytes(), resJSON) - asserts.NoError(err) - asserts.NotEqual(0, resJSON.Code) - } func TestLocalFileUpload(t *testing.T) { diff --git a/service/explorer/directory.go b/service/explorer/directory.go index 60c91ab..bb634b5 100644 --- a/service/explorer/directory.go +++ b/service/explorer/directory.go @@ -9,10 +9,10 @@ import ( // DirectoryService 创建新目录服务 type DirectoryService struct { - Path string `form:"path" json:"path" binding:"required,min=1,max=65535"` + Path string `uri:"path" json:"path" binding:"required,min=1,max=65535"` } -// ListDirectory 列出目录内容 TODO:test +// ListDirectory 列出目录内容 func (service *DirectoryService) ListDirectory(c *gin.Context) serializer.Response { // 创建文件系统 fs, err := filesystem.NewFileSystemFromContext(c) diff --git a/service/explorer/file.go b/service/explorer/file.go index a154cbf..fd4f2b5 100644 --- a/service/explorer/file.go +++ b/service/explorer/file.go @@ -30,7 +30,7 @@ func (service *FileDownloadService) Download(ctx context.Context, c *gin.Context } // 设置文件名 - c.Header("Content-Disposition", "attachment; filename=\""+fs.Target.Name+"\"") + c.Header("Content-Disposition", "attachment; filename=\""+fs.FileTarget[0].Name+"\"") // 发送文件 http.ServeContent(c.Writer, c.Request, "", time.Time{}, rs) diff --git a/service/explorer/objects.go b/service/explorer/objects.go new file mode 100644 index 0000000..d4771e3 --- /dev/null +++ b/service/explorer/objects.go @@ -0,0 +1,34 @@ +package explorer + +import ( + "context" + "github.com/HFO4/cloudreve/pkg/filesystem" + "github.com/HFO4/cloudreve/pkg/serializer" + "github.com/gin-gonic/gin" +) + +// ItemService 处理多文件/目录相关服务 +type ItemService struct { + Items []string `json:"items" binding:"exists"` + Dirs []string `json:"dirs" binding:"exists"` +} + +// Delete 删除对象 +func (service *ItemService) Delete(ctx context.Context, c *gin.Context) serializer.Response { + // 创建文件系统 + fs, err := filesystem.NewFileSystemFromContext(c) + if err != nil { + return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err) + } + + // 删除对象 + err = fs.Delete(ctx, service.Dirs, service.Items) + if err != nil { + return serializer.Err(serializer.CodeNotSet, err.Error(), err) + } + + return serializer.Response{ + Code: 0, + } + +}