diff --git a/drivers/123_open/driver.go b/drivers/123_open/driver.go index 04785ac1e..c8002a9ac 100644 --- a/drivers/123_open/driver.go +++ b/drivers/123_open/driver.go @@ -3,6 +3,7 @@ package _123_open import ( "context" "fmt" + "slices" "strconv" "time" @@ -128,6 +129,26 @@ func (d *Open123) Rename(ctx context.Context, srcObj model.Obj, newName string) return d.rename(fileId, newName) } +func (d *Open123) BatchRename(ctx context.Context, obj model.Obj, renameObjs []model.RenameObj) error { + rl := []string{} + for _, ro := range renameObjs { + fileID, err := strconv.ParseInt(ro.ID, 10, 64) + if err != nil { + return err + } + rl = append(rl, fmt.Sprintf("%d|%s", fileID, ro.NewName)) + + } + // 每次最多30 + for names := range slices.Chunk(rl, 30) { + if err := d.batchRename(names); err != nil { + return err + } + } + + return nil +} + func (d *Open123) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { // 尝试使用上传+MD5秒传功能实现复制 // 1. 创建文件 @@ -151,7 +172,27 @@ func (d *Open123) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { func (d *Open123) Remove(ctx context.Context, obj model.Obj) error { fileId, _ := strconv.ParseInt(obj.GetID(), 10, 64) - return d.trash(fileId) + return d.trash([]int64{fileId}) +} + +// BatchRemove 批量删除 +func (d *Open123) BatchRemove(ctx context.Context, srcObj model.Obj, objs []model.IDName) error { + + ids := []int64{} + for _, obj := range objs { + id, err := strconv.ParseInt(obj.ID, 10, 64) + if err != nil { + return err + } + ids = append(ids, id) + } + //每次最多100 + for cids := range slices.Chunk(ids, 100) { + if err := d.trash(cids); err != nil { + return err + } + } + return nil } func (d *Open123) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { diff --git a/drivers/123_open/util.go b/drivers/123_open/util.go index 52bb5ee87..9d714e337 100644 --- a/drivers/123_open/util.go +++ b/drivers/123_open/util.go @@ -30,6 +30,7 @@ var ( //不同情况下获取的AccessTokenQPS限制不同 如下模块化易于 Mkdir = InitApiInfo(Api+"/upload/v1/file/mkdir", 2) Move = InitApiInfo(Api+"/api/v1/file/move", 1) Rename = InitApiInfo(Api+"/api/v1/file/name", 1) + BatchRename = InitApiInfo(Api+"/api/v1/file/rename", 0) Trash = InitApiInfo(Api+"/api/v1/file/trash", 2) UploadCreate = InitApiInfo(Api+"/upload/v2/file/create", 2) UploadComplete = InitApiInfo(Api+"/upload/v2/file/upload_complete", 0) @@ -248,11 +249,20 @@ func (d *Open123) rename(fileId int64, fileName string) error { return nil } +func (d *Open123) batchRename(renamelist []string) error { -func (d *Open123) trash(fileId int64) error { + _, err := d.Request(BatchRename, http.MethodPost, func(req *resty.Request) { + req.SetBody(base.Json{ + "renameList": renamelist, + }) + }, nil) + return err +} + +func (d *Open123) trash(fileIDs []int64) error { _, err := d.Request(Trash, http.MethodPost, func(req *resty.Request) { req.SetBody(base.Json{ - "fileIDs": []int64{fileId}, + "fileIDs": fileIDs, }) }, nil) if err != nil { diff --git a/drivers/baidu_netdisk/driver.go b/drivers/baidu_netdisk/driver.go index 0fa94e885..86140245e 100644 --- a/drivers/baidu_netdisk/driver.go +++ b/drivers/baidu_netdisk/driver.go @@ -9,6 +9,7 @@ import ( "net/url" "os" stdpath "path" + "path/filepath" "strconv" "time" @@ -133,6 +134,19 @@ func (d *BaiduNetdisk) Rename(ctx context.Context, srcObj model.Obj, newName str return nil, nil } +func (d *BaiduNetdisk) BatchRename(ctx context.Context, obj model.Obj, renameObjs []model.RenameObj) error { + data := []base.Json{} + for _, ro := range renameObjs { + data = append(data, base.Json{ + "path": filepath.Join(obj.GetPath(), ro.SrcName), + "newname": ro.NewName, + }) + } + + _, err := d.manage("rename", data) + return err +} + func (d *BaiduNetdisk) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { data := []base.Json{ { @@ -151,6 +165,16 @@ func (d *BaiduNetdisk) Remove(ctx context.Context, obj model.Obj) error { return err } +func (d *BaiduNetdisk) BatchRemove(ctx context.Context, srcObj model.Obj, objs []model.IDName) error { + data := []string{} + for _, obj := range objs { + data = append(data, filepath.Join(srcObj.GetPath(), obj.Name)) + } + + _, err := d.manage("delete", data) + return err +} + func (d *BaiduNetdisk) PutRapid(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) (model.Obj, error) { contentMd5 := stream.GetHash().GetHash(utils.MD5) if len(contentMd5) < utils.MD5.Width { diff --git a/internal/conf/const.go b/internal/conf/const.go index fd0e1610d..9c883ed53 100644 --- a/internal/conf/const.go +++ b/internal/conf/const.go @@ -173,4 +173,5 @@ const ( UserAgentKey PathKey SharingIDKey + StorageKey ) diff --git a/internal/driver/driver.go b/internal/driver/driver.go index 2884b5438..5b541b4a8 100644 --- a/internal/driver/driver.go +++ b/internal/driver/driver.go @@ -73,6 +73,10 @@ type Rename interface { Rename(ctx context.Context, srcObj model.Obj, newName string) error } +type BatchRename interface { + BatchRename(ctx context.Context, obj model.Obj, renameObjs []model.RenameObj) error +} + type Copy interface { Copy(ctx context.Context, srcObj, dstDir model.Obj) error } @@ -81,6 +85,10 @@ type Remove interface { Remove(ctx context.Context, obj model.Obj) error } +type BatchRemove interface { + BatchRemove(ctx context.Context, srcobj model.Obj, objs []model.IDName) error +} + type Put interface { // Put a file (provided as a FileStreamer) into the driver // Besides the most basic upload functionality, the following features also need to be implemented: diff --git a/internal/fs/fs.go b/internal/fs/fs.go index 8c1f646b5..52f468d0e 100644 --- a/internal/fs/fs.go +++ b/internal/fs/fs.go @@ -3,6 +3,7 @@ package fs import ( "context" "io" + stdpath "path" log "github.com/sirupsen/logrus" @@ -90,6 +91,35 @@ func Rename(ctx context.Context, srcPath, dstName string, lazyCache ...bool) err return err } +func BatchRename(ctx context.Context, storage driver.Driver, srcPath string, renameObjs []model.RenameObj, lazyCache ...bool) error { + + srcRawObj, err := op.Get(ctx, storage, srcPath) + if err != nil { + return errors.WithMessage(err, "failed to get src object") + } + srcObj := model.UnwrapObj(srcRawObj) + srcDirPath := stdpath.Dir(srcPath) + + switch s := storage.(type) { + case driver.BatchRename: + err := s.BatchRename(ctx, srcObj, renameObjs) + if err == nil { + op.ClearCache(storage, srcDirPath) + return nil + } + return err + default: + for _, renameObject := range renameObjs { + err := op.Rename(ctx, storage, stdpath.Join(srcPath, renameObject.SrcName), renameObject.NewName, lazyCache...) + if err != nil { + log.Errorf("failed rename %s to %s: %+v", renameObject.ID, renameObject.NewName, err) + return err + } + } + } + return nil +} + func Remove(ctx context.Context, path string) error { err := remove(ctx, path) if err != nil { @@ -97,6 +127,20 @@ func Remove(ctx context.Context, path string) error { } return err } +func BatchRemove(ctx context.Context, storage driver.Driver, actualPath string, objs []model.IDName) error { + switch storage.(type) { + case driver.BatchRemove: + return op.BatchRemove(ctx, storage, actualPath, objs) + default: + for _, obj := range objs { + err := op.Remove(ctx, storage, stdpath.Join(actualPath, obj.Name)) + if err != nil { + return err + } + } + } + return nil +} func PutDirectly(ctx context.Context, dstDirPath string, file model.FileStreamer, lazyCache ...bool) error { err := putDirectly(ctx, dstDirPath, file, lazyCache...) diff --git a/internal/model/obj.go b/internal/model/obj.go index 836904fce..198ee3f3e 100644 --- a/internal/model/obj.go +++ b/internal/model/obj.go @@ -227,3 +227,15 @@ func (om *ObjMerge) InitHideReg(hides string) { func (om *ObjMerge) Reset() { om.set.Clear() } + +// IDName 文件ID,文件名称,有的网盘使用文件id管理文件,有的使用路径名称 +type IDName struct { + ID string `json:"id" form:"id"` + Name string `json:"name" form:"name"` +} + +type RenameObj struct { + ID string `json:"id"` //文件id,部分网盘需要,如果没有可以不传 + SrcName string `json:"src_name"` + NewName string `json:"new_name"` +} diff --git a/internal/op/fs.go b/internal/op/fs.go index bdf0567be..5f5e16076 100644 --- a/internal/op/fs.go +++ b/internal/op/fs.go @@ -572,6 +572,25 @@ func Remove(ctx context.Context, storage driver.Driver, path string) error { return errors.WithStack(err) } +func BatchRemove(ctx context.Context, storage driver.Driver, actualPath string, objs []model.IDName) error { + srcobj, err := Get(ctx, storage, actualPath) + if err != nil { + return errors.WithMessage(err, "failed to get src object") + } + switch s := storage.(type) { + case driver.BatchRemove: + err := s.BatchRemove(ctx, srcobj, objs) + if err == nil { + ClearCache(storage, actualPath) + return nil + } + return err + default: + return errs.NotImplement + } + +} + func Put(ctx context.Context, storage driver.Driver, dstDirPath string, file model.FileStreamer, up driver.UpdateProgress, lazyCache ...bool) error { close := file.Close defer func() { diff --git a/server/handles/fsbatch.go b/server/handles/fsbatch.go index 162419f7b..86d0400c3 100644 --- a/server/handles/fsbatch.go +++ b/server/handles/fsbatch.go @@ -6,6 +6,7 @@ import ( "slices" "github.com/OpenListTeam/OpenList/v4/internal/conf" + "github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/fs" "github.com/OpenListTeam/OpenList/v4/internal/model" @@ -136,11 +137,7 @@ func FsRecursiveMove(c *gin.Context) { } type BatchRenameReq struct { - SrcDir string `json:"src_dir"` - RenameObjects []struct { - SrcName string `json:"src_name"` - NewName string `json:"new_name"` - } `json:"rename_objects"` + RenameObjects []model.RenameObj `json:"rename_objects"` } func FsBatchRename(c *gin.Context) { @@ -149,41 +146,15 @@ func FsBatchRename(c *gin.Context) { common.ErrorResp(c, err, 400) return } - user := c.Request.Context().Value(conf.UserKey).(*model.User) - if !user.CanRename() { - common.ErrorResp(c, errs.PermissionDenied, 403) - return - } + storage := c.Request.Context().Value(conf.StorageKey).(driver.Driver) + path := c.Request.Context().Value(conf.PathKey).(string) - reqPath, err := user.JoinPath(req.SrcDir) + err := fs.BatchRename(c.Request.Context(), storage, path, req.RenameObjects) if err != nil { - common.ErrorResp(c, err, 403) + common.ErrorResp(c, err, 500) return } - meta, err := op.GetNearestMeta(reqPath) - if err != nil { - if !errors.Is(errors.Cause(err), errs.MetaNotFound) { - common.ErrorResp(c, err, 500, true) - return - } - } - common.GinWithValue(c, conf.MetaKey, meta) - for _, renameObject := range req.RenameObjects { - if renameObject.SrcName == "" || renameObject.NewName == "" { - continue - } - err = checkRelativePath(renameObject.NewName) - if err != nil { - common.ErrorResp(c, err, 403) - return - } - filePath := fmt.Sprintf("%s/%s", reqPath, renameObject.SrcName) - if err := fs.Rename(c.Request.Context(), filePath, renameObject.NewName); err != nil { - common.ErrorResp(c, err, 500) - return - } - } common.SuccessResp(c) } diff --git a/server/handles/fsmanage.go b/server/handles/fsmanage.go index f45da69bb..7d1bef1ed 100644 --- a/server/handles/fsmanage.go +++ b/server/handles/fsmanage.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/OpenListTeam/OpenList/v4/internal/conf" + "github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/task" "github.com/OpenListTeam/OpenList/v4/internal/errs" @@ -236,8 +237,7 @@ func checkRelativePath(path string) error { } type RemoveReq struct { - Dir string `json:"dir"` - Names []string `json:"names"` + Names []model.IDName `json:"names"` } func FsRemove(c *gin.Context) { @@ -250,23 +250,17 @@ func FsRemove(c *gin.Context) { common.ErrorStrResp(c, "Empty file names", 400) return } - user := c.Request.Context().Value(conf.UserKey).(*model.User) - if !user.CanRemove() { - common.ErrorResp(c, errs.PermissionDenied, 403) - return - } - reqDir, err := user.JoinPath(req.Dir) + + storage := c.Request.Context().Value(conf.StorageKey).(driver.Driver) + actualPath := c.Request.Context().Value(conf.PathKey).(string) + + err := fs.BatchRemove(c.Request.Context(), storage, actualPath, req.Names) + if err != nil { - common.ErrorResp(c, err, 403) + common.ErrorResp(c, err, 500) return } - for _, name := range req.Names { - err := fs.Remove(c.Request.Context(), stdpath.Join(reqDir, name)) - if err != nil { - common.ErrorResp(c, err, 500) - return - } - } + //fs.ClearCache(req.Dir) common.SuccessResp(c) } diff --git a/server/middlewares/fs.go b/server/middlewares/fs.go new file mode 100644 index 000000000..9df9f60b0 --- /dev/null +++ b/server/middlewares/fs.go @@ -0,0 +1,88 @@ +package middlewares + +import ( + "net/url" + stdpath "path" + + "github.com/OpenListTeam/OpenList/v4/internal/conf" + "github.com/OpenListTeam/OpenList/v4/internal/errs" + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/internal/op" + "github.com/OpenListTeam/OpenList/v4/server/common" + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +// 文件操作相关中间件 +// 把文件路径校验及鉴权统一放在这里处理,并将处理结果放到上下文里,避免后续重复处理 +// // Middleware for file operations +// Centralizes file path validation and authentication here, and stores the results in the context +// to avoid redundant processing in subsequent steps + +type permissionFunc func(user *model.User, meta *model.Meta, path string, password string) bool + +func FsUp(c *gin.Context) { + fs(c, false, func(user *model.User, meta *model.Meta, path string, password string) bool { + return common.CanAccess(user, meta, path, password) && (user.CanWrite() || common.CanWrite(meta, stdpath.Dir(path))) + }) +} +func FsRename(c *gin.Context) { + fs(c, true, func(user *model.User, meta *model.Meta, path string, password string) bool { + return user.CanRename() + }) +} +func FsRemove(c *gin.Context) { + fs(c, true, func(user *model.User, meta *model.Meta, path string, password string) bool { + return user.CanRemove() + }) +} + +func fs(c *gin.Context, withstorage bool, permission permissionFunc) { + path := c.GetHeader("File-Path") + password := c.GetHeader("Password") + path, err := url.PathUnescape(path) + if err != nil { + common.ErrorResp(c, err, 400) + c.Abort() + return + } + user := c.Request.Context().Value(conf.UserKey).(*model.User) + path, err = user.JoinPath(path) + if err != nil { + common.ErrorResp(c, err, 403) + c.Abort() + return + } + meta, err := op.GetNearestMeta(stdpath.Dir(path)) + if err != nil { + if !errors.Is(errors.Cause(err), errs.MetaNotFound) { + common.ErrorResp(c, err, 500, true) + c.Abort() + return + } + } + + if !permission(user, meta, path, password) { + common.ErrorResp(c, errs.PermissionDenied, 403) + c.Abort() + return + } + + if withstorage { + storage, actualPath, err := op.GetStorageAndActualPath(path) + if err != nil { + common.ErrorResp(c, err, 400) + c.Abort() + return + } + if storage.Config().NoUpload { + common.ErrorStrResp(c, "Current storage doesn't support upload", 403) + c.Abort() + return + } + + common.GinWithValue(c, conf.StorageKey, storage) + common.GinWithValue(c, conf.PathKey, actualPath) //这里的路径已经是网盘真实路径了 + } + c.Next() +} diff --git a/server/middlewares/fsup.go b/server/middlewares/fsup.go deleted file mode 100644 index 08b160ee5..000000000 --- a/server/middlewares/fsup.go +++ /dev/null @@ -1,45 +0,0 @@ -package middlewares - -import ( - "net/url" - stdpath "path" - - "github.com/OpenListTeam/OpenList/v4/internal/conf" - "github.com/OpenListTeam/OpenList/v4/internal/errs" - "github.com/OpenListTeam/OpenList/v4/internal/model" - "github.com/OpenListTeam/OpenList/v4/internal/op" - "github.com/OpenListTeam/OpenList/v4/server/common" - "github.com/gin-gonic/gin" - "github.com/pkg/errors" -) - -func FsUp(c *gin.Context) { - path := c.GetHeader("File-Path") - password := c.GetHeader("Password") - path, err := url.PathUnescape(path) - if err != nil { - common.ErrorResp(c, err, 400) - c.Abort() - return - } - user := c.Request.Context().Value(conf.UserKey).(*model.User) - path, err = user.JoinPath(path) - if err != nil { - common.ErrorResp(c, err, 403) - return - } - meta, err := op.GetNearestMeta(stdpath.Dir(path)) - if err != nil { - if !errors.Is(errors.Cause(err), errs.MetaNotFound) { - common.ErrorResp(c, err, 500, true) - c.Abort() - return - } - } - if !(common.CanAccess(user, meta, path, password) && (user.CanWrite() || common.CanWrite(meta, stdpath.Dir(path)))) { - common.ErrorResp(c, errs.PermissionDenied, 403) - c.Abort() - return - } - c.Next() -} diff --git a/server/router.go b/server/router.go index 66f0539ba..e07599d9e 100644 --- a/server/router.go +++ b/server/router.go @@ -194,12 +194,12 @@ func _fs(g *gin.RouterGroup) { g.Any("/dirs", handles.FsDirs) g.POST("/mkdir", handles.FsMkdir) g.POST("/rename", handles.FsRename) - g.POST("/batch_rename", handles.FsBatchRename) + g.POST("/batch_rename", middlewares.FsRename, handles.FsBatchRename) g.POST("/regex_rename", handles.FsRegexRename) g.POST("/move", handles.FsMove) g.POST("/recursive_move", handles.FsRecursiveMove) g.POST("/copy", handles.FsCopy) - g.POST("/remove", handles.FsRemove) + g.POST("/remove", middlewares.FsRemove, handles.FsRemove) g.POST("/remove_empty_directory", handles.FsRemoveEmptyDirectory) uploadLimiter := middlewares.UploadRateLimiter(stream.ClientUploadLimit) g.PUT("/put", middlewares.FsUp, uploadLimiter, handles.FsStream)