From 56b1ae9f317987eb278fd667a173baabe096d7f0 Mon Sep 17 00:00:00 2001 From: HFO4 <912394456@qq.com> Date: Sat, 7 Dec 2019 19:47:22 +0800 Subject: [PATCH] Modify: use pure tree structure in file system scheme --- models/file.go | 47 +- models/file_test.go | 107 ++-- models/folder.go | 355 ++++------- models/folder_test.go | 1113 +++++++++++++++++---------------- models/user.go | 13 +- models/user_test.go | 22 + pkg/filesystem/manage.go | 336 ++++++++++ pkg/filesystem/manage_test.go | 453 ++++++++++++++ pkg/filesystem/path.go | 361 +---------- pkg/filesystem/path_test.go | 457 +++----------- pkg/util/path.go | 14 + routers/file_router_test.go | 4 +- service/explorer/objects.go | 4 +- 13 files changed, 1697 insertions(+), 1589 deletions(-) create mode 100644 pkg/filesystem/manage.go create mode 100644 pkg/filesystem/manage_test.go diff --git a/models/file.go b/models/file.go index d6d7dfe..f37ee11 100644 --- a/models/file.go +++ b/models/file.go @@ -21,6 +21,9 @@ type File struct { // 关联模型 Policy Policy `gorm:"PRELOAD:false,association_autoupdate:false"` + + // 数据库忽略字段 + PositionTemp string `gorm:"-"` } // Create 创建文件记录 @@ -32,20 +35,31 @@ func (file *File) Create() (uint, error) { return file.ID, nil } -// GetFileByPathAndName 给定路径(s)、文件名、用户ID,查找文件 -func GetFileByPathAndName(path string, name string, uid uint) (File, error) { +// GetChildFile 查找目录下名为name的子文件 +func (folder *Folder) GetChildFile(name string) (*File, error) { var file File - result := DB.Where("user_id = ? AND dir = ? AND name=?", uid, path, name).First(&file) - return file, result.Error + result := DB.Where("folder_id = ? AND name = ?", folder.ID, name).Find(&file) + + if result.Error == nil { + file.PositionTemp = path.Join(folder.PositionTemp, folder.Name, file.Name) + } + return &file, result.Error } -// GetChildFile 查找目录下子文件 -func (folder *Folder) GetChildFile() ([]File, error) { +// GetChildFiles 查找目录下子文件 +func (folder *Folder) GetChildFiles() ([]File, error) { var files []File result := DB.Where("folder_id = ?", folder.ID).Find(&files) return files, result.Error } +// GetFilesByIDs 根据文件ID批量获取文件 +func GetFilesByIDs(ids []uint, uid uint) ([]File, error) { + var files []File + result := DB.Where("id in (?) AND user_id = ?", ids, uid).Find(&files) + return files, result.Error +} + // GetChildFilesOfFolders 批量检索目录子文件 func GetChildFilesOfFolders(folders *[]Folder) ([]File, error) { // 将所有待删除目录ID抽离,以便检索文件 @@ -68,19 +82,6 @@ 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) { // 结果值 @@ -127,14 +128,6 @@ func DeleteFileByIDs(ids []uint) error { return result.Error } -//// GetRecursiveByPaths 根据给定的文件路径(s)递归查找文件 -//func GetRecursiveByPaths(paths []string, uid uint) ([]File, error) { -// files := make([]File, 0, len(paths)) -// search := util.BuildRegexp(paths, "^", "/", "|") -// result := DB.Where("(user_id = ? and dir REGEXP ?) or (user_id = ? and dir in (?))", uid, search, uid, paths).Find(&files) -// return files, result.Error -//} - // GetFilesByParentIDs 根据父目录ID查找文件 func GetFilesByParentIDs(ids []uint, uid uint) ([]File, error) { files := make([]File, 0, len(ids)) diff --git a/models/file_test.go b/models/file_test.go index 4629ccb..4e9054a 100644 --- a/models/file_test.go +++ b/models/file_test.go @@ -8,17 +8,6 @@ import ( "testing" ) -func TestGetFileByPathAndName(t *testing.T) { - asserts := assert.New(t) - - fileRows := sqlmock.NewRows([]string{"id", "name"}). - AddRow(1, "1.cia") - mock.ExpectQuery("SELECT(.+)").WillReturnRows(fileRows) - file, _ := GetFileByPathAndName("/", "1.cia", 1) - asserts.Equal("1.cia", file.Name) - asserts.NoError(mock.ExpectationsWereMet()) -} - func TestFile_Create(t *testing.T) { asserts := assert.New(t) file := File{ @@ -44,6 +33,32 @@ func TestFile_Create(t *testing.T) { } func TestFolder_GetChildFile(t *testing.T) { + asserts := assert.New(t) + folder := Folder{Model: gorm.Model{ID: 1}, Name: "/"} + // 存在 + { + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1, "1.txt"). + WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "1.txt")) + file, err := folder.GetChildFile("1.txt") + asserts.NoError(mock.ExpectationsWereMet()) + asserts.NoError(err) + asserts.Equal("1.txt", file.Name) + asserts.Equal("/1.txt", file.PositionTemp) + } + + // 不存在 + { + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1, "1.txt"). + WillReturnRows(sqlmock.NewRows([]string{"id", "name"})) + _, err := folder.GetChildFile("1.txt") + asserts.NoError(mock.ExpectationsWereMet()) + asserts.Error(err) + } +} + +func TestFolder_GetChildFiles(t *testing.T) { asserts := assert.New(t) folder := &Folder{ Model: gorm.Model{ @@ -53,20 +68,46 @@ func TestFolder_GetChildFile(t *testing.T) { // 找不到 mock.ExpectQuery("SELECT(.+)folder_id(.+)").WithArgs(1).WillReturnError(errors.New("error")) - files, err := folder.GetChildFile() + files, err := folder.GetChildFiles() asserts.Error(err) asserts.Len(files, 0) asserts.NoError(mock.ExpectationsWereMet()) // 找到了 mock.ExpectQuery("SELECT(.+)folder_id(.+)").WithArgs(1).WillReturnRows(sqlmock.NewRows([]string{"name", "id"}).AddRow("1.txt", 1).AddRow("2.txt", 2)) - files, err = folder.GetChildFile() + files, err = folder.GetChildFiles() asserts.NoError(err) asserts.Len(files, 2) asserts.NoError(mock.ExpectationsWereMet()) } +func TestGetFilesByIDs(t *testing.T) { + asserts := assert.New(t) + + // 出错 + { + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1, 2, 3, 1). + WillReturnError(errors.New("error")) + folders, err := GetFilesByIDs([]uint{1, 2, 3}, 1) + asserts.NoError(mock.ExpectationsWereMet()) + asserts.Error(err) + asserts.Len(folders, 0) + } + + // 部分找到 + { + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1, 2, 3, 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "1")) + folders, err := GetFilesByIDs([]uint{1, 2, 3}, 1) + asserts.NoError(mock.ExpectationsWereMet()) + asserts.NoError(err) + asserts.Len(folders, 1) + } +} + func TestGetChildFilesOfFolders(t *testing.T) { asserts := assert.New(t) testFolder := []Folder{ @@ -149,46 +190,6 @@ func TestFile_GetPolicy(t *testing.T) { } } -func TestGetFileByPaths(t *testing.T) { - asserts := assert.New(t) - paths := []string{"/我的目录/文件.txt", "/根目录文件.txt"} - - // 正常情况 - { - mock.ExpectQuery("SELECT(.+)files(.+)"). - WithArgs("/我的目录", "文件.txt", 1, "/", "根目录文件.txt", 1). - WillReturnRows( - sqlmock.NewRows([]string{"id", "name"}). - AddRow(1, "文件.txt"). - AddRow(2, "根目录文件.txt"), - ) - files, err := GetFileByPaths(paths, 1) - asserts.NoError(mock.ExpectationsWereMet()) - asserts.NoError(err) - asserts.Equal([]File{ - File{ - Model: gorm.Model{ID: 1}, - Name: "文件.txt", - }, - File{ - Model: gorm.Model{ID: 2}, - Name: "根目录文件.txt", - }, - }, files) - } - - // 出错 - { - mock.ExpectQuery("SELECT(.+)files(.+)"). - WithArgs("/我的目录", "文件.txt", 1, "/", "根目录文件.txt", 1). - WillReturnError(errors.New("error")) - files, err := GetFileByPaths(paths, 1) - asserts.NoError(mock.ExpectationsWereMet()) - asserts.Error(err) - asserts.Len(files, 0) - } -} - func TestRemoveFilesWithSoftLinks(t *testing.T) { asserts := assert.New(t) files := []File{ diff --git a/models/folder.go b/models/folder.go index ced959a..189b292 100644 --- a/models/folder.go +++ b/models/folder.go @@ -2,11 +2,9 @@ package model import ( "errors" - "github.com/HFO4/cloudreve/pkg/conf" "github.com/HFO4/cloudreve/pkg/util" "github.com/jinzhu/gorm" "path" - "strings" ) // Folder 目录 @@ -18,6 +16,9 @@ type Folder struct { Position string `gorm:"size:65536"` OwnerID uint `gorm:"index:owner_id"` PositionAbsolute string `gorm:"size:65536"` + + // 数据库忽略字段 + PositionTemp string `gorm:"-"` } // Create 创建目录 @@ -29,11 +30,18 @@ func (folder *Folder) Create() (uint, error) { return folder.ID, nil } -// GetFolderByPath 根据绝对路径和UID查找目录 -func GetFolderByPath(path string, uid uint) (Folder, error) { - var folder Folder - result := DB.Where("owner_id = ? AND position_absolute = ?", uid, path).First(&folder) - return folder, result.Error +// GetChild 返回folder下名为name的子目录,不存在则返回错误 +func (folder *Folder) GetChild(name string) (*Folder, error) { + var resFolder Folder + err := DB. + Where("parent_id = ? AND owner_id = ? AND name = ?", folder.ID, folder.OwnerID, name). + First(&resFolder).Error + + // 将子目录的路径传递下去 + if err == nil { + resFolder.PositionTemp = path.Join(folder.PositionTemp, folder.Name) + } + return &resFolder, err } // GetChildFolder 查找子目录 @@ -44,59 +52,48 @@ func (folder *Folder) GetChildFolder() ([]Folder, error) { } // GetRecursiveChildFolder 查找所有递归子目录,包括自身 -func GetRecursiveChildFolder(dirs []string, uid uint, includeSelf bool) ([]Folder, error) { +func GetRecursiveChildFolder(dirs []uint, uid uint, includeSelf bool) ([]Folder, error) { folders := make([]Folder, 0, len(dirs)) var err error - if conf.DatabaseConfig.Type == "mysql" { + // SQLite 下使用递归查询 + var parFolders []Folder + result := DB.Where("owner_id = ? and id in (?)", uid, dirs).Find(&parFolders) + if result.Error != nil { + return folders, err + } - // MySQL 下使用正则查询 - search := util.BuildRegexp(dirs, "^", "/", "|") - result := DB.Where("(owner_id = ? and position_absolute REGEXP ?) or (owner_id = ? and position_absolute in (?))", uid, search, uid, dirs).Find(&folders) - err = result.Error + // 整理父目录的ID + var parentIDs = make([]uint, 0, len(parFolders)) + for _, folder := range parFolders { + parentIDs = append(parentIDs, folder.ID) + } - } else { + if includeSelf { + // 合并至最终结果 + folders = append(folders, parFolders...) + } + parFolders = []Folder{} + + // 递归查询子目录,最大递归65535次 + for i := 0; i < 65535; i++ { - // SQLite 下使用递归查询 - var parFolders []Folder - result := DB.Where("owner_id = ? and position_absolute in (?)", uid, dirs).Find(&parFolders) - if result.Error != nil { - return folders, err + result = DB.Where("owner_id = ? and parent_id in (?)", uid, parentIDs).Find(&parFolders) + + // 查询结束条件 + if len(parFolders) == 0 { + break } // 整理父目录的ID - var parentIDs = make([]uint, 0, len(parFolders)) + parentIDs = make([]uint, 0, len(parFolders)) for _, folder := range parFolders { parentIDs = append(parentIDs, folder.ID) } - if includeSelf { - // 合并至最终结果 - folders = append(folders, parFolders...) - } + // 合并至最终结果 + folders = append(folders, parFolders...) parFolders = []Folder{} - // 递归查询子目录,最大递归65535次 - for i := 0; i < 65535; i++ { - - result = DB.Where("owner_id = ? and parent_id in (?)", uid, parentIDs).Find(&parFolders) - - // 查询结束条件 - if len(parFolders) == 0 { - break - } - - // 整理父目录的ID - parentIDs = make([]uint, 0, len(parFolders)) - for _, folder := range parFolders { - parentIDs = append(parentIDs, folder.ID) - } - - // 合并至最终结果 - folders = append(folders, parFolders...) - parFolders = []Folder{} - - } - } return folders, err @@ -108,9 +105,16 @@ func DeleteFolderByIDs(ids []uint) error { return result.Error } +// GetFoldersByIDs 根据ID和用户查找所有目录 +func GetFoldersByIDs(ids []uint, uid uint) ([]Folder, error) { + var folders []Folder + result := DB.Where("id in (?) AND owner_id = ?", ids, uid).Find(&folders) + return folders, result.Error +} + // MoveOrCopyFileTo 将此目录下的files移动或复制至dstFolder, // 返回此操作新增的容量 -func (folder *Folder) MoveOrCopyFileTo(files []string, dstFolder *Folder, isCopy bool) (uint64, error) { +func (folder *Folder) MoveOrCopyFileTo(files []uint, dstFolder *Folder, isCopy bool) (uint64, error) { // 已复制文件的总大小 var copiedSize uint64 @@ -118,10 +122,10 @@ func (folder *Folder) MoveOrCopyFileTo(files []string, dstFolder *Folder, isCopy // 检索出要复制的文件 var originFiles = make([]File, 0, len(files)) if err := DB.Where( - "name in (?) and user_id = ? and dir = ?", + "id in (?) and user_id = ? and folder_id = ?", files, folder.OwnerID, - folder.PositionAbsolute, + folder.ID, ).Find(&originFiles).Error; err != nil { return 0, err } @@ -130,7 +134,6 @@ func (folder *Folder) MoveOrCopyFileTo(files []string, dstFolder *Folder, isCopy for _, oldFile := range originFiles { oldFile.Model = gorm.Model{} oldFile.FolderID = dstFolder.ID - oldFile.Dir = dstFolder.PositionAbsolute if err := DB.Create(&oldFile).Error; err != nil { return copiedSize, err @@ -142,14 +145,13 @@ func (folder *Folder) MoveOrCopyFileTo(files []string, dstFolder *Folder, isCopy } else { // 更改顶级要移动文件的父目录指向 err := DB.Model(File{}).Where( - "name in (?) and user_id = ? and dir = ?", + "id in (?) and user_id = ? and folder_id = ?", files, folder.OwnerID, - folder.PositionAbsolute, + folder.ID, ). Update(map[string]interface{}{ "folder_id": dstFolder.ID, - "dir": dstFolder.PositionAbsolute, }). Error if err != nil { @@ -162,209 +164,88 @@ func (folder *Folder) MoveOrCopyFileTo(files []string, dstFolder *Folder, isCopy } -// MoveOrCopyFolderTo 将folder目录下的dirs子目录复制或移动到dstFolder, -// 返回此过程中增加的容量 -func (folder *Folder) MoveOrCopyFolderTo(dirs []string, dstFolder *Folder, isCopy bool) (uint64, error) { - // 生成绝对路径 - fullDirs := make([]string, len(dirs)) - for i := 0; i < len(dirs); i++ { - fullDirs[i] = path.Join( - folder.PositionAbsolute, - path.Base(dirs[i]), - ) +// CopyFolderTo 将此目录及其子目录及文件递归复制至dstFolder +// 返回此操作新增的容量 +func (folder *Folder) CopyFolderTo(folderID uint, dstFolder *Folder) (size uint64, err error) { + // 列出所有子目录 + subFolders, err := GetRecursiveChildFolder([]uint{folderID}, folder.OwnerID, true) + if err != nil { + return 0, err } - var subFolders = make([][]Folder, len(fullDirs)) - - // 更新被移动的目录递归的子目录和文件 - for key, parentDir := range fullDirs { - // 检索被移动的目录的所有子目录 - toBeMoved, err := GetRecursiveChildFolder([]string{parentDir}, folder.OwnerID, true) - if err != nil { - return 0, err - } - subFolders[key] = toBeMoved + // 抽离所有子目录的ID + var subFolderIDs = make([]uint, len(subFolders)) + for key, value := range subFolders { + subFolderIDs[key] = value.ID } - // 记录复制要用到的父目录源路径和新的ID - var copyCache = make(map[string]uint) - // 记录已复制文件的容量 - var newUsedStorage uint64 - - var err error - if isCopy { - // 复制 - // TODO:支持多目录 - origin := Folder{} - if DB.Where( - "position_absolute in (?) and owner_id = ?", - fullDirs, - folder.OwnerID, - ).Find(&origin).Error != nil { - return 0, errors.New("找不到原始目录") + // 复制子目录 + var newIDCache = make(map[uint]uint) + for _, folder := range subFolders { + // 新的父目录指向 + var newID uint + // 顶级目录直接指向新的目的目录 + if folder.ID == folderID { + newID = dstFolder.ID + } else if IDCache, ok := newIDCache[folder.ParentID]; ok { + newID = IDCache + } else { + util.Log().Warning("无法取得新的父目录:%d", folder.ParentID) + return size, errors.New("无法取得新的父目录") } - oldPosition := origin.PositionAbsolute - - // 更新复制后的相关属性 - origin.PositionAbsolute = util.FillSlash(dstFolder.PositionAbsolute) + origin.Name - origin.Position = dstFolder.PositionAbsolute - origin.ParentID = dstFolder.ID - - // 清空主键 - origin.Model = gorm.Model{} - - if err := DB.Create(&origin).Error; err != nil { - return 0, err + // 插入新的目录记录 + oldID := folder.ID + folder.Model = gorm.Model{} + folder.ParentID = newID + if err = DB.Create(&folder).Error; err != nil { + return size, err } + // 记录新的ID以便其子目录使用 + newIDCache[oldID] = folder.ID - // 记录新的主键 - copyCache[oldPosition] = origin.Model.ID - - } else { - // 移动 - // 更改顶级要移动目录的父目录指向 - err = DB.Model(Folder{}). - Where("position_absolute in (?) and owner_id = ?", - fullDirs, - folder.OwnerID, - ). - Update(map[string]interface{}{ - "parent_id": dstFolder.ID, - "position": dstFolder.PositionAbsolute, - "position_absolute": gorm.Expr( - util.BuildConcat("?", - "name", - conf.DatabaseConfig.Type, - ), - util.FillSlash(dstFolder.PositionAbsolute), - ), - }).Error } - if err != nil { + // 复制文件 + var originFiles = make([]File, 0, len(subFolderIDs)) + if err := DB.Where( + "user_id = ? and folder_id in (?)", + folder.OwnerID, + subFolderIDs, + ).Find(&originFiles).Error; err != nil { return 0, err } - // 更新被移动的目录递归的子目录和文件 - for parKey, toBeMoved := range subFolders { - ignorePath := fullDirs[parKey] - // TODO 找到更好的修改办法 - - // 抽离所有子目录的ID - var subFolderIDs = make([]uint, len(toBeMoved)) - for key, subFolder := range toBeMoved { - subFolderIDs[key] = subFolder.ID - } - - if isCopy { - index := 0 - for len(toBeMoved) != 0 { - innerIndex := index % len(toBeMoved) - index++ - // 限制循环次数 - if index > 65535 { - return 0, errors.New("循环超出限制") - } - - // 如果是顶级父目录,直接删除,不需要复制 - if toBeMoved[innerIndex].PositionAbsolute == ignorePath { - toBeMoved = append(toBeMoved[:innerIndex], toBeMoved[innerIndex+1:]...) - continue - } - - // 如果缓存中存在父目录ID,执行复制,并删除 - if newID, ok := copyCache[toBeMoved[innerIndex].Position]; ok { - // 记录目录原来的路径 - oldPosition := toBeMoved[innerIndex].PositionAbsolute - - // 设置目录i虚拟的路径 - newPosition := path.Join( - dstFolder.PositionAbsolute, strings.Replace( - toBeMoved[innerIndex].Position, - folder.PositionAbsolute, "", 1), - ) - toBeMoved[innerIndex].Position = newPosition - toBeMoved[innerIndex].PositionAbsolute = path.Join( - newPosition, - toBeMoved[innerIndex].Name, - ) - toBeMoved[innerIndex].ParentID = newID - toBeMoved[innerIndex].Model = gorm.Model{} - if err := DB.Create(&toBeMoved[innerIndex]).Error; err != nil { - return 0, err - } - - // 将当前目录老路径和新ID保存,以便后续待处理目录文件使用 - copyCache[oldPosition] = toBeMoved[innerIndex].Model.ID - toBeMoved = append(toBeMoved[:innerIndex], toBeMoved[innerIndex+1:]...) - } - } + // 复制文件记录 + for _, oldFile := range originFiles { + oldFile.Model = gorm.Model{} + oldFile.FolderID = newIDCache[oldFile.FolderID] - } else { - for _, subFolder := range toBeMoved { - // 每个分组的第一个目录已经变更指向,直接跳过 - if subFolder.PositionAbsolute != ignorePath { - newPosition := path.Join(dstFolder.PositionAbsolute, - strings.Replace(subFolder.Position, - folder.PositionAbsolute, - "", - 1, - ), - ) - // 移动 - DB.Model(&subFolder).Updates(map[string]interface{}{ - "position": newPosition, - "position_absolute": path.Join(newPosition, subFolder.Name), - }) - } - } + if err := DB.Create(&oldFile).Error; err != nil { + return size, err } - // 获取子目录下的所有子文件 - toBeMovedFile, err := GetFilesByParentIDs(subFolderIDs, folder.OwnerID) - if err != nil { - return 0, err - } - - // 开始复制或移动子文件 - for _, subFile := range toBeMovedFile { - newPosition := path.Join(dstFolder.PositionAbsolute, - strings.Replace( - subFile.Dir, - folder.PositionAbsolute, - "", - 1, - ), - ) - if isCopy { - // 复制 - if newID, ok := copyCache[subFile.Dir]; ok { - subFile.FolderID = newID - } else { - util.Log().Debug("无法找到文件的父目录ID,原始路径:%s", subFile.Dir) - } - subFile.Dir = newPosition - subFile.Model = gorm.Model{} - - // 复制文件记录 - if err := DB.Create(&subFile).Error; err != nil { - util.Log().Warning("无法复制子文件:%s", err) - } else { - // 记录此文件容量 - newUsedStorage += subFile.Size - } - } else { - DB.Model(&subFile).Updates(map[string]interface{}{ - "dir": newPosition, - }) - } + size += oldFile.Size + } - } + return size, nil - } +} - return newUsedStorage, nil +// MoveFolderTo 将folder目录下的dirs子目录复制或移动到dstFolder, +// 返回此过程中增加的容量 +func (folder *Folder) MoveFolderTo(dirs []uint, dstFolder *Folder) error { + // 更改顶级要移动目录的父目录指向 + err := DB.Model(Folder{}).Where( + "id in (?) and owner_id = ? and parent_id = ?", + dirs, + folder.OwnerID, + folder.ID, + ).Update(map[string]interface{}{ + "parent_id": dstFolder.ID, + }).Error + + return err } diff --git a/models/folder_test.go b/models/folder_test.go index bf0e231..cc10784 100644 --- a/models/folder_test.go +++ b/models/folder_test.go @@ -4,29 +4,11 @@ import ( "errors" "github.com/DATA-DOG/go-sqlmock" "github.com/HFO4/cloudreve/pkg/conf" - "github.com/HFO4/cloudreve/pkg/util" "github.com/jinzhu/gorm" "github.com/stretchr/testify/assert" "testing" ) -func TestGetFolderByPath(t *testing.T) { - asserts := assert.New(t) - - folderRows := sqlmock.NewRows([]string{"id", "name"}). - AddRow(1, "test") - mock.ExpectQuery("^SELECT (.+)").WillReturnRows(folderRows) - folder, _ := GetFolderByPath("/测试/test", 1) - asserts.Equal("test", folder.Name) - asserts.NoError(mock.ExpectationsWereMet()) - - folderRows = sqlmock.NewRows([]string{"id", "name"}) - mock.ExpectQuery("^SELECT (.+)").WillReturnRows(folderRows) - folder, err := GetFolderByPath("/测试/test", 1) - asserts.Error(err) - asserts.NoError(mock.ExpectationsWereMet()) -} - func TestFolder_Create(t *testing.T) { asserts := assert.New(t) folder := &Folder{ @@ -52,6 +34,39 @@ func TestFolder_Create(t *testing.T) { asserts.NoError(mock.ExpectationsWereMet()) } +func TestFolder_GetChild(t *testing.T) { + asserts := assert.New(t) + folder := Folder{ + Model: gorm.Model{ID: 5}, + OwnerID: 1, + Name: "/", + } + + // 目录存在 + { + mock.ExpectQuery("SELECT(.+)"). + WithArgs(5, 1, "sub"). + WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "sub")) + sub, err := folder.GetChild("sub") + asserts.NoError(mock.ExpectationsWereMet()) + asserts.NoError(err) + asserts.Equal(sub.Name, "sub") + asserts.Equal("/", sub.PositionTemp) + } + + // 目录不存在 + { + mock.ExpectQuery("SELECT(.+)"). + WithArgs(5, 1, "sub"). + WillReturnRows(sqlmock.NewRows([]string{"id", "name"})) + sub, err := folder.GetChild("sub") + asserts.NoError(mock.ExpectationsWereMet()) + asserts.Error(err) + asserts.Equal(uint(0), sub.ID) + + } +} + func TestFolder_GetChildFolder(t *testing.T) { asserts := assert.New(t) folder := &Folder{ @@ -75,38 +90,6 @@ func TestFolder_GetChildFolder(t *testing.T) { asserts.NoError(mock.ExpectationsWereMet()) } -func TestGetRecursiveChildFolder(t *testing.T) { - conf.DatabaseConfig.Type = "mysql" - asserts := assert.New(t) - dirs := []string{"/目录1", "/目录2"} - - // 正常 - { - mock.ExpectQuery("SELECT(.+)folders(.+)"). - WithArgs(1, util.BuildRegexp(dirs, "^", "/", "|"), 1, "/目录1", "/目录2"). - WillReturnRows( - sqlmock.NewRows([]string{"id", "name"}). - AddRow(1, "sub1"). - AddRow(2, "sub2"). - AddRow(3, "sub3"), - ) - subs, err := GetRecursiveChildFolder(dirs, 1, true) - asserts.NoError(mock.ExpectationsWereMet()) - asserts.NoError(err) - asserts.Len(subs, 3) - } - // 出错 - { - mock.ExpectQuery("SELECT(.+)folders(.+)"). - WithArgs(1, util.BuildRegexp(dirs, "^", "/", "|"), 1, "/目录1", "/目录2"). - WillReturnError(errors.New("233")) - subs, err := GetRecursiveChildFolder(dirs, 1, true) - asserts.NoError(mock.ExpectationsWereMet()) - asserts.Error(err) - asserts.Len(subs, 0) - } -} - func TestGetRecursiveChildFolderSQLite(t *testing.T) { conf.DatabaseConfig.Type = "sqlite3" asserts := assert.New(t) @@ -118,7 +101,7 @@ func TestGetRecursiveChildFolderSQLite(t *testing.T) { // 查询第一层 mock.ExpectQuery("SELECT(.+)"). - WithArgs(1, "/test"). + WithArgs(1, 1). WillReturnRows( sqlmock.NewRows([]string{"id", "name"}). AddRow(1, "folder1"), @@ -147,7 +130,7 @@ func TestGetRecursiveChildFolderSQLite(t *testing.T) { sqlmock.NewRows([]string{"id", "name"}), ) - folders, err := GetRecursiveChildFolder([]string{"/test"}, 1, true) + folders, err := GetRecursiveChildFolder([]uint{1}, 1, true) asserts.NoError(err) asserts.NoError(mock.ExpectationsWereMet()) asserts.Len(folders, 6) @@ -178,519 +161,545 @@ func TestDeleteFolderByIDs(t *testing.T) { } } -func TestFolder_MoveOrCopyFileTo(t *testing.T) { - asserts := assert.New(t) - // 当前目录 - folder := Folder{ - OwnerID: 1, - PositionAbsolute: "/test", - } - // 目标目录 - dstFolder := Folder{ - Model: gorm.Model{ID: 10}, - PositionAbsolute: "/dst", - } - - // 复制文件 - { - mock.ExpectQuery("SELECT(.+)"). - WithArgs( - "1.txt", - "2.txt", - 1, - "/test", - ).WillReturnRows( - sqlmock.NewRows([]string{"id", "size"}). - AddRow(1, 10). - AddRow(2, 20), - ) - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) - mock.ExpectCommit() - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) - mock.ExpectCommit() - storage, err := folder.MoveOrCopyFileTo( - []string{"1.txt", "2.txt"}, - &dstFolder, - true, - ) - asserts.NoError(err) - asserts.NoError(mock.ExpectationsWereMet()) - asserts.Equal(uint64(30), storage) - } - - // 复制文件, 检索文件出错 - { - mock.ExpectQuery("SELECT(.+)"). - WithArgs( - "1.txt", - "2.txt", - 1, - "/test", - ).WillReturnError(errors.New("error")) - - storage, err := folder.MoveOrCopyFileTo( - []string{"1.txt", "2.txt"}, - &dstFolder, - true, - ) - asserts.Error(err) - asserts.NoError(mock.ExpectationsWereMet()) - asserts.Equal(uint64(0), storage) - } - - // 复制文件,第二个文件插入出错 - { - mock.ExpectQuery("SELECT(.+)"). - WithArgs( - "1.txt", - "2.txt", - 1, - "/test", - ).WillReturnRows( - sqlmock.NewRows([]string{"id", "size"}). - AddRow(1, 10). - AddRow(2, 20), - ) - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) - mock.ExpectCommit() - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)").WillReturnError(errors.New("error")) - mock.ExpectRollback() - storage, err := folder.MoveOrCopyFileTo( - []string{"1.txt", "2.txt"}, - &dstFolder, - true, - ) - asserts.Error(err) - asserts.NoError(mock.ExpectationsWereMet()) - asserts.Equal(uint64(10), storage) - } - - // 移动文件 成功 - { - mock.ExpectBegin() - mock.ExpectExec("UPDATE(.+)"). - WithArgs("/dst", 10, sqlmock.AnyArg(), "1.txt", "2.txt", 1, "/test"). - WillReturnResult(sqlmock.NewResult(1, 2)) - mock.ExpectCommit() - storage, err := folder.MoveOrCopyFileTo( - []string{"1.txt", "2.txt"}, - &dstFolder, - false, - ) - asserts.NoError(mock.ExpectationsWereMet()) - asserts.NoError(err) - asserts.Equal(uint64(0), storage) - } - - // 移动文件 出错 - { - mock.ExpectBegin() - mock.ExpectExec("UPDATE(.+)"). - WithArgs("/dst", 10, sqlmock.AnyArg(), "1.txt", "2.txt", 1, "/test"). - WillReturnError(errors.New("error")) - mock.ExpectRollback() - storage, err := folder.MoveOrCopyFileTo( - []string{"1.txt", "2.txt"}, - &dstFolder, - false, - ) - asserts.NoError(mock.ExpectationsWereMet()) - asserts.Error(err) - asserts.Equal(uint64(0), storage) - } -} - -func TestFolder_MoveOrCopyFolderTo_Copy(t *testing.T) { - conf.DatabaseConfig.Type = "mysql" +func TestGetFoldersByIDs(t *testing.T) { asserts := assert.New(t) - // 父目录 - parFolder := Folder{ - OwnerID: 1, - PositionAbsolute: "/", - } - // 目标目录 - dstFolder := Folder{ - Model: gorm.Model{ID: 10}, - PositionAbsolute: "/dst", - } - - // 测试复制目录结构 - // test(2) - // 1(3) 2.txt - // 3(4) 4.txt - - // 正常情况 成功 - { - // 查找所有递归子目录,包括自身 - mock.ExpectQuery("SELECT(.+)"). - WithArgs(1, sqlmock.AnyArg(), 1, "/test"). - WillReturnRows( - sqlmock.NewRows([]string{"id", "parent_id", "name", "position", "position_absolute"}). - AddRow(3, 2, "1", "/test", "/test/1"). - AddRow(2, 1, "test", "/", "/test"). - AddRow(4, 3, "3", "/test/1", "/test/1/3"), - ) - // 查找顶级待复制目录 - mock.ExpectQuery("SELECT(.+)"). - WithArgs("/test", 1). - WillReturnRows( - sqlmock.NewRows( - []string{"id", "parent_id", "name", "position", "position_absolute"}). - AddRow(2, 1, "test", "/", "/test"), - ) - - // 更新顶级目录 - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)"). - WillReturnResult(sqlmock.NewResult(5, 1)) - mock.ExpectCommit() - // 更新子目录 - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)"). - WillReturnResult(sqlmock.NewResult(6, 1)) - mock.ExpectCommit() - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)"). - WillReturnResult(sqlmock.NewResult(7, 1)) - mock.ExpectCommit() - - // 获取子目录下的所有子文件 - mock.ExpectQuery("SELECT(.+)"). - WithArgs(1, 3, 2, 4). - WillReturnRows( - sqlmock.NewRows([]string{"id", "folder_id", "dir", "size"}). - AddRow(1, 2, "/test", 10). - AddRow(2, 3, "/test/1", 20), - ) - - // 更新子文件记录 - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) - mock.ExpectCommit() - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) - mock.ExpectCommit() - storage, err := parFolder.MoveOrCopyFolderTo([]string{"/test"}, &dstFolder, true) - asserts.NoError(mock.ExpectationsWereMet()) - asserts.NoError(err) - asserts.Equal(uint64(30), storage) - - } - - // 处理子目录时死循环避免 - { - // 查找所有递归子目录,包括自身 - mock.ExpectQuery("SELECT(.+)"). - WithArgs(1, sqlmock.AnyArg(), 1, "/test"). - WillReturnRows( - sqlmock.NewRows([]string{"id", "parent_id", "name", "position", "position_absolute"}). - AddRow(3, 2, "1", "/test", "/test/1"). - AddRow(2, 1, "test", "/", "/1"). - AddRow(4, 3, "3", "/test/1", "/test/1/3"), - ) - // 查找顶级待复制目录 - mock.ExpectQuery("SELECT(.+)"). - WithArgs("/test", 1). - WillReturnRows( - sqlmock.NewRows( - []string{"id", "parent_id", "name", "position", "position_absolute"}). - AddRow(2, 1, "test", "/", "/test"), - ) - - // 更新顶级目录 - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)"). - WillReturnResult(sqlmock.NewResult(5, 1)) - mock.ExpectCommit() - // 更新子目录 - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)"). - WillReturnResult(sqlmock.NewResult(6, 1)) - mock.ExpectCommit() - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)"). - WillReturnResult(sqlmock.NewResult(6, 1)) - mock.ExpectCommit() - - storage, err := parFolder.MoveOrCopyFolderTo([]string{"/test"}, &dstFolder, true) - asserts.NoError(mock.ExpectationsWereMet()) - asserts.Error(err) - asserts.Equal(uint64(0), storage) - - } - - // 检索子目录出错 - { - // 查找所有递归子目录,包括自身 - mock.ExpectQuery("SELECT(.+)"). - WithArgs(1, sqlmock.AnyArg(), 1, "/test"). - WillReturnError(errors.New("error")) - - storage, err := parFolder.MoveOrCopyFolderTo([]string{"/test"}, &dstFolder, true) - asserts.NoError(mock.ExpectationsWereMet()) - asserts.Error(err) - asserts.Equal(uint64(0), storage) - - } - - // 寻找原始目录出错 + // 出错 { - // 查找所有递归子目录,包括自身 mock.ExpectQuery("SELECT(.+)"). - WithArgs(1, sqlmock.AnyArg(), 1, "/test"). - WillReturnRows( - sqlmock.NewRows([]string{"id", "parent_id", "name", "position", "position_absolute"}). - AddRow(3, 2, "1", "/test", "/test/1"). - AddRow(2, 1, "test", "/", "/test"). - AddRow(4, 3, "3", "/test/1", "/test/1/3"), - ) - // 查找顶级待复制目录 - mock.ExpectQuery("SELECT(.+)"). - WithArgs("/test", 1). + WithArgs(1, 2, 3, 1). WillReturnError(errors.New("error")) - - storage, err := parFolder.MoveOrCopyFolderTo([]string{"/test"}, &dstFolder, true) + folders, err := GetFoldersByIDs([]uint{1, 2, 3}, 1) asserts.NoError(mock.ExpectationsWereMet()) asserts.Error(err) - asserts.Equal(uint64(0), storage) - + asserts.Len(folders, 0) } - // 更新顶级目录出错 + // 部分找到 { - // 查找所有递归子目录,包括自身 - mock.ExpectQuery("SELECT(.+)"). - WithArgs(1, sqlmock.AnyArg(), 1, "/test"). - WillReturnRows( - sqlmock.NewRows([]string{"id", "parent_id", "name", "position", "position_absolute"}). - AddRow(3, 2, "1", "/test", "/test/1"). - AddRow(2, 1, "test", "/", "/test"). - AddRow(4, 3, "3", "/test/1", "/test/1/3"), - ) - // 查找顶级待复制目录 mock.ExpectQuery("SELECT(.+)"). - WithArgs("/test", 1). - WillReturnRows( - sqlmock.NewRows( - []string{"id", "parent_id", "name", "position", "position_absolute"}). - AddRow(2, 1, "test", "/", "/test"), - ) - - // 更新顶级目录 - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)"). - WillReturnError(errors.New("error")) - mock.ExpectRollback() - - storage, err := parFolder.MoveOrCopyFolderTo([]string{"/test"}, &dstFolder, true) - asserts.NoError(mock.ExpectationsWereMet()) - asserts.Error(err) - asserts.Equal(uint64(0), storage) - - } - - // 复制子目录,一个成功一个失败 - { - // 查找所有递归子目录,包括自身 - mock.ExpectQuery("SELECT(.+)"). - WithArgs(1, sqlmock.AnyArg(), 1, "/test"). - WillReturnRows( - sqlmock.NewRows([]string{"id", "parent_id", "name", "position", "position_absolute"}). - AddRow(3, 2, "1", "/test", "/test/1"). - AddRow(2, 1, "test", "/", "/test"). - AddRow(4, 3, "3", "/test/1", "/test/1/3"), - ) - // 查找顶级待复制目录 - mock.ExpectQuery("SELECT(.+)"). - WithArgs("/test", 1). - WillReturnRows( - sqlmock.NewRows( - []string{"id", "parent_id", "name", "position", "position_absolute"}). - AddRow(2, 1, "test", "/", "/test"), - ) - - // 更新顶级目录 - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)"). - WillReturnResult(sqlmock.NewResult(5, 1)) - mock.ExpectCommit() - // 更新子目录 - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)"). - WillReturnResult(sqlmock.NewResult(6, 1)) - mock.ExpectCommit() - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)"). - WillReturnError(errors.New("error")) - mock.ExpectRollback() - - storage, err := parFolder.MoveOrCopyFolderTo([]string{"/test"}, &dstFolder, true) - asserts.NoError(mock.ExpectationsWereMet()) - asserts.Error(err) - asserts.Equal(uint64(0), storage) - - } - - // 复制文件,一个成功一个失败 - { - // 查找所有递归子目录,包括自身 - mock.ExpectQuery("SELECT(.+)"). - WithArgs(1, sqlmock.AnyArg(), 1, "/test"). - WillReturnRows( - sqlmock.NewRows([]string{"id", "parent_id", "name", "position", "position_absolute"}). - AddRow(3, 2, "1", "/test", "/test/1"). - AddRow(2, 1, "test", "/", "/test"). - AddRow(4, 3, "3", "/test/1", "/test/1/3"), - ) - // 查找顶级待复制目录 - mock.ExpectQuery("SELECT(.+)"). - WithArgs("/test", 1). - WillReturnRows( - sqlmock.NewRows( - []string{"id", "parent_id", "name", "position", "position_absolute"}). - AddRow(2, 1, "test", "/", "/test"), - ) - - // 更新顶级目录 - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)"). - WillReturnResult(sqlmock.NewResult(5, 1)) - mock.ExpectCommit() - // 更新子目录 - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)"). - WillReturnResult(sqlmock.NewResult(6, 1)) - mock.ExpectCommit() - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)"). - WillReturnResult(sqlmock.NewResult(7, 1)) - mock.ExpectCommit() - - // 获取子目录下的所有子文件 - mock.ExpectQuery("SELECT(.+)"). - WithArgs(1, 3, 2, 4). - WillReturnRows( - sqlmock.NewRows([]string{"id", "folder_id", "dir", "size"}). - AddRow(1, 2, "/test", 10). - AddRow(2, 3, "/test/1", 20), - ) - - // 更新子文件记录 - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) - mock.ExpectCommit() - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)").WillReturnError(errors.New("error")) - mock.ExpectRollback() - - storage, err := parFolder.MoveOrCopyFolderTo([]string{"/test"}, &dstFolder, true) + WithArgs(1, 2, 3, 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "1")) + folders, err := GetFoldersByIDs([]uint{1, 2, 3}, 1) asserts.NoError(mock.ExpectationsWereMet()) asserts.NoError(err) - asserts.Equal(uint64(10), storage) - + asserts.Len(folders, 1) } - } -func TestFolder_MoveOrCopyFolderTo_Move(t *testing.T) { - conf.DatabaseConfig.Type = "mysql" - asserts := assert.New(t) - // 父目录 - parFolder := Folder{ - OwnerID: 1, - PositionAbsolute: "/", - } - // 目标目录 - dstFolder := Folder{ - Model: gorm.Model{ID: 10}, - PositionAbsolute: "/dst", - } - - // 测试复制目录结构 - // test(2) - // 1(3) 2.txt - // 3(4) 4.txt - - // 正常情况 成功 - { - // 查找所有递归子目录,包括自身 - mock.ExpectQuery("SELECT(.+)"). - WithArgs(1, sqlmock.AnyArg(), 1, "/test"). - WillReturnRows( - sqlmock.NewRows([]string{"id", "parent_id", "name", "position", "position_absolute"}). - AddRow(3, 2, "1", "/test", "/test/1"). - AddRow(2, 1, "test", "/", "/test"). - AddRow(4, 3, "3", "/test/1", "/test/1/3"), - ) - // 更改顶级要移动目录的父目录指向 - mock.ExpectBegin() - mock.ExpectExec("UPDATE(.+)"). - WithArgs(10, "/dst", "/dst/", sqlmock.AnyArg(), "/test", 1). - WillReturnResult(sqlmock.NewResult(1, 1)) - mock.ExpectCommit() - // 移动子目录 - mock.ExpectBegin() - mock.ExpectExec("UPDATE(.+)"). - WithArgs("/dst/test", "/dst/test/1", sqlmock.AnyArg(), 3). - WillReturnResult(sqlmock.NewResult(1, 1)) - mock.ExpectCommit() - mock.ExpectBegin() - mock.ExpectExec("UPDATE(.+)"). - WithArgs("/dst/test/1", "/dst/test/1/3", sqlmock.AnyArg(), 4). - WillReturnResult(sqlmock.NewResult(1, 1)) - mock.ExpectCommit() - // 获取子文件 - mock.ExpectQuery("SELECT(.+)"). - WithArgs(1, 3, 2, 4). - WillReturnRows( - sqlmock.NewRows([]string{"id", "folder_id", "dir", "size"}). - AddRow(1, 2, "/test", 10). - AddRow(2, 3, "/test/1", 20), - ) - // 移动子文件 - mock.ExpectBegin() - mock.ExpectExec("UPDATE(.+)"). - WithArgs("/dst/test", sqlmock.AnyArg(), 1). - WillReturnResult(sqlmock.NewResult(1, 1)) - mock.ExpectCommit() - mock.ExpectBegin() - mock.ExpectExec("UPDATE(.+)"). - WithArgs("/dst/test/1", sqlmock.AnyArg(), 2). - WillReturnResult(sqlmock.NewResult(1, 1)) - mock.ExpectCommit() - - storage, err := parFolder.MoveOrCopyFolderTo([]string{"/test"}, &dstFolder, false) - asserts.NoError(mock.ExpectationsWereMet()) - asserts.NoError(err) - asserts.Equal(uint64(0), storage) - } - - // 无法移动顶层目录 - { - // 查找所有递归子目录,包括自身 - mock.ExpectQuery("SELECT(.+)"). - WithArgs(1, sqlmock.AnyArg(), 1, "/test"). - WillReturnRows( - sqlmock.NewRows([]string{"id", "parent_id", "name", "position", "position_absolute"}). - AddRow(3, 2, "1", "/test", "/test/1"). - AddRow(2, 1, "test", "/", "/test"). - AddRow(4, 3, "3", "/test/1", "/test/1/3"), - ) - // 更改顶级要移动目录的父目录指向 - mock.ExpectBegin() - mock.ExpectExec("UPDATE(.+)"). - WithArgs(10, "/dst", "/dst/", sqlmock.AnyArg(), "/test", 1). - WillReturnError(errors.New("error")) - mock.ExpectRollback() - - storage, err := parFolder.MoveOrCopyFolderTo([]string{"/test"}, &dstFolder, false) - asserts.NoError(mock.ExpectationsWereMet()) - asserts.Error(err) - asserts.Equal(uint64(0), storage) - } -} +//func TestFolder_MoveOrCopyFileTo(t *testing.T) { +// asserts := assert.New(t) +// // 当前目录 +// folder := Folder{ +// OwnerID: 1, +// PositionAbsolute: "/test", +// } +// // 目标目录 +// dstFolder := Folder{ +// Model: gorm.Model{ID: 10}, +// PositionAbsolute: "/dst", +// } +// +// // 复制文件 +// { +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs( +// "1.txt", +// "2.txt", +// 1, +// "/test", +// ).WillReturnRows( +// sqlmock.NewRows([]string{"id", "size"}). +// AddRow(1, 10). +// AddRow(2, 20), +// ) +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) +// mock.ExpectCommit() +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) +// mock.ExpectCommit() +// storage, err := folder.MoveOrCopyFileTo( +// []string{"1.txt", "2.txt"}, +// &dstFolder, +// true, +// ) +// asserts.NoError(err) +// asserts.NoError(mock.ExpectationsWereMet()) +// asserts.Equal(uint64(30), storage) +// } +// +// // 复制文件, 检索文件出错 +// { +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs( +// "1.txt", +// "2.txt", +// 1, +// "/test", +// ).WillReturnError(errors.New("error")) +// +// storage, err := folder.MoveOrCopyFileTo( +// []string{"1.txt", "2.txt"}, +// &dstFolder, +// true, +// ) +// asserts.Error(err) +// asserts.NoError(mock.ExpectationsWereMet()) +// asserts.Equal(uint64(0), storage) +// } +// +// // 复制文件,第二个文件插入出错 +// { +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs( +// "1.txt", +// "2.txt", +// 1, +// "/test", +// ).WillReturnRows( +// sqlmock.NewRows([]string{"id", "size"}). +// AddRow(1, 10). +// AddRow(2, 20), +// ) +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) +// mock.ExpectCommit() +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)").WillReturnError(errors.New("error")) +// mock.ExpectRollback() +// storage, err := folder.MoveOrCopyFileTo( +// []string{"1.txt", "2.txt"}, +// &dstFolder, +// true, +// ) +// asserts.Error(err) +// asserts.NoError(mock.ExpectationsWereMet()) +// asserts.Equal(uint64(10), storage) +// } +// +// // 移动文件 成功 +// { +// mock.ExpectBegin() +// mock.ExpectExec("UPDATE(.+)"). +// WithArgs("/dst", 10, sqlmock.AnyArg(), "1.txt", "2.txt", 1, "/test"). +// WillReturnResult(sqlmock.NewResult(1, 2)) +// mock.ExpectCommit() +// storage, err := folder.MoveOrCopyFileTo( +// []string{"1.txt", "2.txt"}, +// &dstFolder, +// false, +// ) +// asserts.NoError(mock.ExpectationsWereMet()) +// asserts.NoError(err) +// asserts.Equal(uint64(0), storage) +// } +// +// // 移动文件 出错 +// { +// mock.ExpectBegin() +// mock.ExpectExec("UPDATE(.+)"). +// WithArgs("/dst", 10, sqlmock.AnyArg(), "1.txt", "2.txt", 1, "/test"). +// WillReturnError(errors.New("error")) +// mock.ExpectRollback() +// storage, err := folder.MoveOrCopyFileTo( +// []string{"1.txt", "2.txt"}, +// &dstFolder, +// false, +// ) +// asserts.NoError(mock.ExpectationsWereMet()) +// asserts.Error(err) +// asserts.Equal(uint64(0), storage) +// } +//} +// +//func TestFolder_MoveOrCopyFolderTo_Copy(t *testing.T) { +// conf.DatabaseConfig.Type = "mysql" +// asserts := assert.New(t) +// // 父目录 +// parFolder := Folder{ +// OwnerID: 1, +// PositionAbsolute: "/", +// } +// // 目标目录 +// dstFolder := Folder{ +// Model: gorm.Model{ID: 10}, +// PositionAbsolute: "/dst", +// } +// +// // 测试复制目录结构 +// // test(2) +// // 1(3) 2.txt +// // 3(4) 4.txt +// +// // 正常情况 成功 +// { +// // 查找所有递归子目录,包括自身 +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs(1, sqlmock.AnyArg(), 1, "/test"). +// WillReturnRows( +// sqlmock.NewRows([]string{"id", "parent_id", "name", "position", "position_absolute"}). +// AddRow(3, 2, "1", "/test", "/test/1"). +// AddRow(2, 1, "test", "/", "/test"). +// AddRow(4, 3, "3", "/test/1", "/test/1/3"), +// ) +// // 查找顶级待复制目录 +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs("/test", 1). +// WillReturnRows( +// sqlmock.NewRows( +// []string{"id", "parent_id", "name", "position", "position_absolute"}). +// AddRow(2, 1, "test", "/", "/test"), +// ) +// +// // 更新顶级目录 +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)"). +// WillReturnResult(sqlmock.NewResult(5, 1)) +// mock.ExpectCommit() +// // 更新子目录 +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)"). +// WillReturnResult(sqlmock.NewResult(6, 1)) +// mock.ExpectCommit() +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)"). +// WillReturnResult(sqlmock.NewResult(7, 1)) +// mock.ExpectCommit() +// +// // 获取子目录下的所有子文件 +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs(1, 3, 2, 4). +// WillReturnRows( +// sqlmock.NewRows([]string{"id", "folder_id", "dir", "size"}). +// AddRow(1, 2, "/test", 10). +// AddRow(2, 3, "/test/1", 20), +// ) +// +// // 更新子文件记录 +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) +// mock.ExpectCommit() +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) +// mock.ExpectCommit() +// +// storage, err := parFolder.MoveFolderTo([]string{"/test"}, &dstFolder, true) +// asserts.NoError(mock.ExpectationsWereMet()) +// asserts.NoError(err) +// asserts.Equal(uint64(30), storage) +// +// } +// +// // 处理子目录时死循环避免 +// { +// // 查找所有递归子目录,包括自身 +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs(1, sqlmock.AnyArg(), 1, "/test"). +// WillReturnRows( +// sqlmock.NewRows([]string{"id", "parent_id", "name", "position", "position_absolute"}). +// AddRow(3, 2, "1", "/test", "/test/1"). +// AddRow(2, 1, "test", "/", "/1"). +// AddRow(4, 3, "3", "/test/1", "/test/1/3"), +// ) +// // 查找顶级待复制目录 +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs("/test", 1). +// WillReturnRows( +// sqlmock.NewRows( +// []string{"id", "parent_id", "name", "position", "position_absolute"}). +// AddRow(2, 1, "test", "/", "/test"), +// ) +// +// // 更新顶级目录 +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)"). +// WillReturnResult(sqlmock.NewResult(5, 1)) +// mock.ExpectCommit() +// // 更新子目录 +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)"). +// WillReturnResult(sqlmock.NewResult(6, 1)) +// mock.ExpectCommit() +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)"). +// WillReturnResult(sqlmock.NewResult(6, 1)) +// mock.ExpectCommit() +// +// storage, err := parFolder.MoveFolderTo([]string{"/test"}, &dstFolder, true) +// asserts.NoError(mock.ExpectationsWereMet()) +// asserts.Error(err) +// asserts.Equal(uint64(0), storage) +// +// } +// +// // 检索子目录出错 +// { +// // 查找所有递归子目录,包括自身 +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs(1, sqlmock.AnyArg(), 1, "/test"). +// WillReturnError(errors.New("error")) +// +// storage, err := parFolder.MoveFolderTo([]string{"/test"}, &dstFolder, true) +// asserts.NoError(mock.ExpectationsWereMet()) +// asserts.Error(err) +// asserts.Equal(uint64(0), storage) +// +// } +// +// // 寻找原始目录出错 +// { +// // 查找所有递归子目录,包括自身 +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs(1, sqlmock.AnyArg(), 1, "/test"). +// WillReturnRows( +// sqlmock.NewRows([]string{"id", "parent_id", "name", "position", "position_absolute"}). +// AddRow(3, 2, "1", "/test", "/test/1"). +// AddRow(2, 1, "test", "/", "/test"). +// AddRow(4, 3, "3", "/test/1", "/test/1/3"), +// ) +// // 查找顶级待复制目录 +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs("/test", 1). +// WillReturnError(errors.New("error")) +// +// storage, err := parFolder.MoveFolderTo([]string{"/test"}, &dstFolder, true) +// asserts.NoError(mock.ExpectationsWereMet()) +// asserts.Error(err) +// asserts.Equal(uint64(0), storage) +// +// } +// +// // 更新顶级目录出错 +// { +// // 查找所有递归子目录,包括自身 +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs(1, sqlmock.AnyArg(), 1, "/test"). +// WillReturnRows( +// sqlmock.NewRows([]string{"id", "parent_id", "name", "position", "position_absolute"}). +// AddRow(3, 2, "1", "/test", "/test/1"). +// AddRow(2, 1, "test", "/", "/test"). +// AddRow(4, 3, "3", "/test/1", "/test/1/3"), +// ) +// // 查找顶级待复制目录 +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs("/test", 1). +// WillReturnRows( +// sqlmock.NewRows( +// []string{"id", "parent_id", "name", "position", "position_absolute"}). +// AddRow(2, 1, "test", "/", "/test"), +// ) +// +// // 更新顶级目录 +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)"). +// WillReturnError(errors.New("error")) +// mock.ExpectRollback() +// +// storage, err := parFolder.MoveFolderTo([]string{"/test"}, &dstFolder, true) +// asserts.NoError(mock.ExpectationsWereMet()) +// asserts.Error(err) +// asserts.Equal(uint64(0), storage) +// +// } +// +// // 复制子目录,一个成功一个失败 +// { +// // 查找所有递归子目录,包括自身 +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs(1, sqlmock.AnyArg(), 1, "/test"). +// WillReturnRows( +// sqlmock.NewRows([]string{"id", "parent_id", "name", "position", "position_absolute"}). +// AddRow(3, 2, "1", "/test", "/test/1"). +// AddRow(2, 1, "test", "/", "/test"). +// AddRow(4, 3, "3", "/test/1", "/test/1/3"), +// ) +// // 查找顶级待复制目录 +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs("/test", 1). +// WillReturnRows( +// sqlmock.NewRows( +// []string{"id", "parent_id", "name", "position", "position_absolute"}). +// AddRow(2, 1, "test", "/", "/test"), +// ) +// +// // 更新顶级目录 +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)"). +// WillReturnResult(sqlmock.NewResult(5, 1)) +// mock.ExpectCommit() +// // 更新子目录 +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)"). +// WillReturnResult(sqlmock.NewResult(6, 1)) +// mock.ExpectCommit() +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)"). +// WillReturnError(errors.New("error")) +// mock.ExpectRollback() +// +// storage, err := parFolder.MoveFolderTo([]string{"/test"}, &dstFolder, true) +// asserts.NoError(mock.ExpectationsWereMet()) +// asserts.Error(err) +// asserts.Equal(uint64(0), storage) +// +// } +// +// // 复制文件,一个成功一个失败 +// { +// // 查找所有递归子目录,包括自身 +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs(1, sqlmock.AnyArg(), 1, "/test"). +// WillReturnRows( +// sqlmock.NewRows([]string{"id", "parent_id", "name", "position", "position_absolute"}). +// AddRow(3, 2, "1", "/test", "/test/1"). +// AddRow(2, 1, "test", "/", "/test"). +// AddRow(4, 3, "3", "/test/1", "/test/1/3"), +// ) +// // 查找顶级待复制目录 +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs("/test", 1). +// WillReturnRows( +// sqlmock.NewRows( +// []string{"id", "parent_id", "name", "position", "position_absolute"}). +// AddRow(2, 1, "test", "/", "/test"), +// ) +// +// // 更新顶级目录 +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)"). +// WillReturnResult(sqlmock.NewResult(5, 1)) +// mock.ExpectCommit() +// // 更新子目录 +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)"). +// WillReturnResult(sqlmock.NewResult(6, 1)) +// mock.ExpectCommit() +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)"). +// WillReturnResult(sqlmock.NewResult(7, 1)) +// mock.ExpectCommit() +// +// // 获取子目录下的所有子文件 +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs(1, 3, 2, 4). +// WillReturnRows( +// sqlmock.NewRows([]string{"id", "folder_id", "dir", "size"}). +// AddRow(1, 2, "/test", 10). +// AddRow(2, 3, "/test/1", 20), +// ) +// +// // 更新子文件记录 +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) +// mock.ExpectCommit() +// mock.ExpectBegin() +// mock.ExpectExec("INSERT(.+)").WillReturnError(errors.New("error")) +// mock.ExpectRollback() +// +// storage, err := parFolder.MoveFolderTo([]string{"/test"}, &dstFolder, true) +// asserts.NoError(mock.ExpectationsWereMet()) +// asserts.NoError(err) +// asserts.Equal(uint64(10), storage) +// +// } +// +//} + +//func TestFolder_MoveOrCopyFolderTo_Move(t *testing.T) { +// conf.DatabaseConfig.Type = "mysql" +// asserts := assert.New(t) +// // 父目录 +// parFolder := Folder{ +// OwnerID: 1, +// PositionAbsolute: "/", +// } +// // 目标目录 +// dstFolder := Folder{ +// Model: gorm.Model{ID: 10}, +// PositionAbsolute: "/dst", +// } +// +// // 测试复制目录结构 +// // test(2) +// // 1(3) 2.txt +// // 3(4) 4.txt +// +// // 正常情况 成功 +// { +// // 查找所有递归子目录,包括自身 +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs(1, sqlmock.AnyArg(), 1, "/test"). +// WillReturnRows( +// sqlmock.NewRows([]string{"id", "parent_id", "name", "position", "position_absolute"}). +// AddRow(3, 2, "1", "/test", "/test/1"). +// AddRow(2, 1, "test", "/", "/test"). +// AddRow(4, 3, "3", "/test/1", "/test/1/3"), +// ) +// // 更改顶级要移动目录的父目录指向 +// mock.ExpectBegin() +// mock.ExpectExec("UPDATE(.+)"). +// WithArgs(10, "/dst", "/dst/", sqlmock.AnyArg(), "/test", 1). +// WillReturnResult(sqlmock.NewResult(1, 1)) +// mock.ExpectCommit() +// // 移动子目录 +// mock.ExpectBegin() +// mock.ExpectExec("UPDATE(.+)"). +// WithArgs("/dst/test", "/dst/test/1", sqlmock.AnyArg(), 3). +// WillReturnResult(sqlmock.NewResult(1, 1)) +// mock.ExpectCommit() +// mock.ExpectBegin() +// mock.ExpectExec("UPDATE(.+)"). +// WithArgs("/dst/test/1", "/dst/test/1/3", sqlmock.AnyArg(), 4). +// WillReturnResult(sqlmock.NewResult(1, 1)) +// mock.ExpectCommit() +// // 获取子文件 +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs(1, 3, 2, 4). +// WillReturnRows( +// sqlmock.NewRows([]string{"id", "folder_id", "dir", "size"}). +// AddRow(1, 2, "/test", 10). +// AddRow(2, 3, "/test/1", 20), +// ) +// // 移动子文件 +// mock.ExpectBegin() +// mock.ExpectExec("UPDATE(.+)"). +// WithArgs("/dst/test", sqlmock.AnyArg(), 1). +// WillReturnResult(sqlmock.NewResult(1, 1)) +// mock.ExpectCommit() +// mock.ExpectBegin() +// mock.ExpectExec("UPDATE(.+)"). +// WithArgs("/dst/test/1", sqlmock.AnyArg(), 2). +// WillReturnResult(sqlmock.NewResult(1, 1)) +// mock.ExpectCommit() +// +// storage, err := parFolder.MoveFolderTo([]string{"/test"}, &dstFolder, false) +// asserts.NoError(mock.ExpectationsWereMet()) +// asserts.NoError(err) +// asserts.Equal(uint64(0), storage) +// } +// +// // 无法移动顶层目录 +// { +// // 查找所有递归子目录,包括自身 +// mock.ExpectQuery("SELECT(.+)"). +// WithArgs(1, sqlmock.AnyArg(), 1, "/test"). +// WillReturnRows( +// sqlmock.NewRows([]string{"id", "parent_id", "name", "position", "position_absolute"}). +// AddRow(3, 2, "1", "/test", "/test/1"). +// AddRow(2, 1, "test", "/", "/test"). +// AddRow(4, 3, "3", "/test/1", "/test/1/3"), +// ) +// // 更改顶级要移动目录的父目录指向 +// mock.ExpectBegin() +// mock.ExpectExec("UPDATE(.+)"). +// WithArgs(10, "/dst", "/dst/", sqlmock.AnyArg(), "/test", 1). +// WillReturnError(errors.New("error")) +// mock.ExpectRollback() +// +// storage, err := parFolder.MoveFolderTo([]string{"/test"}, &dstFolder, false) +// asserts.NoError(mock.ExpectationsWereMet()) +// asserts.Error(err) +// asserts.Equal(uint64(0), storage) +// } +//} diff --git a/models/user.go b/models/user.go index 67e3440..daedf12 100644 --- a/models/user.go +++ b/models/user.go @@ -55,6 +55,13 @@ type UserOption struct { PreferredTheme string `json:"preferred_theme"` } +// Root 获取用户的根目录 +func (user *User) Root() (*Folder, error) { + var folder Folder + err := DB.Where("parent_id = 0 AND owner_id = ?", user.ID).First(&folder).Error + return &folder, err +} + // DeductionStorage 减少用户已用容量 func (user *User) DeductionStorage(size uint64) bool { if size == 0 { @@ -160,10 +167,8 @@ func (user *User) BeforeSave() (err error) { func (user *User) AfterCreate(tx *gorm.DB) (err error) { // 创建用户的默认根目录 defaultFolder := &Folder{ - Name: "根目录", - Position: ".", - OwnerID: user.ID, - PositionAbsolute: "/", + Name: "/", + OwnerID: user.ID, } tx.Create(defaultFolder) return err diff --git a/models/user_test.go b/models/user_test.go index 3b17663..d5f1023 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -306,3 +306,25 @@ func TestUser_AfterCreate(t *testing.T) { asserts.NoError(err) asserts.NoError(mock.ExpectationsWereMet()) } + +func TestUser_Root(t *testing.T) { + asserts := assert.New(t) + user := User{Model: gorm.Model{ID: 1}} + + // 根目录存在 + { + mock.ExpectQuery("SELECT(.+)").WithArgs(1).WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "根目录")) + root, err := user.Root() + asserts.NoError(mock.ExpectationsWereMet()) + asserts.NoError(err) + asserts.Equal("根目录", root.Name) + } + + // 根目录不存在 + { + mock.ExpectQuery("SELECT(.+)").WithArgs(1).WillReturnRows(sqlmock.NewRows([]string{"id", "name"})) + _, err := user.Root() + asserts.NoError(mock.ExpectationsWereMet()) + asserts.Error(err) + } +} diff --git a/pkg/filesystem/manage.go b/pkg/filesystem/manage.go new file mode 100644 index 0000000..2448cb6 --- /dev/null +++ b/pkg/filesystem/manage.go @@ -0,0 +1,336 @@ +package filesystem + +import ( + "context" + "fmt" + model "github.com/HFO4/cloudreve/models" + "github.com/HFO4/cloudreve/pkg/serializer" + "github.com/HFO4/cloudreve/pkg/util" + "path" +) + +/* ================= + 文件/目录管理 + ================= +*/ + +// Object 文件或者目录 +type Object struct { + ID uint `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Pic string `json:"pic"` + Size uint64 `json:"size"` + Type string `json:"type"` + Date string `json:"date"` +} + +// Rename 重命名对象 +func (fs *FileSystem) Rename(ctx context.Context, src, new string) (err error) { + // 验证新名字 + if !fs.ValidateLegalName(ctx, new) || !fs.ValidateExtension(ctx, new) { + return ErrIllegalObjectName + } + + // 如果源对象是文件 + fileExist, file := fs.IsFileExist(src) + if fileExist { + err = file.Rename(new) + return err + } + + // 源对象是目录 + folderExist, folder := fs.IsPathExist(src) + if folderExist { + err = folder.Rename(new) + return err + } + + return ErrPathNotExist +} + +// Copy 复制src目录下的文件或目录到dst, +// 暂时只支持单文件 +func (fs *FileSystem) Copy(ctx context.Context, dirs, files []uint, src, dst string) error { + // 获取目的目录 + isDstExist, dstFolder := fs.IsPathExist(dst) + isSrcExist, srcFolder := fs.IsPathExist(src) + // 不存在时返回空的结果 + if !isDstExist || !isSrcExist { + return ErrPathNotExist + } + + // 记录复制的文件的总容量 + var newUsedStorage uint64 + + // 复制目录 + if len(dirs) > 0 { + subFileSizes, err := srcFolder.CopyFolderTo(dirs[0], dstFolder) + if err != nil { + return serializer.NewError(serializer.CodeDBError, "操作失败,可能有重名冲突", err) + } + newUsedStorage += subFileSizes + } + + // 复制文件 + if len(files) > 0 { + subFileSizes, err := srcFolder.MoveOrCopyFileTo(files, dstFolder, true) + if err != nil { + return serializer.NewError(serializer.CodeDBError, "操作失败,可能有重名冲突", err) + } + newUsedStorage += subFileSizes + } + + // 扣除容量 + fs.User.IncreaseStorageWithoutCheck(newUsedStorage) + + return nil +} + +// Move 移动文件和目录 +func (fs *FileSystem) Move(ctx context.Context, dirs, files []uint, src, dst string) error { + // 获取目的目录 + isDstExist, dstFolder := fs.IsPathExist(dst) + isSrcExist, srcFolder := fs.IsPathExist(src) + // 不存在时返回空的结果 + if !isDstExist || !isSrcExist { + return ErrPathNotExist + } + + // 处理目录及子文件移动 + err := srcFolder.MoveFolderTo(dirs, dstFolder) + if err != nil { + return serializer.NewError(serializer.CodeDBError, "操作失败,可能有重名冲突", err) + } + + // 处理文件移动 + _, err = srcFolder.MoveOrCopyFileTo(files, dstFolder, false) + if err != nil { + return serializer.NewError(serializer.CodeDBError, "操作失败,可能有重名冲突", err) + } + + // 移动文件 + + return err +} + +// Delete 递归删除对象 +func (fs *FileSystem) Delete(ctx context.Context, dirs, files []uint) 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 ErrDBDeleteObjects.WithError(err) + } + + // 归还容量 + var total uint64 + for _, value := range deletedStorage { + total += value + } + fs.User.DeductionStorage(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 ErrDBDeleteObjects.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, ids []uint) error { + // 列出所有递归子目录 + folders, err := model.GetRecursiveChildFolder(ids, fs.User.ID, true) + 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, ids []uint) error { + files, err := model.GetFilesByIDs(ids, fs.User.ID) + if err != nil { + return ErrDBListObjects.WithError(err) + } + fs.SetTargetFile(&files) + return nil +} + +// List 列出路径下的内容, +// pathProcessor为最终对象路径的处理钩子。 +// 有些情况下(如在分享页面列对象)时, +// 路径需要截取掉被分享目录路径之前的部分。 +func (fs *FileSystem) List(ctx context.Context, dirPath string, pathProcessor func(string) string) ([]Object, error) { + // 获取父目录 + isExist, folder := fs.IsPathExist(dirPath) + // 不存在时返回空的结果 + if !isExist { + return []Object{}, nil + } + + var parentPath = path.Join(folder.PositionTemp, folder.Name) + var childFolders []model.Folder + var childFiles []model.File + + // 获取子目录 + childFolders, _ = folder.GetChildFolder() + + // 获取子文件 + childFiles, _ = folder.GetChildFiles() + + // 汇总处理结果 + objects := make([]Object, 0, len(childFiles)+len(childFolders)) + // 所有对象的父目录 + var processedPath string + + for _, subFolder := range childFolders { + // 路径处理钩子, + // 所有对象父目录都是一样的,所以只处理一次 + if processedPath == "" { + if pathProcessor != nil { + processedPath = pathProcessor(parentPath) + } else { + processedPath = parentPath + } + } + + objects = append(objects, Object{ + ID: subFolder.ID, + Name: subFolder.Name, + Path: processedPath, + Pic: "", + Size: 0, + Type: "dir", + Date: subFolder.CreatedAt.Format("2006-01-02 15:04:05"), + }) + } + + for _, file := range childFiles { + if processedPath == "" { + if pathProcessor != nil { + processedPath = pathProcessor(parentPath) + } else { + processedPath = parentPath + } + } + + objects = append(objects, Object{ + ID: file.ID, + Name: file.Name, + Path: processedPath, + Pic: file.PicInfo, + Size: file.Size, + Type: "file", + Date: file.CreatedAt.Format("2006-01-02 15:04:05"), + }) + } + + return objects, nil +} + +// CreateDirectory 根据给定的完整创建目录,不会递归创建 +func (fs *FileSystem) CreateDirectory(ctx context.Context, fullPath string) error { + // 获取要创建目录的父路径和目录名 + fullPath = path.Clean(fullPath) + base := path.Dir(fullPath) + dir := path.Base(fullPath) + + // 检查目录名是否合法 + if !fs.ValidateLegalName(ctx, dir) { + return ErrIllegalObjectName + } + + // 父目录是否存在 + isExist, parent := fs.IsPathExist(base) + if !isExist { + return ErrPathNotExist + } + + // 是否有同名文件 + if ok, _ := fs.IsChildFileExist(parent, dir); ok { + return ErrFileExisted + } + + // 创建目录 + newFolder := model.Folder{ + Name: dir, + ParentID: parent.ID, + OwnerID: fs.User.ID, + } + _, err := newFolder.Create() + + if err != nil { + return ErrFolderExisted + } + return nil +} diff --git a/pkg/filesystem/manage_test.go b/pkg/filesystem/manage_test.go new file mode 100644 index 0000000..14f04f3 --- /dev/null +++ b/pkg/filesystem/manage_test.go @@ -0,0 +1,453 @@ +package filesystem + +import ( + "context" + "errors" + "github.com/DATA-DOG/go-sqlmock" + model "github.com/HFO4/cloudreve/models" + "github.com/HFO4/cloudreve/pkg/conf" + "github.com/HFO4/cloudreve/pkg/serializer" + "github.com/jinzhu/gorm" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func TestFileSystem_List(t *testing.T) { + asserts := assert.New(t) + fs := &FileSystem{User: &model.User{ + Model: gorm.Model{ + ID: 1, + }, + }} + ctx := context.Background() + + // 成功,子目录包含文件和路径,不使用路径处理钩子 + // 根目录 + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "owner_id"}).AddRow(1, "/", 1)) + // folder + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1, 1, "folder"). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "owner_id"}).AddRow(5, "folder", 1)) + + mock.ExpectQuery("SELECT(.+)folder(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(6, "sub_folder1").AddRow(7, "sub_folder2")) + mock.ExpectQuery("SELECT(.+)file(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(6, "sub_file1.txt").AddRow(7, "sub_file2.txt")) + objects, err := fs.List(ctx, "/folder", nil) + asserts.Len(objects, 4) + asserts.NoError(err) + asserts.NoError(mock.ExpectationsWereMet()) + + // 成功,子目录包含文件和路径,使用路径处理钩子 + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "owner_id"}).AddRow(1, "/", 1)) + // folder + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1, 1, "folder"). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "owner_id"}).AddRow(2, "folder", 1)) + + mock.ExpectQuery("SELECT(.+)folder(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "position"}).AddRow(6, "sub_folder1", "/folder").AddRow(7, "sub_folder2", "/folder")) + mock.ExpectQuery("SELECT(.+)file(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "dir"}).AddRow(6, "sub_file1.txt", "/folder").AddRow(7, "sub_file2.txt", "/folder")) + objects, err = fs.List(ctx, "/folder", func(s string) string { + return "prefix" + s + }) + asserts.Len(objects, 4) + asserts.NoError(err) + asserts.NoError(mock.ExpectationsWereMet()) + for _, value := range objects { + asserts.Contains(value.Path, "prefix/") + } + + // 成功,子目录包含路径,使用路径处理钩子 + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "owner_id"}).AddRow(1, "/", 1)) + // folder + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1, 1, "folder"). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "owner_id"}).AddRow(2, "folder", 1)) + + mock.ExpectQuery("SELECT(.+)folder(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "position"})) + mock.ExpectQuery("SELECT(.+)file(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "dir"}).AddRow(6, "sub_file1.txt", "/folder").AddRow(7, "sub_file2.txt", "/folder")) + objects, err = fs.List(ctx, "/folder", func(s string) string { + return "prefix" + s + }) + asserts.Len(objects, 2) + asserts.NoError(err) + asserts.NoError(mock.ExpectationsWereMet()) + for _, value := range objects { + asserts.Contains(value.Path, "prefix/") + } + + // 成功,子目录下为空,使用路径处理钩子 + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "owner_id"}).AddRow(1, "/", 1)) + // folder + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1, 1, "folder"). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "owner_id"}).AddRow(2, "folder", 1)) + + mock.ExpectQuery("SELECT(.+)folder(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "position"})) + mock.ExpectQuery("SELECT(.+)file(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "dir"})) + objects, err = fs.List(ctx, "/folder", func(s string) string { + return "prefix" + s + }) + asserts.Len(objects, 0) + asserts.NoError(err) + asserts.NoError(mock.ExpectationsWereMet()) + + // 成功,子目录路径不存在 + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "owner_id"}).AddRow(1, "/", 1)) + // folder + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1, 1, "folder"). + WillReturnRows(sqlmock.NewRows([]string{"id", "name", "owner_id"})) + + objects, err = fs.List(ctx, "/folder", func(s string) string { + return "prefix" + s + }) + asserts.Len(objects, 0) + asserts.NoError(mock.ExpectationsWereMet()) +} + +func TestFileSystem_CreateDirectory(t *testing.T) { + asserts := assert.New(t) + fs := &FileSystem{User: &model.User{ + Model: gorm.Model{ + ID: 1, + }, + }} + ctx := context.Background() + + // 目录名非法 + err := fs.CreateDirectory(ctx, "/ad/a+?") + asserts.Equal(ErrIllegalObjectName, err) + + // 父目录不存在 + mock.ExpectQuery("SELECT(.+)folders").WillReturnRows(sqlmock.NewRows([]string{"id", "name"})) + err = fs.CreateDirectory(ctx, "/ad/ab") + asserts.Equal(ErrPathNotExist, err) + asserts.NoError(mock.ExpectationsWereMet()) + + // 存在同名文件 + // 根目录 + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}).AddRow(1, 1)) + // ad + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1, 1, "ad"). + WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}).AddRow(2, 1)) + + mock.ExpectQuery("SELECT(.+)files").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "ab")) + err = fs.CreateDirectory(ctx, "/ad/ab") + asserts.Equal(ErrFileExisted, err) + asserts.NoError(mock.ExpectationsWereMet()) + + // 存在同名目录 + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}).AddRow(1, 1)) + // ad + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1, 1, "ad"). + WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}).AddRow(2, 1)) + + mock.ExpectQuery("SELECT(.+)files").WillReturnRows(sqlmock.NewRows([]string{"id", "name"})) + mock.ExpectBegin() + mock.ExpectExec("INSERT(.+)").WillReturnError(errors.New("s")) + mock.ExpectRollback() + err = fs.CreateDirectory(ctx, "/ad/ab") + asserts.Error(err) + asserts.NoError(mock.ExpectationsWereMet()) + + // 成功创建 + // 根目录 + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}).AddRow(1, 1)) + // ad + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1, 1, "ad"). + WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}).AddRow(2, 1)) + + mock.ExpectQuery("SELECT(.+)files").WillReturnRows(sqlmock.NewRows([]string{"id", "name"})) + mock.ExpectBegin() + mock.ExpectExec("INSERT(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + err = fs.CreateDirectory(ctx, "/ad/ab") + asserts.NoError(err) + asserts.NoError(mock.ExpectationsWereMet()) +} + +func TestFileSystem_ListDeleteFiles(t *testing.T) { + conf.DatabaseConfig.Type = "mysql" + asserts := assert.New(t) + fs := &FileSystem{User: &model.User{ + Model: gorm.Model{ + ID: 1, + }, + }} + + // 成功 + { + mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "1.txt").AddRow(2, "2.txt")) + err := fs.ListDeleteFiles(context.Background(), []uint{1}) + asserts.NoError(err) + asserts.NoError(mock.ExpectationsWereMet()) + } + + // 失败 + { + mock.ExpectQuery("SELECT(.+)").WillReturnError(errors.New("error")) + err := fs.ListDeleteFiles(context.Background(), []uint{1}) + asserts.Error(err) + asserts.Equal(serializer.CodeDBError, err.(serializer.AppError).Code) + asserts.NoError(mock.ExpectationsWereMet()) + } +} + +func TestFileSystem_ListDeleteDirs(t *testing.T) { + conf.DatabaseConfig.Type = "mysql" + asserts := assert.New(t) + fs := &FileSystem{User: &model.User{ + Model: gorm.Model{ + ID: 1, + }, + }} + + // 成功 + { + mock.ExpectQuery("SELECT(.+)"). + WillReturnRows( + sqlmock.NewRows([]string{"id"}). + AddRow(1). + AddRow(2). + AddRow(3), + ) + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1, 2, 3). + WillReturnRows( + sqlmock.NewRows([]string{"id", "name"}). + AddRow(4, "1.txt"). + AddRow(5, "2.txt"). + AddRow(6, "3.txt"), + ) + err := fs.ListDeleteDirs(context.Background(), []uint{1}) + asserts.NoError(err) + asserts.Len(fs.FileTarget, 3) + asserts.Len(fs.DirTarget, 3) + asserts.NoError(mock.ExpectationsWereMet()) + } + + // 检索文件发生错误 + { + mock.ExpectQuery("SELECT(.+)"). + WillReturnRows( + sqlmock.NewRows([]string{"id"}). + AddRow(1). + AddRow(2). + AddRow(3), + ) + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1, 2, 3). + WillReturnError(errors.New("error")) + err := fs.ListDeleteDirs(context.Background(), []uint{1}) + asserts.Error(err) + asserts.Len(fs.DirTarget, 6) + asserts.NoError(mock.ExpectationsWereMet()) + } + // 检索目录发生错误 + { + mock.ExpectQuery("SELECT(.+)"). + WillReturnError(errors.New("error")) + err := fs.ListDeleteDirs(context.Background(), []uint{1}) + asserts.Error(err) + asserts.NoError(mock.ExpectationsWereMet()) + } +} + +func TestFileSystem_Delete(t *testing.T) { + conf.DatabaseConfig.Type = "mysql" + asserts := assert.New(t) + fs := &FileSystem{User: &model.User{ + Model: gorm.Model{ + ID: 1, + }, + Storage: 3, + Group: model.Group{MaxStorage: 3}, + }} + ctx := context.Background() + + // 全部未成功 + { + // 列出要删除的目录 + mock.ExpectQuery("SELECT(.+)"). + WillReturnRows( + sqlmock.NewRows([]string{"id"}). + AddRow(1). + AddRow(2). + AddRow(3), + ) + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1, 2, 3). + WillReturnRows( + sqlmock.NewRows([]string{"id", "name", "source_name", "policy_id", "size"}). + AddRow(4, "1.txt", "1.txt", 2, 1), + ) + // 查询顶级的文件 + mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "source_name", "policy_id", "size"}).AddRow(1, "1.txt", "1.txt", 1, 2)) + mock.ExpectQuery("SELECT(.+)files(.+)"). + WillReturnRows(sqlmock.NewRows([]string{"id", "policy_id", "source_name"})) + // 查询上传策略 + mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "type"}).AddRow(1, "local")) + mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "type"}).AddRow(1, "local")) + // 删除文件记录 + mock.ExpectBegin() + mock.ExpectExec("DELETE(.+)files"). + WillReturnResult(sqlmock.NewResult(0, 3)) + mock.ExpectCommit() + // 归还容量 + mock.ExpectBegin() + mock.ExpectExec("UPDATE(.+)users"). + WillReturnResult(sqlmock.NewResult(0, 3)) + mock.ExpectCommit() + // 删除目录 + mock.ExpectBegin() + mock.ExpectExec("DELETE(.+)folders"). + WillReturnResult(sqlmock.NewResult(0, 3)) + mock.ExpectCommit() + + err := fs.Delete(ctx, []uint{1}, []uint{1}) + asserts.Error(err) + asserts.Equal(203, err.(serializer.AppError).Code) + asserts.Equal(uint64(0), fs.User.Storage) + } + // 全部成功 + { + file, err := os.Create("1.txt") + file2, err := os.Create("2.txt") + file.Close() + file2.Close() + asserts.NoError(err) + mock.ExpectQuery("SELECT(.+)"). + WillReturnRows( + sqlmock.NewRows([]string{"id"}). + AddRow(1). + AddRow(2). + AddRow(3), + ) + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1, 2, 3). + WillReturnRows( + sqlmock.NewRows([]string{"id", "name", "source_name", "policy_id", "size"}). + AddRow(4, "1.txt", "1.txt", 2, 1), + ) + mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "source_name", "policy_id", "size"}).AddRow(1, "2.txt", "2.txt", 1, 2)) + mock.ExpectQuery("SELECT(.+)files(.+)"). + WillReturnRows(sqlmock.NewRows([]string{"id", "policy_id", "source_name"})) + // 查询上传策略 + mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "type"}).AddRow(1, "local")) + mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "type"}).AddRow(1, "local")) + // 删除文件记录 + mock.ExpectBegin() + mock.ExpectExec("DELETE(.+)"). + WillReturnResult(sqlmock.NewResult(0, 3)) + mock.ExpectCommit() + // 归还容量 + mock.ExpectBegin() + mock.ExpectExec("UPDATE(.+)"). + WillReturnResult(sqlmock.NewResult(0, 3)) + mock.ExpectCommit() + // 删除目录 + mock.ExpectBegin() + mock.ExpectExec("DELETE(.+)"). + WillReturnResult(sqlmock.NewResult(0, 3)) + mock.ExpectCommit() + + fs.FileTarget = []model.File{} + fs.DirTarget = []model.Folder{} + err = fs.Delete(ctx, []uint{1}, []uint{1}) + asserts.NoError(err) + asserts.Equal(uint64(0), fs.User.Storage) + } + +} + +func TestFileSystem_Copy(t *testing.T) { + asserts := assert.New(t) + fs := &FileSystem{User: &model.User{ + Model: gorm.Model{ + ID: 1, + }, + Storage: 3, + Group: model.Group{MaxStorage: 3}, + }} + ctx := context.Background() + + // 目录不存在 + { + mock.ExpectQuery("SELECT(.+)").WithArgs(uint(1), "/dst").WillReturnRows( + sqlmock.NewRows([]string{"name"}), + ) + mock.ExpectQuery("SELECT(.+)").WithArgs(uint(1), "/src").WillReturnRows( + sqlmock.NewRows([]string{"name"}), + ) + err := fs.Copy(ctx, []string{}, []string{}, "/src", "/dst") + asserts.Equal(ErrPathNotExist, err) + asserts.NoError(mock.ExpectationsWereMet()) + } + + // 复制目录出错 + { + mock.ExpectQuery("SELECT(.+)").WithArgs(uint(1), "/src").WillReturnRows( + sqlmock.NewRows([]string{"name"}).AddRow("123"), + ) + mock.ExpectQuery("SELECT(.+)").WithArgs(uint(1), "/dst").WillReturnRows( + sqlmock.NewRows([]string{"name"}).AddRow("123"), + ) + err := fs.Copy(ctx, []string{"test"}, []string{}, "/src", "/dst") + asserts.Error(err) + } + +} + +func TestFileSystem_Move(t *testing.T) { + conf.DatabaseConfig.Type = "mysql" + asserts := assert.New(t) + fs := &FileSystem{User: &model.User{ + Model: gorm.Model{ + ID: 1, + }, + Storage: 3, + Group: model.Group{MaxStorage: 3}, + }} + ctx := context.Background() + + // 目录不存在 + { + mock.ExpectQuery("SELECT(.+)").WillReturnRows( + sqlmock.NewRows([]string{"name"}), + ) + err := fs.Move(ctx, []string{}, []string{}, "/src", "/dst") + asserts.Equal(ErrPathNotExist, err) + asserts.NoError(mock.ExpectationsWereMet()) + } + + // 移动目录出错 + { + mock.ExpectQuery("SELECT(.+)").WithArgs(uint(1), "/src").WillReturnRows( + sqlmock.NewRows([]string{"name"}).AddRow("123"), + ) + mock.ExpectQuery("SELECT(.+)").WithArgs(uint(1), "/dst").WillReturnRows( + sqlmock.NewRows([]string{"name"}).AddRow("123"), + ) + err := fs.Move(ctx, []string{"test"}, []string{}, "/src", "/dst") + asserts.Error(err) + } +} diff --git a/pkg/filesystem/path.go b/pkg/filesystem/path.go index 583393d..b9dbfad 100644 --- a/pkg/filesystem/path.go +++ b/pkg/filesystem/path.go @@ -1,13 +1,9 @@ package filesystem import ( - "context" - "fmt" model "github.com/HFO4/cloudreve/models" - "github.com/HFO4/cloudreve/pkg/serializer" "github.com/HFO4/cloudreve/pkg/util" "path" - "sync" ) /* ================= @@ -15,335 +11,34 @@ import ( ================= */ -// Object 文件或者目录 -type Object struct { - ID uint `json:"id"` - Name string `json:"name"` - Path string `json:"path"` - Pic string `json:"pic"` - Size uint64 `json:"size"` - Type string `json:"type"` - Date string `json:"date"` -} - -// Rename 重命名对象 -func (fs *FileSystem) Rename(ctx context.Context, src, new string) (err error) { - // 验证新名字 - if !fs.ValidateLegalName(ctx, new) || !fs.ValidateExtension(ctx, new) { - return ErrIllegalObjectName - } - - // 如果源对象是文件 - fileExist, file := fs.IsFileExist(src) - if fileExist { - err = file.Rename(new) - return err - } - - // 源对象是目录 - folderExist, folder := fs.IsPathExist(src) - if folderExist { - err = folder.Rename(new) - return err - } - - return ErrPathNotExist -} - -// Copy 复制src目录下的文件或目录到dst -func (fs *FileSystem) Copy(ctx context.Context, dirs, files []string, src, dst string) error { - // 获取目的目录 - isDstExist, dstFolder := fs.IsPathExist(dst) - isSrcExist, srcFolder := fs.IsPathExist(src) - // 不存在时返回空的结果 - if !isDstExist || !isSrcExist { - return ErrPathNotExist - } - - // 记录复制的文件的总容量 - var newUsedStorage uint64 - - // 复制目录 - if len(dirs) > 0 { - subFileSizes, err := srcFolder.MoveOrCopyFolderTo(dirs, dstFolder, true) - if err != nil { - return serializer.NewError(serializer.CodeDBError, "操作失败,可能有重名冲突", err) - } - newUsedStorage += subFileSizes - } - - // 复制文件 - if len(files) > 0 { - subFileSizes, err := srcFolder.MoveOrCopyFileTo(files, dstFolder, true) - if err != nil { - return serializer.NewError(serializer.CodeDBError, "操作失败,可能有重名冲突", err) - } - newUsedStorage += subFileSizes - } - - // 扣除容量 - fs.User.IncreaseStorageWithoutCheck(newUsedStorage) - - return nil -} - -// Move 移动文件和目录 -func (fs *FileSystem) Move(ctx context.Context, dirs, files []string, src, dst string) error { - // 获取目的目录 - isDstExist, dstFolder := fs.IsPathExist(dst) - isSrcExist, srcFolder := fs.IsPathExist(src) - // 不存在时返回空的结果 - if !isDstExist || !isSrcExist { - return ErrPathNotExist - } - - // 处理目录及子文件移动 - _, err := srcFolder.MoveOrCopyFolderTo(dirs, dstFolder, false) - if err != nil { - return serializer.NewError(serializer.CodeDBError, "操作失败,可能有重名冲突", err) - } - - // 处理文件移动 - _, err = srcFolder.MoveOrCopyFileTo(files, dstFolder, false) - if err != nil { - return serializer.NewError(serializer.CodeDBError, "操作失败,可能有重名冲突", err) - } - - // 移动文件 - - return err -} - -// 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 ErrDBDeleteObjects.WithError(err) - } - - // 归还容量 - var total uint64 - for _, value := range deletedStorage { - total += value - } - fs.User.DeductionStorage(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 ErrDBDeleteObjects.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, true) - 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为最终对象路径的处理钩子。 -// 有些情况下(如在分享页面列对象)时, -// 路径需要截取掉被分享目录路径之前的部分。 -func (fs *FileSystem) List(ctx context.Context, dirPath string, pathProcessor func(string) string) ([]Object, error) { - // 获取父目录 - isExist, folder := fs.IsPathExist(dirPath) - // 不存在时返回空的结果 - if !isExist { - return []Object{}, nil +// IsPathExist 返回给定目录是否存在 +// 如果存在就返回目录 +func (fs *FileSystem) IsPathExist(path string) (bool, *model.Folder) { + pathList := util.SplitPath(path) + if len(pathList) == 0 { + return false, nil } - var wg sync.WaitGroup - var childFolders []model.Folder - var childFiles []model.File - wg.Add(2) - - // 获取子目录 - go func() { - childFolders, _ = folder.GetChildFolder() - wg.Done() - }() - - // 获取子文件 - go func() { - childFiles, _ = folder.GetChildFile() - wg.Done() - }() + // 递归步入目录 + var currentFolder *model.Folder + for _, folderName := range pathList { + var err error - // 汇总处理结果 - wg.Wait() - objects := make([]Object, 0, len(childFiles)+len(childFolders)) - // 所有对象的父目录 - var processedPath string - - for _, folder := range childFolders { - // 路径处理钩子, - // 所有对象父目录都是一样的,所以只处理一次 - if processedPath == "" { - if pathProcessor != nil { - processedPath = pathProcessor(folder.Position) - } else { - processedPath = folder.Position + // 根目录 + if folderName == "/" { + currentFolder, err = fs.User.Root() + if err != nil { + return false, nil } - } - - objects = append(objects, Object{ - ID: folder.ID, - Name: folder.Name, - Path: processedPath, - Pic: "", - Size: 0, - Type: "dir", - Date: folder.CreatedAt.Format("2006-01-02 15:04:05"), - }) - } - - for _, file := range childFiles { - if processedPath == "" { - if pathProcessor != nil { - processedPath = pathProcessor(file.Dir) - } else { - processedPath = file.Dir + } else { + currentFolder, err = currentFolder.GetChild(folderName) + if err != nil { + return false, nil } } - - objects = append(objects, Object{ - ID: file.ID, - Name: file.Name, - Path: processedPath, - Pic: file.PicInfo, - Size: file.Size, - Type: "file", - Date: file.CreatedAt.Format("2006-01-02 15:04:05"), - }) } - return objects, nil -} - -// CreateDirectory 根据给定的完整创建目录,不会递归创建 -func (fs *FileSystem) CreateDirectory(ctx context.Context, fullPath string) error { - // 获取要创建目录的父路径和目录名 - fullPath = path.Clean(fullPath) - base := path.Dir(fullPath) - dir := path.Base(fullPath) - // 检查目录名是否合法 - if !fs.ValidateLegalName(ctx, dir) { - return ErrIllegalObjectName - } - // 父目录是否存在 - isExist, parent := fs.IsPathExist(base) - if !isExist { - return ErrPathNotExist - } - // 是否有同名目录 - // TODO: 有了unique约束后可以不用在此检查 - b, _ := fs.IsPathExist(fullPath) - if b { - return ErrFolderExisted - } - // 是否有同名文件 - if ok, _ := fs.IsFileExist(path.Join(base, dir)); ok { - return ErrFileExisted - } - // 创建目录 - newFolder := model.Folder{ - Name: dir, - ParentID: parent.ID, - Position: base, - PositionAbsolute: fullPath, - OwnerID: fs.User.ID, - } - _, err := newFolder.Create() - return err -} - -// IsPathExist 返回给定目录是否存在 -// 如果存在就返回目录 -func (fs *FileSystem) IsPathExist(path string) (bool, *model.Folder) { - folder, err := model.GetFolderByPath(path, fs.User.ID) - return err == nil, &folder + return true, currentFolder } // IsFileExist 返回给定路径的文件是否存在 @@ -351,7 +46,19 @@ func (fs *FileSystem) IsFileExist(fullPath string) (bool, *model.File) { basePath := path.Dir(fullPath) fileName := path.Base(fullPath) - file, err := model.GetFileByPathAndName(basePath, fileName, fs.User.ID) + // 获得父目录 + exist, parent := fs.IsPathExist(basePath) + if !exist { + return false, nil + } + + file, err := parent.GetChildFile(fileName) + + return err == nil, file +} - return err == nil, &file +// IsChildFileExist 确定folder目录下是否有名为name的文件 +func (fs *FileSystem) IsChildFileExist(folder *model.Folder, name string) (bool, *model.File) { + file, err := folder.GetChildFile(name) + return err == nil, file } diff --git a/pkg/filesystem/path_test.go b/pkg/filesystem/path_test.go index 9da0bf9..c867a6a 100644 --- a/pkg/filesystem/path_test.go +++ b/pkg/filesystem/path_test.go @@ -1,15 +1,10 @@ package filesystem import ( - "context" - "errors" "github.com/DATA-DOG/go-sqlmock" model "github.com/HFO4/cloudreve/models" - "github.com/HFO4/cloudreve/pkg/conf" - "github.com/HFO4/cloudreve/pkg/serializer" "github.com/jinzhu/gorm" "github.com/stretchr/testify/assert" - "os" "testing" ) @@ -22,188 +17,46 @@ func TestFileSystem_IsFileExist(t *testing.T) { }} // 存在 - mock.ExpectQuery("SELECT(.+)").WithArgs(uint(1), "/s", "1.txt").WillReturnRows( - sqlmock.NewRows([]string{"Name"}).AddRow("s"), - ) - testResult, _ := fs.IsFileExist("/s/1.txt") - asserts.True(testResult) - asserts.NoError(mock.ExpectationsWereMet()) - - // 不存在 - mock.ExpectQuery("SELECT(.+)").WithArgs(uint(1), "/ss/dfsd", "1.txt").WillReturnRows( - sqlmock.NewRows([]string{"Name"}), - ) - testResult, _ = fs.IsFileExist("/ss/dfsd/1.txt") - asserts.False(testResult) - asserts.NoError(mock.ExpectationsWereMet()) -} - -func TestFileSystem_IsPathExist(t *testing.T) { - asserts := assert.New(t) - fs := &FileSystem{User: &model.User{ - Model: gorm.Model{ - ID: 1, - }, - }} - - // 存在 - mock.ExpectQuery("SELECT(.+)").WithArgs(uint(1), "/s/sda").WillReturnRows( - sqlmock.NewRows([]string{"name"}).AddRow("sda"), - ) - testResult, folder := fs.IsPathExist("/s/sda") - asserts.True(testResult) - asserts.Equal("sda", folder.Name) - asserts.NoError(mock.ExpectationsWereMet()) - - // 不存在 - mock.ExpectQuery("SELECT(.+)").WithArgs(uint(1), "/s/sda").WillReturnRows( - sqlmock.NewRows([]string{"name"}), - ) - testResult, _ = fs.IsPathExist("/s/sda") - asserts.False(testResult) - asserts.NoError(mock.ExpectationsWereMet()) -} - -func TestFileSystem_List(t *testing.T) { - asserts := assert.New(t) - fs := &FileSystem{User: &model.User{ - Model: gorm.Model{ - ID: 1, - }, - }} - ctx := context.Background() - - // 成功,子目录包含文件和路径,不使用路径处理钩子 - mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(5, "folder")) - mock.ExpectQuery("SELECT(.+)folder(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(6, "sub_folder1").AddRow(7, "sub_folder2")) - mock.ExpectQuery("SELECT(.+)file(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(6, "sub_file1.txt").AddRow(7, "sub_file2.txt")) - objects, err := fs.List(ctx, "/folder", nil) - asserts.Len(objects, 4) - asserts.NoError(err) - asserts.NoError(mock.ExpectationsWereMet()) - - // 成功,子目录包含文件和路径,使用路径处理钩子 - mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(5, "folder")) - mock.ExpectQuery("SELECT(.+)folder(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "position"}).AddRow(6, "sub_folder1", "/folder").AddRow(7, "sub_folder2", "/folder")) - mock.ExpectQuery("SELECT(.+)file(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "dir"}).AddRow(6, "sub_file1.txt", "/folder").AddRow(7, "sub_file2.txt", "/folder")) - objects, err = fs.List(ctx, "/folder", func(s string) string { - return "prefix" + s - }) - asserts.Len(objects, 4) - asserts.NoError(err) - asserts.NoError(mock.ExpectationsWereMet()) - for _, value := range objects { - asserts.Contains(value.Path, "prefix/") - } - - // 成功,子目录包含路径,使用路径处理钩子 - mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(5, "folder")) - mock.ExpectQuery("SELECT(.+)folder(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "position"})) - mock.ExpectQuery("SELECT(.+)file(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "dir"}).AddRow(6, "sub_file1.txt", "/folder").AddRow(7, "sub_file2.txt", "/folder")) - objects, err = fs.List(ctx, "/folder", func(s string) string { - return "prefix" + s - }) - asserts.Len(objects, 2) - asserts.NoError(err) - asserts.NoError(mock.ExpectationsWereMet()) - for _, value := range objects { - asserts.Contains(value.Path, "prefix/") + { + path := "/1.txt" + // 根目录 + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1)) + mock.ExpectQuery("SELECT(.+)").WithArgs(1, "1.txt").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "1.txt")) + exist, file := fs.IsFileExist(path) + asserts.NoError(mock.ExpectationsWereMet()) + asserts.True(exist) + asserts.Equal(uint(1), file.ID) } - // 成功,子目录下为空,使用路径处理钩子 - mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(5, "folder")) - mock.ExpectQuery("SELECT(.+)folder(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "position"})) - mock.ExpectQuery("SELECT(.+)file(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "dir"})) - objects, err = fs.List(ctx, "/folder", func(s string) string { - return "prefix" + s - }) - asserts.Len(objects, 0) - asserts.NoError(err) - asserts.NoError(mock.ExpectationsWereMet()) - - // 成功,子目录路径不存在 - mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name"})) - - objects, err = fs.List(ctx, "/folder", func(s string) string { - return "prefix" + s - }) - asserts.Len(objects, 0) - asserts.NoError(mock.ExpectationsWereMet()) -} - -func TestFileSystem_CreateDirectory(t *testing.T) { - asserts := assert.New(t) - fs := &FileSystem{User: &model.User{ - Model: gorm.Model{ - ID: 1, - }, - }} - ctx := context.Background() - - // 目录名非法 - err := fs.CreateDirectory(ctx, "/ad/a+?") - asserts.Equal(ErrIllegalObjectName, err) - - // 父目录不存在 - mock.ExpectQuery("SELECT(.+)folders").WillReturnRows(sqlmock.NewRows([]string{"id", "name"})) - err = fs.CreateDirectory(ctx, "/ad/ab") - asserts.Equal(ErrPathNotExist, err) - asserts.NoError(mock.ExpectationsWereMet()) - - // 存在同名文件 - mock.ExpectQuery("SELECT(.+)folders").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "ab")) - mock.ExpectQuery("SELECT(.+)files").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "ab")) - err = fs.CreateDirectory(ctx, "/ad/ab") - asserts.Equal(ErrFileExisted, err) - asserts.NoError(mock.ExpectationsWereMet()) - - // 存在同名目录 - mock.ExpectQuery("SELECT(.+)folders").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "ab")) - mock.ExpectQuery("SELECT(.+)folders").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "ab")) - err = fs.CreateDirectory(ctx, "/ad/ab") - asserts.Equal(ErrFolderExisted, err) - asserts.NoError(mock.ExpectationsWereMet()) - - // 成功创建 - mock.ExpectQuery("SELECT(.+)folders").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "ab")) - mock.ExpectQuery("SELECT(.+)files").WillReturnRows(sqlmock.NewRows([]string{"id", "name"})) - mock.ExpectBegin() - mock.ExpectExec("INSERT(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) - mock.ExpectCommit() - err = fs.CreateDirectory(ctx, "/ad/ab") - asserts.NoError(err) - asserts.NoError(mock.ExpectationsWereMet()) -} - -func TestFileSystem_ListDeleteFiles(t *testing.T) { - conf.DatabaseConfig.Type = "mysql" - asserts := assert.New(t) - fs := &FileSystem{User: &model.User{ - Model: gorm.Model{ - ID: 1, - }, - }} - - // 成功 + // 文件不存在 { - mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "1.txt").AddRow(2, "2.txt")) - err := fs.ListDeleteFiles(context.Background(), []string{"/"}) - asserts.NoError(err) + path := "/1.txt" + // 根目录 + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1)) + mock.ExpectQuery("SELECT(.+)").WithArgs(1, "1.txt").WillReturnRows(sqlmock.NewRows([]string{"id", "name"})) + exist, _ := fs.IsFileExist(path) asserts.NoError(mock.ExpectationsWereMet()) + asserts.False(exist) } - // 失败 + // 父目录不存在 { - mock.ExpectQuery("SELECT(.+)").WillReturnError(errors.New("error")) - err := fs.ListDeleteFiles(context.Background(), []string{"/"}) - asserts.Error(err) - asserts.Equal(serializer.CodeDBError, err.(serializer.AppError).Code) + path := "/1.txt" + // 根目录 + mock.ExpectQuery("SELECT(.+)"). + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{"id"})) + exist, _ := fs.IsFileExist(path) asserts.NoError(mock.ExpectationsWereMet()) + asserts.False(exist) } } -func TestFileSystem_ListDeleteDirs(t *testing.T) { - conf.DatabaseConfig.Type = "mysql" +func TestFileSystem_IsPathExist(t *testing.T) { asserts := assert.New(t) fs := &FileSystem{User: &model.User{ Model: gorm.Model{ @@ -211,233 +64,67 @@ func TestFileSystem_ListDeleteDirs(t *testing.T) { }, }} - // 成功 + // 查询根目录 { + path := "/" + // 根目录 mock.ExpectQuery("SELECT(.+)"). - WillReturnRows( - sqlmock.NewRows([]string{"id"}). - AddRow(1). - AddRow(2). - AddRow(3), - ) - mock.ExpectQuery("SELECT(.+)"). - WithArgs(1, 2, 3). - WillReturnRows( - sqlmock.NewRows([]string{"id", "name"}). - AddRow(4, "1.txt"). - AddRow(5, "2.txt"). - AddRow(6, "3.txt"), - ) - err := fs.ListDeleteDirs(context.Background(), []string{"/"}) - asserts.NoError(err) - asserts.Len(fs.FileTarget, 3) - asserts.Len(fs.DirTarget, 3) + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1)) + exist, folder := fs.IsPathExist(path) asserts.NoError(mock.ExpectationsWereMet()) + asserts.True(exist) + asserts.Equal(uint(1), folder.ID) } - // 检索文件发生错误 + // 深层路径 { + path := "/1/2/3" + // 根目录 mock.ExpectQuery("SELECT(.+)"). - WillReturnRows( - sqlmock.NewRows([]string{"id"}). - AddRow(1). - AddRow(2). - AddRow(3), - ) + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}).AddRow(1, 1)) + // 1 mock.ExpectQuery("SELECT(.+)"). - WithArgs(1, 2, 3). - WillReturnError(errors.New("error")) - err := fs.ListDeleteDirs(context.Background(), []string{"/"}) - asserts.Error(err) - asserts.Len(fs.DirTarget, 6) - asserts.NoError(mock.ExpectationsWereMet()) - } - // 检索目录发生错误 - { + WithArgs(1, 1, "1"). + WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}).AddRow(2, 1)) + // 2 + mock.ExpectQuery("SELECT(.+)"). + WithArgs(2, 1, "2"). + WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}).AddRow(3, 1)) + // 3 mock.ExpectQuery("SELECT(.+)"). - WillReturnError(errors.New("error")) - err := fs.ListDeleteDirs(context.Background(), []string{"/"}) - asserts.Error(err) + WithArgs(3, 1, "3"). + WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}).AddRow(4, 1)) + exist, folder := fs.IsPathExist(path) asserts.NoError(mock.ExpectationsWereMet()) + asserts.True(exist) + asserts.Equal(uint(4), folder.ID) } -} - -func TestFileSystem_Delete(t *testing.T) { - conf.DatabaseConfig.Type = "mysql" - asserts := assert.New(t) - fs := &FileSystem{User: &model.User{ - Model: gorm.Model{ - ID: 1, - }, - Storage: 3, - Group: model.Group{MaxStorage: 3}, - }} - ctx := context.Background() - // 全部未成功 + // 深层 不存在 { - // 列出要删除的目录 + path := "/1/2/3" + // 根目录 mock.ExpectQuery("SELECT(.+)"). - WillReturnRows( - sqlmock.NewRows([]string{"id"}). - AddRow(1). - AddRow(2). - AddRow(3), - ) + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}).AddRow(1, 1)) + // 1 mock.ExpectQuery("SELECT(.+)"). - WithArgs(1, 2, 3). - WillReturnRows( - sqlmock.NewRows([]string{"id", "name", "source_name", "policy_id", "size"}). - AddRow(4, "1.txt", "1.txt", 2, 1), - ) - // 查询顶级的文件 - mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "source_name", "policy_id", "size"}).AddRow(1, "1.txt", "1.txt", 1, 2)) - mock.ExpectQuery("SELECT(.+)files(.+)"). - WillReturnRows(sqlmock.NewRows([]string{"id", "policy_id", "source_name"})) - // 查询上传策略 - mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "type"}).AddRow(1, "local")) - mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "type"}).AddRow(1, "local")) - // 删除文件记录 - mock.ExpectBegin() - mock.ExpectExec("DELETE(.+)files"). - WillReturnResult(sqlmock.NewResult(0, 3)) - mock.ExpectCommit() - // 归还容量 - mock.ExpectBegin() - mock.ExpectExec("UPDATE(.+)users"). - WillReturnResult(sqlmock.NewResult(0, 3)) - mock.ExpectCommit() - // 删除目录 - mock.ExpectBegin() - mock.ExpectExec("DELETE(.+)folders"). - WillReturnResult(sqlmock.NewResult(0, 3)) - mock.ExpectCommit() - - err := fs.Delete(ctx, []string{"/"}, []string{"2.txt"}) - asserts.Error(err) - asserts.Equal(203, err.(serializer.AppError).Code) - asserts.Equal(uint64(0), fs.User.Storage) - } - // 全部成功 - { - file, err := os.Create("1.txt") - file2, err := os.Create("2.txt") - file.Close() - file2.Close() - asserts.NoError(err) + WithArgs(1, 1, "1"). + WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}).AddRow(2, 1)) + // 2 mock.ExpectQuery("SELECT(.+)"). - WillReturnRows( - sqlmock.NewRows([]string{"id"}). - AddRow(1). - AddRow(2). - AddRow(3), - ) + WithArgs(2, 1, "2"). + WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}).AddRow(3, 1)) + // 3 mock.ExpectQuery("SELECT(.+)"). - WithArgs(1, 2, 3). - WillReturnRows( - sqlmock.NewRows([]string{"id", "name", "source_name", "policy_id", "size"}). - AddRow(4, "1.txt", "1.txt", 2, 1), - ) - mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "name", "source_name", "policy_id", "size"}).AddRow(1, "2.txt", "2.txt", 1, 2)) - mock.ExpectQuery("SELECT(.+)files(.+)"). - WillReturnRows(sqlmock.NewRows([]string{"id", "policy_id", "source_name"})) - // 查询上传策略 - mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "type"}).AddRow(1, "local")) - mock.ExpectQuery("SELECT(.+)").WillReturnRows(sqlmock.NewRows([]string{"id", "type"}).AddRow(1, "local")) - // 删除文件记录 - mock.ExpectBegin() - mock.ExpectExec("DELETE(.+)"). - WillReturnResult(sqlmock.NewResult(0, 3)) - mock.ExpectCommit() - // 归还容量 - mock.ExpectBegin() - mock.ExpectExec("UPDATE(.+)"). - WillReturnResult(sqlmock.NewResult(0, 3)) - mock.ExpectCommit() - // 删除目录 - mock.ExpectBegin() - mock.ExpectExec("DELETE(.+)"). - WillReturnResult(sqlmock.NewResult(0, 3)) - mock.ExpectCommit() - - fs.FileTarget = []model.File{} - fs.DirTarget = []model.Folder{} - err = fs.Delete(ctx, []string{"/"}, []string{"2.txt"}) - asserts.NoError(err) - asserts.Equal(uint64(0), fs.User.Storage) - } - -} - -func TestFileSystem_Copy(t *testing.T) { - asserts := assert.New(t) - fs := &FileSystem{User: &model.User{ - Model: gorm.Model{ - ID: 1, - }, - Storage: 3, - Group: model.Group{MaxStorage: 3}, - }} - ctx := context.Background() - - // 目录不存在 - { - mock.ExpectQuery("SELECT(.+)").WithArgs(uint(1), "/dst").WillReturnRows( - sqlmock.NewRows([]string{"name"}), - ) - mock.ExpectQuery("SELECT(.+)").WithArgs(uint(1), "/src").WillReturnRows( - sqlmock.NewRows([]string{"name"}), - ) - err := fs.Copy(ctx, []string{}, []string{}, "/src", "/dst") - asserts.Equal(ErrPathNotExist, err) - asserts.NoError(mock.ExpectationsWereMet()) - } - - // 复制目录出错 - { - mock.ExpectQuery("SELECT(.+)").WithArgs(uint(1), "/src").WillReturnRows( - sqlmock.NewRows([]string{"name"}).AddRow("123"), - ) - mock.ExpectQuery("SELECT(.+)").WithArgs(uint(1), "/dst").WillReturnRows( - sqlmock.NewRows([]string{"name"}).AddRow("123"), - ) - err := fs.Copy(ctx, []string{"test"}, []string{}, "/src", "/dst") - asserts.Error(err) - } - -} - -func TestFileSystem_Move(t *testing.T) { - conf.DatabaseConfig.Type = "mysql" - asserts := assert.New(t) - fs := &FileSystem{User: &model.User{ - Model: gorm.Model{ - ID: 1, - }, - Storage: 3, - Group: model.Group{MaxStorage: 3}, - }} - ctx := context.Background() - - // 目录不存在 - { - mock.ExpectQuery("SELECT(.+)").WillReturnRows( - sqlmock.NewRows([]string{"name"}), - ) - err := fs.Move(ctx, []string{}, []string{}, "/src", "/dst") - asserts.Equal(ErrPathNotExist, err) + WithArgs(3, 1, "3"). + WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"})) + exist, folder := fs.IsPathExist(path) asserts.NoError(mock.ExpectationsWereMet()) + asserts.False(exist) + asserts.Nil(folder) } - // 移动目录出错 - { - mock.ExpectQuery("SELECT(.+)").WithArgs(uint(1), "/src").WillReturnRows( - sqlmock.NewRows([]string{"name"}).AddRow("123"), - ) - mock.ExpectQuery("SELECT(.+)").WithArgs(uint(1), "/dst").WillReturnRows( - sqlmock.NewRows([]string{"name"}).AddRow("123"), - ) - err := fs.Move(ctx, []string{"test"}, []string{}, "/src", "/dst") - asserts.Error(err) - } } diff --git a/pkg/util/path.go b/pkg/util/path.go index eb70909..dcb0587 100644 --- a/pkg/util/path.go +++ b/pkg/util/path.go @@ -13,5 +13,19 @@ func FillSlash(path string) string { return path } return path + "/" +} + +// SplitPath 分割路径为列表 +func SplitPath(path string) []string { + if len(path) == 0 || path[0] != '/' { + return []string{} + } + + if path == "/" { + return []string{"/"} + } + pathSplit := strings.Split(path, "/") + pathSplit[0] = "/" + return pathSplit } diff --git a/routers/file_router_test.go b/routers/file_router_test.go index 86f06a4..fabac28 100644 --- a/routers/file_router_test.go +++ b/routers/file_router_test.go @@ -124,7 +124,7 @@ func TestObjectDelete(t *testing.T) { // 路径不存在,返回无错误 { GetRequest: func() *http.Request { - body := explorer.ItemService{ + body := explorer.ItemServiceTemp{ Items: []string{"/TestObjectDelete.txt"}, } bodyStr, _ := json.Marshal(body) @@ -141,7 +141,7 @@ func TestObjectDelete(t *testing.T) { { Mock: []string{"INSERT INTO `files` (`id`, `created_at`, `updated_at`, `deleted_at`, `name`, `source_name`, `user_id`, `size`, `pic_info`, `folder_id`, `policy_id`, `dir`) VALUES(5, '2019-11-30 07:08:33', '2019-11-30 07:08:33', NULL, 'pigeon.zip', '65azil3B_pigeon.zip', 1, 1667217, '', 1, 1, '/');"}, GetRequest: func() *http.Request { - body := explorer.ItemService{ + body := explorer.ItemServiceTemp{ Items: []string{"/pigeon.zip"}, } bodyStr, _ := json.Marshal(body) diff --git a/service/explorer/objects.go b/service/explorer/objects.go index e4bea8e..9ea0cba 100644 --- a/service/explorer/objects.go +++ b/service/explorer/objects.go @@ -22,8 +22,8 @@ type ItemRenameService struct { // ItemService 处理多文件/目录相关服务 type ItemService struct { - Items []string `json:"items" binding:"exists,dive,ne=/"` - Dirs []string `json:"dirs" binding:"exists,dive,ne=/"` + Items []uint `json:"items" binding:"exists"` + Dirs []uint `json:"dirs" binding:"exists"` } // Delete 删除对象