Modify: use pure tree structure in file system scheme
parent
10a2ef4267
commit
56b1ae9f31
|
|
@ -21,6 +21,9 @@ type File struct {
|
||||||
|
|
||||||
// 关联模型
|
// 关联模型
|
||||||
Policy Policy `gorm:"PRELOAD:false,association_autoupdate:false"`
|
Policy Policy `gorm:"PRELOAD:false,association_autoupdate:false"`
|
||||||
|
|
||||||
|
// 数据库忽略字段
|
||||||
|
PositionTemp string `gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create 创建文件记录
|
// Create 创建文件记录
|
||||||
|
|
@ -32,20 +35,31 @@ func (file *File) Create() (uint, error) {
|
||||||
return file.ID, nil
|
return file.ID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFileByPathAndName 给定路径(s)、文件名、用户ID,查找文件
|
// GetChildFile 查找目录下名为name的子文件
|
||||||
func GetFileByPathAndName(path string, name string, uid uint) (File, error) {
|
func (folder *Folder) GetChildFile(name string) (*File, error) {
|
||||||
var file File
|
var file File
|
||||||
result := DB.Where("user_id = ? AND dir = ? AND name=?", uid, path, name).First(&file)
|
result := DB.Where("folder_id = ? AND name = ?", folder.ID, name).Find(&file)
|
||||||
return file, result.Error
|
|
||||||
|
if result.Error == nil {
|
||||||
|
file.PositionTemp = path.Join(folder.PositionTemp, folder.Name, file.Name)
|
||||||
|
}
|
||||||
|
return &file, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetChildFile 查找目录下子文件
|
// GetChildFiles 查找目录下子文件
|
||||||
func (folder *Folder) GetChildFile() ([]File, error) {
|
func (folder *Folder) GetChildFiles() ([]File, error) {
|
||||||
var files []File
|
var files []File
|
||||||
result := DB.Where("folder_id = ?", folder.ID).Find(&files)
|
result := DB.Where("folder_id = ?", folder.ID).Find(&files)
|
||||||
return files, result.Error
|
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 批量检索目录子文件
|
// GetChildFilesOfFolders 批量检索目录子文件
|
||||||
func GetChildFilesOfFolders(folders *[]Folder) ([]File, error) {
|
func GetChildFilesOfFolders(folders *[]Folder) ([]File, error) {
|
||||||
// 将所有待删除目录ID抽离,以便检索文件
|
// 将所有待删除目录ID抽离,以便检索文件
|
||||||
|
|
@ -68,19 +82,6 @@ func (file *File) GetPolicy() *Policy {
|
||||||
return &file.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 去除给定的文件列表中有软链接的文件
|
// RemoveFilesWithSoftLinks 去除给定的文件列表中有软链接的文件
|
||||||
func RemoveFilesWithSoftLinks(files []File) ([]File, error) {
|
func RemoveFilesWithSoftLinks(files []File) ([]File, error) {
|
||||||
// 结果值
|
// 结果值
|
||||||
|
|
@ -127,14 +128,6 @@ func DeleteFileByIDs(ids []uint) error {
|
||||||
return result.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查找文件
|
// GetFilesByParentIDs 根据父目录ID查找文件
|
||||||
func GetFilesByParentIDs(ids []uint, uid uint) ([]File, error) {
|
func GetFilesByParentIDs(ids []uint, uid uint) ([]File, error) {
|
||||||
files := make([]File, 0, len(ids))
|
files := make([]File, 0, len(ids))
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,6 @@ import (
|
||||||
"testing"
|
"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) {
|
func TestFile_Create(t *testing.T) {
|
||||||
asserts := assert.New(t)
|
asserts := assert.New(t)
|
||||||
file := File{
|
file := File{
|
||||||
|
|
@ -44,6 +33,32 @@ func TestFile_Create(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFolder_GetChildFile(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)
|
asserts := assert.New(t)
|
||||||
folder := &Folder{
|
folder := &Folder{
|
||||||
Model: gorm.Model{
|
Model: gorm.Model{
|
||||||
|
|
@ -53,20 +68,46 @@ func TestFolder_GetChildFile(t *testing.T) {
|
||||||
|
|
||||||
// 找不到
|
// 找不到
|
||||||
mock.ExpectQuery("SELECT(.+)folder_id(.+)").WithArgs(1).WillReturnError(errors.New("error"))
|
mock.ExpectQuery("SELECT(.+)folder_id(.+)").WithArgs(1).WillReturnError(errors.New("error"))
|
||||||
files, err := folder.GetChildFile()
|
files, err := folder.GetChildFiles()
|
||||||
asserts.Error(err)
|
asserts.Error(err)
|
||||||
asserts.Len(files, 0)
|
asserts.Len(files, 0)
|
||||||
asserts.NoError(mock.ExpectationsWereMet())
|
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))
|
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.NoError(err)
|
||||||
asserts.Len(files, 2)
|
asserts.Len(files, 2)
|
||||||
asserts.NoError(mock.ExpectationsWereMet())
|
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) {
|
func TestGetChildFilesOfFolders(t *testing.T) {
|
||||||
asserts := assert.New(t)
|
asserts := assert.New(t)
|
||||||
testFolder := []Folder{
|
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) {
|
func TestRemoveFilesWithSoftLinks(t *testing.T) {
|
||||||
asserts := assert.New(t)
|
asserts := assert.New(t)
|
||||||
files := []File{
|
files := []File{
|
||||||
|
|
|
||||||
369
models/folder.go
369
models/folder.go
|
|
@ -2,11 +2,9 @@ package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/HFO4/cloudreve/pkg/conf"
|
|
||||||
"github.com/HFO4/cloudreve/pkg/util"
|
"github.com/HFO4/cloudreve/pkg/util"
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Folder 目录
|
// Folder 目录
|
||||||
|
|
@ -18,6 +16,9 @@ type Folder struct {
|
||||||
Position string `gorm:"size:65536"`
|
Position string `gorm:"size:65536"`
|
||||||
OwnerID uint `gorm:"index:owner_id"`
|
OwnerID uint `gorm:"index:owner_id"`
|
||||||
PositionAbsolute string `gorm:"size:65536"`
|
PositionAbsolute string `gorm:"size:65536"`
|
||||||
|
|
||||||
|
// 数据库忽略字段
|
||||||
|
PositionTemp string `gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create 创建目录
|
// Create 创建目录
|
||||||
|
|
@ -29,11 +30,18 @@ func (folder *Folder) Create() (uint, error) {
|
||||||
return folder.ID, nil
|
return folder.ID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFolderByPath 根据绝对路径和UID查找目录
|
// GetChild 返回folder下名为name的子目录,不存在则返回错误
|
||||||
func GetFolderByPath(path string, uid uint) (Folder, error) {
|
func (folder *Folder) GetChild(name string) (*Folder, error) {
|
||||||
var folder Folder
|
var resFolder Folder
|
||||||
result := DB.Where("owner_id = ? AND position_absolute = ?", uid, path).First(&folder)
|
err := DB.
|
||||||
return folder, result.Error
|
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 查找子目录
|
// GetChildFolder 查找子目录
|
||||||
|
|
@ -44,59 +52,48 @@ func (folder *Folder) GetChildFolder() ([]Folder, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRecursiveChildFolder 查找所有递归子目录,包括自身
|
// 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))
|
folders := make([]Folder, 0, len(dirs))
|
||||||
var err error
|
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 下使用正则查询
|
// 整理父目录的ID
|
||||||
search := util.BuildRegexp(dirs, "^", "/", "|")
|
var parentIDs = make([]uint, 0, len(parFolders))
|
||||||
result := DB.Where("(owner_id = ? and position_absolute REGEXP ?) or (owner_id = ? and position_absolute in (?))", uid, search, uid, dirs).Find(&folders)
|
for _, folder := range parFolders {
|
||||||
err = result.Error
|
parentIDs = append(parentIDs, folder.ID)
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
if includeSelf {
|
||||||
|
// 合并至最终结果
|
||||||
|
folders = append(folders, parFolders...)
|
||||||
|
}
|
||||||
|
parFolders = []Folder{}
|
||||||
|
|
||||||
// SQLite 下使用递归查询
|
// 递归查询子目录,最大递归65535次
|
||||||
var parFolders []Folder
|
for i := 0; i < 65535; i++ {
|
||||||
result := DB.Where("owner_id = ? and position_absolute in (?)", uid, dirs).Find(&parFolders)
|
|
||||||
if result.Error != nil {
|
result = DB.Where("owner_id = ? and parent_id in (?)", uid, parentIDs).Find(&parFolders)
|
||||||
return folders, err
|
|
||||||
|
// 查询结束条件
|
||||||
|
if len(parFolders) == 0 {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// 整理父目录的ID
|
// 整理父目录的ID
|
||||||
var parentIDs = make([]uint, 0, len(parFolders))
|
parentIDs = make([]uint, 0, len(parFolders))
|
||||||
for _, folder := range parFolders {
|
for _, folder := range parFolders {
|
||||||
parentIDs = append(parentIDs, folder.ID)
|
parentIDs = append(parentIDs, folder.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if includeSelf {
|
// 合并至最终结果
|
||||||
// 合并至最终结果
|
folders = append(folders, parFolders...)
|
||||||
folders = append(folders, parFolders...)
|
|
||||||
}
|
|
||||||
parFolders = []Folder{}
|
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
|
return folders, err
|
||||||
|
|
@ -108,9 +105,16 @@ func DeleteFolderByIDs(ids []uint) error {
|
||||||
return result.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,
|
// 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
|
var copiedSize uint64
|
||||||
|
|
||||||
|
|
@ -118,10 +122,10 @@ func (folder *Folder) MoveOrCopyFileTo(files []string, dstFolder *Folder, isCopy
|
||||||
// 检索出要复制的文件
|
// 检索出要复制的文件
|
||||||
var originFiles = make([]File, 0, len(files))
|
var originFiles = make([]File, 0, len(files))
|
||||||
if err := DB.Where(
|
if err := DB.Where(
|
||||||
"name in (?) and user_id = ? and dir = ?",
|
"id in (?) and user_id = ? and folder_id = ?",
|
||||||
files,
|
files,
|
||||||
folder.OwnerID,
|
folder.OwnerID,
|
||||||
folder.PositionAbsolute,
|
folder.ID,
|
||||||
).Find(&originFiles).Error; err != nil {
|
).Find(&originFiles).Error; err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +134,6 @@ func (folder *Folder) MoveOrCopyFileTo(files []string, dstFolder *Folder, isCopy
|
||||||
for _, oldFile := range originFiles {
|
for _, oldFile := range originFiles {
|
||||||
oldFile.Model = gorm.Model{}
|
oldFile.Model = gorm.Model{}
|
||||||
oldFile.FolderID = dstFolder.ID
|
oldFile.FolderID = dstFolder.ID
|
||||||
oldFile.Dir = dstFolder.PositionAbsolute
|
|
||||||
|
|
||||||
if err := DB.Create(&oldFile).Error; err != nil {
|
if err := DB.Create(&oldFile).Error; err != nil {
|
||||||
return copiedSize, err
|
return copiedSize, err
|
||||||
|
|
@ -142,14 +145,13 @@ func (folder *Folder) MoveOrCopyFileTo(files []string, dstFolder *Folder, isCopy
|
||||||
} else {
|
} else {
|
||||||
// 更改顶级要移动文件的父目录指向
|
// 更改顶级要移动文件的父目录指向
|
||||||
err := DB.Model(File{}).Where(
|
err := DB.Model(File{}).Where(
|
||||||
"name in (?) and user_id = ? and dir = ?",
|
"id in (?) and user_id = ? and folder_id = ?",
|
||||||
files,
|
files,
|
||||||
folder.OwnerID,
|
folder.OwnerID,
|
||||||
folder.PositionAbsolute,
|
folder.ID,
|
||||||
).
|
).
|
||||||
Update(map[string]interface{}{
|
Update(map[string]interface{}{
|
||||||
"folder_id": dstFolder.ID,
|
"folder_id": dstFolder.ID,
|
||||||
"dir": dstFolder.PositionAbsolute,
|
|
||||||
}).
|
}).
|
||||||
Error
|
Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -162,209 +164,88 @@ func (folder *Folder) MoveOrCopyFileTo(files []string, dstFolder *Folder, isCopy
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MoveOrCopyFolderTo 将folder目录下的dirs子目录复制或移动到dstFolder,
|
// CopyFolderTo 将此目录及其子目录及文件递归复制至dstFolder
|
||||||
// 返回此过程中增加的容量
|
// 返回此操作新增的容量
|
||||||
func (folder *Folder) MoveOrCopyFolderTo(dirs []string, dstFolder *Folder, isCopy bool) (uint64, error) {
|
func (folder *Folder) CopyFolderTo(folderID uint, dstFolder *Folder) (size uint64, err error) {
|
||||||
// 生成绝对路径
|
// 列出所有子目录
|
||||||
fullDirs := make([]string, len(dirs))
|
subFolders, err := GetRecursiveChildFolder([]uint{folderID}, folder.OwnerID, true)
|
||||||
for i := 0; i < len(dirs); i++ {
|
|
||||||
fullDirs[i] = path.Join(
|
|
||||||
folder.PositionAbsolute,
|
|
||||||
path.Base(dirs[i]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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("找不到原始目录")
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录新的主键
|
|
||||||
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 {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新被移动的目录递归的子目录和文件
|
// 抽离所有子目录的ID
|
||||||
for parKey, toBeMoved := range subFolders {
|
var subFolderIDs = make([]uint, len(subFolders))
|
||||||
ignorePath := fullDirs[parKey]
|
for key, value := range subFolders {
|
||||||
// TODO 找到更好的修改办法
|
subFolderIDs[key] = value.ID
|
||||||
|
}
|
||||||
// 抽离所有子目录的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:]...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 复制子目录
|
||||||
|
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 {
|
} else {
|
||||||
for _, subFolder := range toBeMoved {
|
util.Log().Warning("无法取得新的父目录:%d", folder.ParentID)
|
||||||
// 每个分组的第一个目录已经变更指向,直接跳过
|
return size, errors.New("无法取得新的父目录")
|
||||||
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),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取子目录下的所有子文件
|
// 插入新的目录记录
|
||||||
toBeMovedFile, err := GetFilesByParentIDs(subFolderIDs, folder.OwnerID)
|
oldID := folder.ID
|
||||||
if err != nil {
|
folder.Model = gorm.Model{}
|
||||||
return 0, err
|
folder.ParentID = newID
|
||||||
}
|
if err = DB.Create(&folder).Error; err != nil {
|
||||||
|
return size, 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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
// 记录新的ID以便其子目录使用
|
||||||
|
newIDCache[oldID] = folder.ID
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return newUsedStorage, 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 _, oldFile := range originFiles {
|
||||||
|
oldFile.Model = gorm.Model{}
|
||||||
|
oldFile.FolderID = newIDCache[oldFile.FolderID]
|
||||||
|
|
||||||
|
if err := DB.Create(&oldFile).Error; err != nil {
|
||||||
|
return size, err
|
||||||
|
}
|
||||||
|
|
||||||
|
size += oldFile.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
return size, 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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -55,6 +55,13 @@ type UserOption struct {
|
||||||
PreferredTheme string `json:"preferred_theme"`
|
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 减少用户已用容量
|
// DeductionStorage 减少用户已用容量
|
||||||
func (user *User) DeductionStorage(size uint64) bool {
|
func (user *User) DeductionStorage(size uint64) bool {
|
||||||
if size == 0 {
|
if size == 0 {
|
||||||
|
|
@ -160,10 +167,8 @@ func (user *User) BeforeSave() (err error) {
|
||||||
func (user *User) AfterCreate(tx *gorm.DB) (err error) {
|
func (user *User) AfterCreate(tx *gorm.DB) (err error) {
|
||||||
// 创建用户的默认根目录
|
// 创建用户的默认根目录
|
||||||
defaultFolder := &Folder{
|
defaultFolder := &Folder{
|
||||||
Name: "根目录",
|
Name: "/",
|
||||||
Position: ".",
|
OwnerID: user.ID,
|
||||||
OwnerID: user.ID,
|
|
||||||
PositionAbsolute: "/",
|
|
||||||
}
|
}
|
||||||
tx.Create(defaultFolder)
|
tx.Create(defaultFolder)
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -306,3 +306,25 @@ func TestUser_AfterCreate(t *testing.T) {
|
||||||
asserts.NoError(err)
|
asserts.NoError(err)
|
||||||
asserts.NoError(mock.ExpectationsWereMet())
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
package filesystem
|
package filesystem
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
model "github.com/HFO4/cloudreve/models"
|
model "github.com/HFO4/cloudreve/models"
|
||||||
"github.com/HFO4/cloudreve/pkg/serializer"
|
|
||||||
"github.com/HFO4/cloudreve/pkg/util"
|
"github.com/HFO4/cloudreve/pkg/util"
|
||||||
"path"
|
"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
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 汇总处理结果
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 返回给定目录是否存在
|
// IsPathExist 返回给定目录是否存在
|
||||||
// 如果存在就返回目录
|
// 如果存在就返回目录
|
||||||
func (fs *FileSystem) IsPathExist(path string) (bool, *model.Folder) {
|
func (fs *FileSystem) IsPathExist(path string) (bool, *model.Folder) {
|
||||||
folder, err := model.GetFolderByPath(path, fs.User.ID)
|
pathList := util.SplitPath(path)
|
||||||
return err == nil, &folder
|
if len(pathList) == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归步入目录
|
||||||
|
var currentFolder *model.Folder
|
||||||
|
for _, folderName := range pathList {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// 根目录
|
||||||
|
if folderName == "/" {
|
||||||
|
currentFolder, err = fs.User.Root()
|
||||||
|
if err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentFolder, err = currentFolder.GetChild(folderName)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, currentFolder
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsFileExist 返回给定路径的文件是否存在
|
// IsFileExist 返回给定路径的文件是否存在
|
||||||
|
|
@ -351,7 +46,19 @@ func (fs *FileSystem) IsFileExist(fullPath string) (bool, *model.File) {
|
||||||
basePath := path.Dir(fullPath)
|
basePath := path.Dir(fullPath)
|
||||||
fileName := path.Base(fullPath)
|
fileName := path.Base(fullPath)
|
||||||
|
|
||||||
file, err := model.GetFileByPathAndName(basePath, fileName, fs.User.ID)
|
// 获得父目录
|
||||||
|
exist, parent := fs.IsPathExist(basePath)
|
||||||
|
if !exist {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
return err == nil, &file
|
file, err := parent.GetChildFile(fileName)
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,10 @@
|
||||||
package filesystem
|
package filesystem
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"github.com/DATA-DOG/go-sqlmock"
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
model "github.com/HFO4/cloudreve/models"
|
model "github.com/HFO4/cloudreve/models"
|
||||||
"github.com/HFO4/cloudreve/pkg/conf"
|
|
||||||
"github.com/HFO4/cloudreve/pkg/serializer"
|
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -22,20 +17,43 @@ func TestFileSystem_IsFileExist(t *testing.T) {
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// 存在
|
// 存在
|
||||||
mock.ExpectQuery("SELECT(.+)").WithArgs(uint(1), "/s", "1.txt").WillReturnRows(
|
{
|
||||||
sqlmock.NewRows([]string{"Name"}).AddRow("s"),
|
path := "/1.txt"
|
||||||
)
|
// 根目录
|
||||||
testResult, _ := fs.IsFileExist("/s/1.txt")
|
mock.ExpectQuery("SELECT(.+)").
|
||||||
asserts.True(testResult)
|
WithArgs(1).
|
||||||
asserts.NoError(mock.ExpectationsWereMet())
|
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(.+)").WithArgs(uint(1), "/ss/dfsd", "1.txt").WillReturnRows(
|
{
|
||||||
sqlmock.NewRows([]string{"Name"}),
|
path := "/1.txt"
|
||||||
)
|
// 根目录
|
||||||
testResult, _ = fs.IsFileExist("/ss/dfsd/1.txt")
|
mock.ExpectQuery("SELECT(.+)").
|
||||||
asserts.False(testResult)
|
WithArgs(1).
|
||||||
asserts.NoError(mock.ExpectationsWereMet())
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 父目录不存在
|
||||||
|
{
|
||||||
|
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_IsPathExist(t *testing.T) {
|
func TestFileSystem_IsPathExist(t *testing.T) {
|
||||||
|
|
@ -46,398 +64,67 @@ func TestFileSystem_IsPathExist(t *testing.T) {
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// 存在
|
// 查询根目录
|
||||||
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/")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 成功,子目录下为空,使用路径处理钩子
|
|
||||||
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"))
|
path := "/"
|
||||||
err := fs.ListDeleteFiles(context.Background(), []string{"/"})
|
// 根目录
|
||||||
asserts.NoError(err)
|
mock.ExpectQuery("SELECT(.+)").
|
||||||
|
WithArgs(1).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
|
||||||
|
exist, folder := fs.IsPathExist(path)
|
||||||
asserts.NoError(mock.ExpectationsWereMet())
|
asserts.NoError(mock.ExpectationsWereMet())
|
||||||
|
asserts.True(exist)
|
||||||
|
asserts.Equal(uint(1), folder.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 失败
|
// 深层路径
|
||||||
{
|
{
|
||||||
mock.ExpectQuery("SELECT(.+)").WillReturnError(errors.New("error"))
|
path := "/1/2/3"
|
||||||
err := fs.ListDeleteFiles(context.Background(), []string{"/"})
|
// 根目录
|
||||||
asserts.Error(err)
|
mock.ExpectQuery("SELECT(.+)").
|
||||||
asserts.Equal(serializer.CodeDBError, err.(serializer.AppError).Code)
|
WithArgs(1).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}).AddRow(1, 1))
|
||||||
|
// 1
|
||||||
|
mock.ExpectQuery("SELECT(.+)").
|
||||||
|
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(.+)").
|
||||||
|
WithArgs(3, 1, "3").
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}).AddRow(4, 1))
|
||||||
|
exist, folder := fs.IsPathExist(path)
|
||||||
asserts.NoError(mock.ExpectationsWereMet())
|
asserts.NoError(mock.ExpectationsWereMet())
|
||||||
|
asserts.True(exist)
|
||||||
|
asserts.Equal(uint(4), folder.ID)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileSystem_ListDeleteDirs(t *testing.T) {
|
// 深层 不存在
|
||||||
conf.DatabaseConfig.Type = "mysql"
|
|
||||||
asserts := assert.New(t)
|
|
||||||
fs := &FileSystem{User: &model.User{
|
|
||||||
Model: gorm.Model{
|
|
||||||
ID: 1,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
|
|
||||||
// 成功
|
|
||||||
{
|
{
|
||||||
|
path := "/1/2/3"
|
||||||
|
// 根目录
|
||||||
mock.ExpectQuery("SELECT(.+)").
|
mock.ExpectQuery("SELECT(.+)").
|
||||||
WillReturnRows(
|
WithArgs(1).
|
||||||
sqlmock.NewRows([]string{"id"}).
|
WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}).AddRow(1, 1))
|
||||||
AddRow(1).
|
// 1
|
||||||
AddRow(2).
|
|
||||||
AddRow(3),
|
|
||||||
)
|
|
||||||
mock.ExpectQuery("SELECT(.+)").
|
mock.ExpectQuery("SELECT(.+)").
|
||||||
WithArgs(1, 2, 3).
|
WithArgs(1, 1, "1").
|
||||||
WillReturnRows(
|
WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}).AddRow(2, 1))
|
||||||
sqlmock.NewRows([]string{"id", "name"}).
|
// 2
|
||||||
AddRow(4, "1.txt").
|
mock.ExpectQuery("SELECT(.+)").
|
||||||
AddRow(5, "2.txt").
|
WithArgs(2, 1, "2").
|
||||||
AddRow(6, "3.txt"),
|
WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}).AddRow(3, 1))
|
||||||
)
|
// 3
|
||||||
err := fs.ListDeleteDirs(context.Background(), []string{"/"})
|
mock.ExpectQuery("SELECT(.+)").
|
||||||
asserts.NoError(err)
|
WithArgs(3, 1, "3").
|
||||||
asserts.Len(fs.FileTarget, 3)
|
WillReturnRows(sqlmock.NewRows([]string{"id", "owner_id"}))
|
||||||
asserts.Len(fs.DirTarget, 3)
|
exist, folder := fs.IsPathExist(path)
|
||||||
asserts.NoError(mock.ExpectationsWereMet())
|
asserts.NoError(mock.ExpectationsWereMet())
|
||||||
}
|
asserts.False(exist)
|
||||||
|
asserts.Nil(folder)
|
||||||
// 检索文件发生错误
|
|
||||||
{
|
|
||||||
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(), []string{"/"})
|
|
||||||
asserts.Error(err)
|
|
||||||
asserts.Len(fs.DirTarget, 6)
|
|
||||||
asserts.NoError(mock.ExpectationsWereMet())
|
|
||||||
}
|
|
||||||
// 检索目录发生错误
|
|
||||||
{
|
|
||||||
mock.ExpectQuery("SELECT(.+)").
|
|
||||||
WillReturnError(errors.New("error"))
|
|
||||||
err := fs.ListDeleteDirs(context.Background(), []string{"/"})
|
|
||||||
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, []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)
|
|
||||||
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, []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)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,19 @@ func FillSlash(path string) string {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ func TestObjectDelete(t *testing.T) {
|
||||||
// 路径不存在,返回无错误
|
// 路径不存在,返回无错误
|
||||||
{
|
{
|
||||||
GetRequest: func() *http.Request {
|
GetRequest: func() *http.Request {
|
||||||
body := explorer.ItemService{
|
body := explorer.ItemServiceTemp{
|
||||||
Items: []string{"/TestObjectDelete.txt"},
|
Items: []string{"/TestObjectDelete.txt"},
|
||||||
}
|
}
|
||||||
bodyStr, _ := json.Marshal(body)
|
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, '/');"},
|
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 {
|
GetRequest: func() *http.Request {
|
||||||
body := explorer.ItemService{
|
body := explorer.ItemServiceTemp{
|
||||||
Items: []string{"/pigeon.zip"},
|
Items: []string{"/pigeon.zip"},
|
||||||
}
|
}
|
||||||
bodyStr, _ := json.Marshal(body)
|
bodyStr, _ := json.Marshal(body)
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,8 @@ type ItemRenameService struct {
|
||||||
|
|
||||||
// ItemService 处理多文件/目录相关服务
|
// ItemService 处理多文件/目录相关服务
|
||||||
type ItemService struct {
|
type ItemService struct {
|
||||||
Items []string `json:"items" binding:"exists,dive,ne=/"`
|
Items []uint `json:"items" binding:"exists"`
|
||||||
Dirs []string `json:"dirs" binding:"exists,dive,ne=/"`
|
Dirs []uint `json:"dirs" binding:"exists"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete 删除对象
|
// Delete 删除对象
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue