Feat: delete objects

pull/247/head
HFO4 5 years ago
parent 0cbbe5bb79
commit 93010e3525

@ -12,7 +12,7 @@ TablePrefix = v3_
[Redis]
Server = 127.0.0.1:6379
Password = 52121225
Password =
DB = 0
[Captcha]

@ -28,6 +28,7 @@ func Session(secret string) gin.HandlerFunc {
}
// Also set Secure: true if using SSL, you should though
// TODO:same-site policy
Store.Options(sessions.Options{HttpOnly: true, MaxAge: 7 * 86400, Path: "/"})
return sessions.Sessions("cloudreve-session", Store)
}

@ -3,6 +3,7 @@ package model
import (
"github.com/HFO4/cloudreve/pkg/util"
"github.com/jinzhu/gorm"
"path"
)
// File 文件
@ -31,7 +32,7 @@ func (file *File) Create() (uint, error) {
return file.ID, nil
}
// GetFileByPathAndName 给定路径、文件名、用户ID查找文件
// GetFileByPathAndName 给定路径(s)、文件名、用户ID查找文件
func GetFileByPathAndName(path string, name string, uid uint) (File, error) {
var file File
result := DB.Where("user_id = ? AND dir = ? AND name=?", uid, path, name).First(&file)
@ -45,6 +46,20 @@ func (folder *Folder) GetChildFile() ([]File, error) {
return files, result.Error
}
// GetChildFilesOfFolders 批量检索目录子文件
func GetChildFilesOfFolders(folders *[]Folder) ([]File, error) {
// 将所有待删除目录ID抽离以便检索文件
folderIDs := make([]uint, 0, len(*folders))
for _, value := range *folders {
folderIDs = append(folderIDs, value.ID)
}
// 检索文件
var files []File
result := DB.Where("folder_id in (?)", folderIDs).Find(&files)
return files, result.Error
}
// GetPolicy 获取文件所属策略
// TODO:test
func (file *File) GetPolicy() *Policy {
@ -53,3 +68,52 @@ func (file *File) GetPolicy() *Policy {
}
return &file.Policy
}
// GetFileByPaths 根据给定的文件路径(s)查找文件
func GetFileByPaths(paths []string, uid uint) ([]File, error) {
var files []File
tx := DB
for _, value := range paths {
base := path.Base(value)
dir := path.Dir(value)
tx = tx.Or("dir = ? and name = ? and user_id = ?", dir, base, uid)
}
result := tx.Find(&files)
return files, result.Error
}
// RemoveFilesWithSoftLinks 去除给定的文件列表中有软链接的文件
func RemoveFilesWithSoftLinks(files []File) ([]File, error) {
// 结果值
filteredFiles := make([]File, 0)
// 查询软链接的文件
var filesWithSoftLinks []File
tx := DB
for _, value := range files {
tx = tx.Or("source_name = ? and policy_id = ? and id != ?", value.SourceName, value.GetPolicy().ID, value.ID)
}
result := tx.Find(&filesWithSoftLinks)
if result.Error != nil {
return nil, result.Error
}
// 过滤具有软连接的文件
for i := 0; i < len(files); i++ {
for _, value := range filesWithSoftLinks {
if value.PolicyID != files[i].PolicyID || value.SourceName != files[i].SourceName {
filteredFiles = append(filteredFiles, files[i])
break
}
}
}
return filteredFiles, nil
}
// DeleteFileByIDs 根据给定ID批量删除文件记录
func DeleteFileByIDs(ids []uint) error {
result := DB.Where("id in (?)", ids).Delete(&File{})
return result.Error
}

@ -38,3 +38,17 @@ func (folder *Folder) GetChildFolder() ([]Folder, error) {
result := DB.Where("parent_id = ?", folder.ID).Find(&folders)
return folders, result.Error
}
// GetRecursiveChildFolder 查找所有递归子目录
func GetRecursiveChildFolder(dirs []string, uid uint) ([]Folder, error) {
folders := make([]Folder, 0, len(dirs))
search := util.BuildRegexp(dirs, "^", "/", "|")
result := DB.Where("(owner_id = ? and position_absolute REGEXP ?) or position_absolute in (?)", uid, search, dirs).Find(&folders)
return folders, result.Error
}
// DeleteFolderByIDs 根据给定ID批量删除目录记录
func DeleteFolderByIDs(ids []uint) error {
result := DB.Where("id in (?)", ids).Delete(&Folder{})
return result.Error
}

@ -54,17 +54,17 @@ func migration() {
}
func addDefaultPolicy() {
_, err := GetPolicyByID(1)
_, err := GetPolicyByID(uint(1))
// 未找到初始存储策略时,则创建
if gorm.IsRecordNotFoundError(err) {
defaultPolicy := Policy{
Name: "默认上传策略",
Name: "默认存储策略",
Type: "local",
Server: "/Api/V3/File/Upload",
Server: "/api/v3/file/upload",
BaseURL: "http://cloudreve.org/public/uploads/",
MaxSize: 10 * 1024 * 1024 * 1024,
AutoRename: true,
DirNameRule: "{date}/{uid}",
DirNameRule: "uploads/{uid}/{path}",
FileNameRule: "{uid}_{randomkey8}_{originname}",
IsOriginLinkEnable: false,
}

@ -6,6 +6,7 @@ import (
"github.com/jinzhu/gorm"
"path/filepath"
"strconv"
"sync"
"time"
)
@ -41,16 +42,36 @@ type PolicyOption struct {
RangeTransferEnabled bool `json:"range_transfer_enabled"`
}
// 存储策略缓存,部分情况下需要频繁查询存储策略
var policyCache = make(map[uint]Policy)
var rw sync.RWMutex
// GetPolicyByID 用ID获取存储策略
func GetPolicyByID(ID interface{}) (Policy, error) {
// 尝试读取缓存
rw.RLock()
if policy, ok := policyCache[ID.(uint)]; ok {
rw.RUnlock()
return policy, nil
}
rw.RUnlock()
var policy Policy
result := DB.First(&policy, ID)
// 写入缓存
if result.Error == nil {
rw.Lock()
policyCache[policy.ID] = policy
rw.Unlock()
}
return policy, result.Error
}
// AfterFind 找到上传策略后的钩子
// AfterFind 找到存储策略后的钩子
func (policy *Policy) AfterFind() (err error) {
// 解析上传策略设置到OptionsSerialized
// 解析存储策略设置到OptionsSerialized
err = json.Unmarshal([]byte(policy.Options), &policy.OptionsSerialized)
if policy.OptionsSerialized.FileType == nil {
policy.OptionsSerialized.FileType = []string{}

@ -13,16 +13,16 @@ func TestGetPolicyByID(t *testing.T) {
asserts := assert.New(t)
rows := sqlmock.NewRows([]string{"name", "type", "options"}).
AddRow("默认上传策略", "local", "{\"op_name\":\"123\"}")
AddRow("默认存储策略", "local", "{\"op_name\":\"123\"}")
mock.ExpectQuery("^SELECT \\* FROM `(.+)` WHERE `(.+)`\\.`deleted_at` IS NULL AND \\(\\(`policies`.`id` = 1\\)\\)(.+)$").WillReturnRows(rows)
policy, err := GetPolicyByID(1)
policy, err := GetPolicyByID(uint(1))
asserts.NoError(err)
asserts.Equal("默认上传策略", policy.Name)
asserts.Equal("默认存储策略", policy.Name)
asserts.Equal("123", policy.OptionsSerialized.OPName)
rows = sqlmock.NewRows([]string{"name", "type", "options"})
mock.ExpectQuery("^SELECT \\* FROM `(.+)` WHERE `(.+)`\\.`deleted_at` IS NULL AND \\(\\(`policies`.`id` = 1\\)\\)(.+)$").WillReturnRows(rows)
policy, err = GetPolicyByID(1)
policy, err = GetPolicyByID(uint(1))
asserts.Error(err)
}

@ -83,7 +83,7 @@ func (user *User) GetRemainingCapacity() uint64 {
return user.Group.MaxStorage - user.Storage
}
// GetPolicyID 获取用户当前的上传策略ID
// GetPolicyID 获取用户当前的存储策略ID
func (user *User) GetPolicyID() uint {
// 用户未指定时,返回可用的第一个
if user.OptionsSerialized.PreferredPolicy == 0 {

@ -22,7 +22,7 @@ func TestGetUserByID(t *testing.T) {
mock.ExpectQuery("^SELECT (.+)").WillReturnRows(groupRows)
policyRows := sqlmock.NewRows([]string{"id", "name"}).
AddRow(1, "默认上传策略")
AddRow(1, "默认存储策略")
mock.ExpectQuery("^SELECT (.+)").WillReturnRows(policyRows)
user, err := GetUserByID(1)
@ -50,7 +50,7 @@ func TestGetUserByID(t *testing.T) {
OptionsSerialized: PolicyOption{
FileType: []string{},
},
Name: "默认上传策略",
Name: "默认存储策略",
},
}, user)
@ -105,7 +105,7 @@ func TestUser_AfterFind(t *testing.T) {
asserts := assert.New(t)
policyRows := sqlmock.NewRows([]string{"id", "name"}).
AddRow(1, "默认上传策略")
AddRow(1, "默认存储策略")
mock.ExpectQuery("^SELECT (.+)").WillReturnRows(policyRows)
newUser := NewUser()
@ -117,7 +117,7 @@ func TestUser_AfterFind(t *testing.T) {
asserts.NoError(err)
asserts.NoError(mock.ExpectationsWereMet())
asserts.Equal(expected, newUser.OptionsSerialized)
asserts.Equal("默认上传策略", newUser.Policy.Name)
asserts.Equal("默认存储策略", newUser.Policy.Name)
}
func TestUser_BeforeSave(t *testing.T) {
@ -199,7 +199,7 @@ func TestUser_DeductionCapacity(t *testing.T) {
mock.ExpectQuery("^SELECT (.+)").WillReturnRows(groupRows)
policyRows := sqlmock.NewRows([]string{"id", "name"}).
AddRow(1, "默认上传策略")
AddRow(1, "默认存储策略")
mock.ExpectQuery("^SELECT (.+)").WillReturnRows(policyRows)
newUser, err := GetUserByID(1)

@ -17,4 +17,5 @@ var (
ErrPathNotExist = serializer.NewError(404, "路径不存在", nil)
ErrObjectNotExist = serializer.NewError(404, "文件不存在", nil)
ErrIO = serializer.NewError(serializer.CodeIOFailed, "无法读取文件数据", nil)
ErrDBListObjects = serializer.NewError(serializer.CodeDBError, "无法列取对象记录", nil)
)

@ -85,7 +85,7 @@ func (fs *FileSystem) GetContent(ctx context.Context, path string) (io.ReadSeeke
if !exist {
return nil, ErrObjectNotExist
}
fs.Target = &file
fs.FileTarget = []model.File{file}
// 将当前存储策略重设为文件使用的
fs.Policy = file.GetPolicy()
@ -102,3 +102,51 @@ func (fs *FileSystem) GetContent(ctx context.Context, path string) (io.ReadSeeke
return rs, nil
}
// deleteGroupedFile 对分组好的文件执行删除操作,
// 返回每个分组失败的文件列表
func (fs *FileSystem) deleteGroupedFile(ctx context.Context, files map[uint][]*model.File) map[uint][]string {
// 失败的文件列表
failed := make(map[uint][]string, len(files))
for policyID, toBeDeletedFiles := range files {
// 列举出需要物理删除的文件的物理路径
sourceNames := make([]string, 0, len(toBeDeletedFiles))
for i := 0; i < len(toBeDeletedFiles); i++ {
sourceNames = append(sourceNames, toBeDeletedFiles[i].SourceName)
}
// 切换上传策略
fs.Policy = toBeDeletedFiles[0].GetPolicy()
err := fs.dispatchHandler()
if err != nil {
failed[policyID] = sourceNames
continue
}
// 执行删除
failedFile, _ := fs.Handler.Delete(ctx, sourceNames)
failed[policyID] = failedFile
}
return failed
}
// GroupFileByPolicy 将目标文件按照存储策略分组
func (fs *FileSystem) GroupFileByPolicy(ctx context.Context, files []model.File) map[uint][]*model.File {
var policyGroup = make(map[uint][]*model.File)
for key := range files {
if file, ok := policyGroup[files[key].GetPolicy().ID]; ok {
// 如果已存在分组,直接追加
policyGroup[files[key].GetPolicy().ID] = append(file, &files[key])
} else {
// 分布不存在,创建
policyGroup[files[key].GetPolicy().ID] = make([]*model.File, 0)
policyGroup[files[key].GetPolicy().ID] = append(policyGroup[files[key].GetPolicy().ID], &files[key])
}
}
return policyGroup
}

@ -74,7 +74,7 @@ func TestFileSystem_GetContent(t *testing.T) {
asserts.Equal(ErrObjectNotExist, err)
asserts.Nil(rs)
// 未知上传策略
// 未知存储策略
file, err := os.Create("TestFileSystem_GetContent.txt")
asserts.NoError(err)
_ = file.Close()

@ -33,10 +33,12 @@ type Handler interface {
type FileSystem struct {
// 文件系统所有者
User *model.User
// 操作文件使用的上传策略
// 操作文件使用的存储策略
Policy *model.Policy
// 当前正在处理的文件对象
Target *model.File
FileTarget []model.File
// 当前正在处理的目录对象
DirTarget []model.Folder
/*
@ -100,3 +102,23 @@ func NewFileSystemFromContext(c *gin.Context) (*FileSystem, error) {
fs, err := NewFileSystem(user.(*model.User))
return fs, err
}
// SetTargetFile 设置当前处理的目标文件
func (fs *FileSystem) SetTargetFile(files *[]model.File) {
if len(fs.FileTarget) == 0 {
fs.FileTarget = *files
} else {
fs.FileTarget = append(fs.FileTarget, *files...)
}
}
// SetTargetDir 设置当前处理的目标目录
func (fs *FileSystem) SetTargetDir(dirs *[]model.Folder) {
if len(fs.DirTarget) == 0 {
fs.DirTarget = *dirs
} else {
fs.DirTarget = append(fs.DirTarget, *dirs...)
}
}

@ -63,20 +63,19 @@ func (handler Handler) Put(ctx context.Context, file io.ReadCloser, dst string,
}
// Delete 删除一个或多个文件,
// 返回删除的文件,及遇到的最后一个错误
// 返回删除的文件,及遇到的最后一个错误
func (handler Handler) Delete(ctx context.Context, files []string) ([]string, error) {
deleted := make([]string, 0, len(files))
deleteFailed := make([]string, 0, len(files))
var retErr error
for _, value := range files {
err := os.Remove(value)
if err == nil {
deleted = append(deleted, value)
} else {
if err != nil {
util.Log().Warning("无法删除文件,%s", err)
retErr = err
deleteFailed = append(deleteFailed, value)
}
}
return deleted, retErr
return deleteFailed, retErr
}

@ -2,7 +2,10 @@ package filesystem
import (
"context"
"fmt"
model "github.com/HFO4/cloudreve/models"
"github.com/HFO4/cloudreve/pkg/serializer"
"github.com/HFO4/cloudreve/pkg/util"
"path"
)
@ -22,6 +25,113 @@ type Object struct {
Date string `json:"date"`
}
// Delete 递归删除对象
func (fs *FileSystem) Delete(ctx context.Context, dirs, files []string) error {
// 已删除的总容量,map用于去重
var deletedStorage = make(map[uint]uint64)
// 已删除的文件ID
var deletedFileIDs = make([]uint, 0, len(fs.FileTarget))
// 删除失败的文件的父目录ID
// 所有文件的ID
var allFileIDs = make([]uint, 0, len(fs.FileTarget))
// 列出要删除的目录
if len(dirs) > 0 {
err := fs.ListDeleteDirs(ctx, dirs)
if err != nil {
return err
}
}
// 列出要删除的文件
if len(files) > 0 {
err := fs.ListDeleteFiles(ctx, files)
if err != nil {
return err
}
// 去除待删除文件中包含软连接的部分
filesToBeDelete, err := model.RemoveFilesWithSoftLinks(fs.FileTarget)
if err != nil {
return ErrDBListObjects.WithError(err)
}
// 根据存储策略将文件分组
policyGroup := fs.GroupFileByPolicy(ctx, filesToBeDelete)
// 按照存储策略分组删除对象
failed := fs.deleteGroupedFile(ctx, policyGroup)
for i := 0; i < len(fs.FileTarget); i++ {
if util.ContainsString(failed[fs.FileTarget[i].PolicyID], fs.FileTarget[i].SourceName) {
// TODO 删除失败时不删除文件记录及父目录
} else {
deletedFileIDs = append(deletedFileIDs, fs.FileTarget[i].ID)
deletedStorage[fs.FileTarget[i].ID] = fs.FileTarget[i].Size
}
allFileIDs = append(allFileIDs, fs.FileTarget[i].ID)
}
}
// 删除文件记录
err := model.DeleteFileByIDs(allFileIDs)
if err != nil {
return ErrDBListObjects.WithError(err)
}
// 归还容量
var total uint64
for _, value := range deletedStorage {
total += value
}
fs.User.IncreaseStorage(total)
// 删除目录
var allFolderIDs = make([]uint, 0, len(fs.DirTarget))
for _, value := range fs.DirTarget {
allFolderIDs = append(allFolderIDs, value.ID)
}
err = model.DeleteFolderByIDs(allFolderIDs)
if err != nil {
return ErrDBListObjects.WithError(err)
}
if notDeleted := len(fs.FileTarget) - len(deletedFileIDs); notDeleted > 0 {
return serializer.NewError(serializer.CodeNotFullySuccess, fmt.Sprintf("有 %d 个文件未能成功删除,已删除它们的文件记录", notDeleted), nil)
}
return nil
}
// ListDeleteDirs 递归列出要删除目录,及目录下所有文件
func (fs *FileSystem) ListDeleteDirs(ctx context.Context, dirs []string) error {
// 列出所有递归子目录
folders, err := model.GetRecursiveChildFolder(dirs, fs.User.ID)
if err != nil {
return ErrDBListObjects.WithError(err)
}
fs.SetTargetDir(&folders)
// 检索目录下的子文件
files, err := model.GetChildFilesOfFolders(&folders)
if err != nil {
return ErrDBListObjects.WithError(err)
}
fs.SetTargetFile(&files)
return nil
}
// ListDeleteFiles 根据给定的路径列出要删除的文件
func (fs *FileSystem) ListDeleteFiles(ctx context.Context, paths []string) error {
files, err := model.GetFileByPaths(paths, fs.User.ID)
if err != nil {
return ErrDBListObjects.WithError(err)
}
fs.SetTargetFile(&files)
return nil
}
// List 列出路径下的内容,
// pathProcessor为最终对象路径的处理钩子。
// 有些情况下(如在分享页面列对象)时,

@ -13,7 +13,7 @@ import (
*/
// 文件/路径名保留字符
var reservedCharacter = []string{"\\", "?", "*", "<", "\"", ":", ">", "/"}
var reservedCharacter = []string{"\\", "?", "*", "<", "\"", ":", ">", "/", "|"}
// ValidateLegalName 验证文件名/文件夹名是否合法
func (fs *FileSystem) ValidateLegalName(ctx context.Context, name string) bool {

@ -42,6 +42,8 @@ func (err AppError) Error() string {
// 五开头的五位数错误编码为服务器端错误,比如数据库操作失败
// 四开头的五位数错误编码为客户端错误,有时候是客户端代码写错了,有时候是用户操作错误
const (
// CodeNotFullySuccess 未完全成功
CodeNotFullySuccess = 203
// CodeCheckLogin 未登录
CodeCheckLogin = 401
// CodeNoRightErr 未授权访问

@ -2,6 +2,7 @@ package util
import (
"math/rand"
"regexp"
"strings"
)
@ -43,3 +44,15 @@ func Replace(table map[string]string, s string) string {
}
return s
}
// BuildRegexp 构建用于SQL查询用的多条件正则
func BuildRegexp(search []string, prefix, suffix, condition string) string {
var res string
for key, value := range search {
res += prefix + regexp.QuoteMeta(value) + suffix
if key < len(search)-1 {
res += condition
}
}
return res
}

@ -19,7 +19,7 @@ func CreateDirectory(c *gin.Context) {
// ListDirectory 列出目录下内容
func ListDirectory(c *gin.Context) {
var service explorer.DirectoryService
if err := c.ShouldBindQuery(&service); err == nil {
if err := c.ShouldBindUri(&service); err == nil {
res := service.ListDirectory(c)
c.JSON(200, res)
} else {

@ -38,7 +38,7 @@ func FileUploadStream(c *gin.Context) {
// 非本地策略时拒绝上传
if user, ok := c.Get("user"); ok && user.(*model.User).Policy.Type != "local" {
c.JSON(200, serializer.Err(serializer.CodePolicyNotAllowed, "当前上传策略无法使用", nil))
c.JSON(200, serializer.Err(serializer.CodePolicyNotAllowed, "当前存储策略无法使用", nil))
return
}

@ -0,0 +1,22 @@
package controllers
import (
"context"
"github.com/HFO4/cloudreve/service/explorer"
"github.com/gin-gonic/gin"
)
// Delete 删除文件或目录
func Delete(c *gin.Context) {
// 创建上下文
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var service explorer.ItemService
if err := c.ShouldBindJSON(&service); err == nil {
res := service.Delete(ctx, c)
c.JSON(200, res)
} else {
c.JSON(200, ErrorResponse(err))
}
}

@ -72,7 +72,13 @@ func InitRouter() *gin.Engine {
// 创建目录
directory.PUT("", controllers.CreateDirectory)
// 列出目录下内容
directory.GET("", controllers.ListDirectory)
directory.GET("*path", controllers.ListDirectory)
}
// 对象,文件和目录的抽象
object := auth.Group("object")
{
object.DELETE("", controllers.Delete)
}
}

@ -77,7 +77,7 @@ func TestUserSession(t *testing.T) {
AddRow("login_captcha", "0", "login"),
userRows: sqlmock.NewRows([]string{"email", "nick", "password", "options"}).
AddRow("admin@cloudreve.org", "admin", "CKLmDKa1C9SD64vU:76adadd4fd4bad86959155f6f7bc8993c94e7adf", "{}"), policyRows: sqlmock.NewRows([]string{"name", "type", "options"}).
AddRow("默认上传策略", "local", "{\"op_name\":\"123\"}"),
AddRow("默认存储策略", "local", "{\"op_name\":\"123\"}"),
reqBody: `{"userName":"admin@cloudreve.org","captchaCode":"captchaCode","Password":"admin"}`,
expected: serializer.BuildUserResponse(model.User{
Email: "admin@cloudreve.org",
@ -95,7 +95,7 @@ func TestUserSession(t *testing.T) {
userRows: sqlmock.NewRows([]string{"email", "nick", "password", "options"}).
AddRow("admin@cloudreve.org", "admin", "CKLmDKa1C9SD64vU:76adadd4fd4bad86959155f6f7bc8993c94e7adf", "{}"),
policyRows: sqlmock.NewRows([]string{"name", "type", "options"}).
AddRow("默认上传策略", "local", "{\"op_name\":\"123\"}"),
AddRow("默认存储策略", "local", "{\"op_name\":\"123\"}"),
reqBody: `{"userName":"admin@cloudreve.org","captchaCode":"captchaCode","Password":"admin"}`,
expected: serializer.ParamErr("验证码错误", nil),
},
@ -106,7 +106,7 @@ func TestUserSession(t *testing.T) {
userRows: sqlmock.NewRows([]string{"email", "nick", "password", "options"}).
AddRow("admin@cloudreve.org", "admin", "CKLmDKa1C9SD64vU:76adadd4fd4bad86959155f6f7bc8993c94e7adf", "{}"),
policyRows: sqlmock.NewRows([]string{"name", "type", "options"}).
AddRow("默认上传策略", "local", "{\"op_name\":\"123\"}"),
AddRow("默认存储策略", "local", "{\"op_name\":\"123\"}"),
reqBody: `{"userName":"admin@cloudreve.org","captchaCode":"captchaCode","Password":"admin123"}`,
expected: serializer.Err(401, "用户邮箱或密码错误", nil),
},
@ -122,7 +122,7 @@ func TestUserSession(t *testing.T) {
userRows: sqlmock.NewRows([]string{"email", "nick", "password", "options", "status"}).
AddRow("admin@cloudreve.org", "admin", "CKLmDKa1C9SD64vU:76adadd4fd4bad86959155f6f7bc8993c94e7adf", "{}", model.Baned),
policyRows: sqlmock.NewRows([]string{"name", "type", "options"}).
AddRow("默认上传策略", "local", "{\"op_name\":\"123\"}"),
AddRow("默认存储策略", "local", "{\"op_name\":\"123\"}"),
reqBody: `{"userName":"admin@cloudreve.org","captchaCode":"captchaCode","Password":"admin"}`,
expected: serializer.Err(403, "该账号已被封禁", nil),
},
@ -133,7 +133,7 @@ func TestUserSession(t *testing.T) {
userRows: sqlmock.NewRows([]string{"email", "nick", "password", "options", "status"}).
AddRow("admin@cloudreve.org", "admin", "CKLmDKa1C9SD64vU:76adadd4fd4bad86959155f6f7bc8993c94e7adf", "{}", model.NotActivicated),
policyRows: sqlmock.NewRows([]string{"name", "type", "options"}).
AddRow("默认上传策略", "local", "{\"op_name\":\"123\"}"),
AddRow("默认存储策略", "local", "{\"op_name\":\"123\"}"),
reqBody: `{"userName":"admin@cloudreve.org","captchaCode":"captchaCode","Password":"admin"}`,
expected: serializer.Err(403, "该账号未激活", nil),
},
@ -273,7 +273,7 @@ func TestListDirectoryRoute(t *testing.T) {
// 成功
req, _ := http.NewRequest(
"GET",
"/api/v3/directory?path=/",
"/api/v3/directory/",
nil,
)
middleware.SessionMock = map[string]interface{}{"user_id": 1}
@ -286,20 +286,6 @@ func TestListDirectoryRoute(t *testing.T) {
w.Body.Reset()
// 缺少参数
req, _ = http.NewRequest(
"GET",
"/api/v3/directory",
nil,
)
middleware.SessionMock = map[string]interface{}{"user_id": 1}
router.ServeHTTP(w, req)
asserts.Equal(200, w.Code)
resJSON = &serializer.Response{}
err = json.Unmarshal(w.Body.Bytes(), resJSON)
asserts.NoError(err)
asserts.NotEqual(0, resJSON.Code)
}
func TestLocalFileUpload(t *testing.T) {

@ -9,10 +9,10 @@ import (
// DirectoryService 创建新目录服务
type DirectoryService struct {
Path string `form:"path" json:"path" binding:"required,min=1,max=65535"`
Path string `uri:"path" json:"path" binding:"required,min=1,max=65535"`
}
// ListDirectory 列出目录内容 TODO:test
// ListDirectory 列出目录内容
func (service *DirectoryService) ListDirectory(c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)

@ -30,7 +30,7 @@ func (service *FileDownloadService) Download(ctx context.Context, c *gin.Context
}
// 设置文件名
c.Header("Content-Disposition", "attachment; filename=\""+fs.Target.Name+"\"")
c.Header("Content-Disposition", "attachment; filename=\""+fs.FileTarget[0].Name+"\"")
// 发送文件
http.ServeContent(c.Writer, c.Request, "", time.Time{}, rs)

@ -0,0 +1,34 @@
package explorer
import (
"context"
"github.com/HFO4/cloudreve/pkg/filesystem"
"github.com/HFO4/cloudreve/pkg/serializer"
"github.com/gin-gonic/gin"
)
// ItemService 处理多文件/目录相关服务
type ItemService struct {
Items []string `json:"items" binding:"exists"`
Dirs []string `json:"dirs" binding:"exists"`
}
// Delete 删除对象
func (service *ItemService) Delete(ctx context.Context, c *gin.Context) serializer.Response {
// 创建文件系统
fs, err := filesystem.NewFileSystemFromContext(c)
if err != nil {
return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err)
}
// 删除对象
err = fs.Delete(ctx, service.Dirs, service.Items)
if err != nil {
return serializer.Err(serializer.CodeNotSet, err.Error(), err)
}
return serializer.Response{
Code: 0,
}
}
Loading…
Cancel
Save