diff --git a/models/group.go b/models/group.go index bf2ffce..58d7334 100644 --- a/models/group.go +++ b/models/group.go @@ -25,11 +25,13 @@ type Group struct { // GroupOption 用户组其他配置 type GroupOption struct { - ArchiveDownload bool `json:"archive_download,omitempty"` - ArchiveTask bool `json:"archive_task,omitempty"` - OneTimeDownload bool `json:"one_time_download,omitempty"` - ShareDownload bool `json:"share_download,omitempty"` - ShareFree bool `json:"share_free,omitempty"` + ArchiveDownload bool `json:"archive_download,omitempty"` + ArchiveTask bool `json:"archive_task,omitempty"` + CompressSize uint64 `json:"compress_size,omitempty"` + DecompressSize uint64 `json:"decompress_size,omitempty"` + OneTimeDownload bool `json:"one_time_download,omitempty"` + ShareDownload bool `json:"share_download,omitempty"` + ShareFree bool `json:"share_free,omitempty"` } // GetAria2Option 获取用户离线下载设备 diff --git a/models/migration.go b/models/migration.go index d025c2f..06267d1 100644 --- a/models/migration.go +++ b/models/migration.go @@ -160,6 +160,7 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti {Name: "aria2_rpcurl", Value: `http://127.0.0.1:6800/`, Type: "aria2"}, {Name: "aria2_options", Value: `{"max-tries":5}`, Type: "aria2"}, {Name: "max_worker_num", Value: `10`, Type: "task"}, + {Name: "max_parallel_transfer", Value: `4`, Type: "task"}, {Name: "secret_key", Value: util.RandStringRunes(256), Type: "auth"}, {Name: "temp_path", Value: "temp", Type: "path"}, {Name: "score_enabled", Value: "1", Type: "score"}, diff --git a/pkg/filesystem/archive.go b/pkg/filesystem/archive.go index 18a9965..b602bb6 100644 --- a/pkg/filesystem/archive.go +++ b/pkg/filesystem/archive.go @@ -10,7 +10,10 @@ import ( "github.com/gin-gonic/gin" "io" "os" + "path" "path/filepath" + "strings" + "sync" "time" ) @@ -148,7 +151,7 @@ func (fs *FileSystem) doCompress(ctx context.Context, file *model.File, folder * // 创建压缩文件头 header := &zip.FileHeader{ - Name: filepath.Join(file.Position, file.Name), + Name: filepath.FromSlash(path.Join(file.Position, file.Name)), Modified: file.UpdatedAt, UncompressedSize64: file.Size, } @@ -185,3 +188,117 @@ func (fs *FileSystem) doCompress(ctx context.Context, file *model.File, folder * } } } + +// Decompress 解压缩给定压缩文件到dst目录 +func (fs *FileSystem) Decompress(ctx context.Context, src, dst string) error { + err := fs.resetFileIfNotExist(ctx, src) + if err != nil { + return err + } + + tempZipFilePath := "" + defer func() { + // 结束时删除临时压缩文件 + if tempZipFilePath != "" { + if err := os.Remove(tempZipFilePath); err != nil { + util.Log().Warning("无法删除临时压缩文件 %s , %s", tempZipFilePath, err) + } + } + }() + + // 下载压缩文件到临时目录 + fileStream, err := fs.Handler.Get(ctx, fs.FileTarget[0].SourceName) + if err != nil { + return err + } + + tempZipFilePath = filepath.Join( + model.GetSettingByName("temp_path"), + "decompress", + fmt.Sprintf("archive_%d.zip", time.Now().UnixNano()), + ) + + zipFile, err := util.CreatNestedFile(tempZipFilePath) + if err != nil { + util.Log().Warning("无法创建临时压缩文件 %s , %s", tempZipFilePath, err) + tempZipFilePath = "" + return err + } + defer zipFile.Close() + + _, err = io.Copy(zipFile, fileStream) + if err != nil { + util.Log().Warning("无法写入临时压缩文件 %s , %s", tempZipFilePath, err) + return err + } + + zipFile.Close() + + // 解压缩文件 + r, err := zip.OpenReader(tempZipFilePath) + if err != nil { + return err + } + defer r.Close() + + // 重设存储策略 + fs.Policy = &fs.User.Policy + err = fs.DispatchHandler() + if err != nil { + return err + } + + var wg sync.WaitGroup + parallel := model.GetIntSetting("max_parallel_transfer", 4) + worker := make(chan int, parallel) + for i := 0; i < parallel; i++ { + worker <- i + } + + for _, f := range r.File { + rawPath := util.FormSlash(f.Name) + savePath := path.Join(dst, rawPath) + // 路径是否合法 + if !strings.HasPrefix(savePath, path.Clean(dst)+"/") { + return fmt.Errorf("%s: illegal file path", f.Name) + } + + // 如果是目录 + if f.FileInfo().IsDir() { + fs.CreateDirectory(ctx, savePath) + continue + } + + // 上传文件 + fileStream, err := f.Open() + if err != nil { + util.Log().Warning("无法打开压缩包内文件%s , %s , 跳过", rawPath, err) + continue + } + + select { + case <-worker: + go func(fileStream io.ReadCloser, size int64) { + wg.Add(1) + defer func() { + worker <- 1 + wg.Done() + if err := recover(); err != nil { + util.Log().Warning("上传压缩包内文件时出错") + fmt.Println(err) + } + }() + + err = fs.UploadFromStream(ctx, fileStream, savePath, uint64(size)) + fileStream.Close() + if err != nil { + util.Log().Debug("无法上传压缩包内的文件%s , %s , 跳过", rawPath, err) + } + }(fileStream, f.FileInfo().Size()) + } + + } + wg.Wait() + return nil + +} diff --git a/pkg/filesystem/driver/onedrive/api.go b/pkg/filesystem/driver/onedrive/api.go index a624646..4c26eaf 100644 --- a/pkg/filesystem/driver/onedrive/api.go +++ b/pkg/filesystem/driver/onedrive/api.go @@ -180,7 +180,7 @@ func (client *Client) UploadChunk(ctx context.Context, uploadURL string, chunk * func (client *Client) Upload(ctx context.Context, dst string, size int, file io.Reader) error { // 小文件,使用简单上传接口上传 if size <= int(SmallFileSize) { - _, err := client.SimpleUpload(ctx, dst, file) + _, err := client.SimpleUpload(ctx, dst, file, int64(size)) return err } @@ -248,11 +248,11 @@ func (client *Client) DeleteUploadSession(ctx context.Context, uploadURL string) } // SimpleUpload 上传小文件到dst -func (client *Client) SimpleUpload(ctx context.Context, dst string, body io.Reader) (*UploadResult, error) { +func (client *Client) SimpleUpload(ctx context.Context, dst string, body io.Reader, size int64) (*UploadResult, error) { dst = strings.TrimPrefix(dst, "/") requestURL := client.getRequestURL("me/drive/root:/" + dst + ":/content") - res, err := client.request(ctx, "PUT", requestURL, body) + res, err := client.request(ctx, "PUT", requestURL, body, request.WithContentLength(int64(size))) if err != nil { return nil, err } @@ -381,7 +381,7 @@ func (client *Client) MonitorUpload(uploadURL, callbackKey, path string, size ui case <-time.After(time.Duration(ttl) * time.Second): // 上传会话到期,仍未完成上传,创建占位符 client.DeleteUploadSession(context.Background(), uploadURL) - _, err := client.SimpleUpload(context.Background(), path, strings.NewReader("")) + _, err := client.SimpleUpload(context.Background(), path, strings.NewReader(""), 0) if err != nil { util.Log().Debug("无法创建占位文件,%s", err) } @@ -428,7 +428,7 @@ func (client *Client) MonitorUpload(uploadURL, callbackKey, path string, size ui // 取消上传会话,实测OneDrive取消上传会话后,客户端还是可以上传, // 所以上传一个空文件占位,阻止客户端上传 client.DeleteUploadSession(context.Background(), uploadURL) - _, err := client.SimpleUpload(context.Background(), path, strings.NewReader("")) + _, err := client.SimpleUpload(context.Background(), path, strings.NewReader(""), 0) if err != nil { util.Log().Debug("无法创建占位文件,%s", err) } diff --git a/pkg/filesystem/errors.go b/pkg/filesystem/errors.go index 7fa54f8..56f3d35 100644 --- a/pkg/filesystem/errors.go +++ b/pkg/filesystem/errors.go @@ -12,6 +12,7 @@ var ( ErrInsufficientCapacity = errors.New("容量空间不足") ErrIllegalObjectName = errors.New("目标名称非法") ErrClientCanceled = errors.New("客户端取消操作") + ErrRootProtected = errors.New("无法对根目录进行操作") ErrInsertFileRecord = serializer.NewError(serializer.CodeDBError, "无法插入文件记录", nil) ErrFileExisted = serializer.NewError(serializer.CodeObjectExist, "同名文件或目录已存在", nil) ErrFolderExisted = serializer.NewError(serializer.CodeObjectExist, "同名目录已存在", nil) diff --git a/pkg/filesystem/filesystem.go b/pkg/filesystem/filesystem.go index e718f79..d6119db 100644 --- a/pkg/filesystem/filesystem.go +++ b/pkg/filesystem/filesystem.go @@ -78,6 +78,8 @@ type FileSystem struct { DirTarget []model.Folder // 相对根目录 Root *model.Folder + // 互斥锁 + Lock sync.Mutex /* 钩子函数 @@ -110,6 +112,7 @@ func (fs *FileSystem) reset() { fs.Hooks = nil fs.Handler = nil fs.Root = nil + fs.Lock = sync.Mutex{} } // NewFileSystem 初始化一个文件系统 diff --git a/pkg/filesystem/fsctx/context.go b/pkg/filesystem/fsctx/context.go index c5700d0..355d0a5 100644 --- a/pkg/filesystem/fsctx/context.go +++ b/pkg/filesystem/fsctx/context.go @@ -31,4 +31,6 @@ const ( ShareKeyCtx // LimitParentCtx 限制父目录 LimitParentCtx + // IgnoreConflictCtx 忽略重名冲突 + IgnoreConflictCtx ) diff --git a/pkg/filesystem/hooks.go b/pkg/filesystem/hooks.go index 6561fca..725d689 100644 --- a/pkg/filesystem/hooks.go +++ b/pkg/filesystem/hooks.go @@ -28,6 +28,15 @@ func (fs *FileSystem) Use(name string, hook Hook) { fs.Hooks[name] = []Hook{hook} } +// CleanHooks 清空钩子,name为空表示全部清空 +func (fs *FileSystem) CleanHooks(name string) { + if name == "" { + fs.Hooks = nil + } else { + delete(fs.Hooks, name) + } +} + // Trigger 触发钩子,遇到第一个错误时 // 返回错误,后续钩子不会继续执行 func (fs *FileSystem) Trigger(ctx context.Context, name string) error { @@ -247,10 +256,14 @@ func GenericAfterUpload(ctx context.Context, fs *FileSystem) error { // 文件存放的虚拟路径 virtualPath := ctx.Value(fsctx.FileHeaderCtx).(FileHeader).GetVirtualPath() - // 检查路径是否存在 + // 检查路径是否存在,不存在就创建 isExist, folder := fs.IsPathExist(virtualPath) if !isExist { - return ErrPathNotExist + newFolder, err := fs.CreateDirectory(ctx, virtualPath) + if err != nil { + return err + } + folder = newFolder } // 检查文件是否存在 diff --git a/pkg/filesystem/manage.go b/pkg/filesystem/manage.go index 6a21f14..650f9e5 100644 --- a/pkg/filesystem/manage.go +++ b/pkg/filesystem/manage.go @@ -328,8 +328,12 @@ func (fs *FileSystem) List(ctx context.Context, dirPath string, pathProcessor fu return objects, nil } -// CreateDirectory 根据给定的完整创建目录,不会递归创建 -func (fs *FileSystem) CreateDirectory(ctx context.Context, fullPath string) error { +// CreateDirectory 根据给定的完整创建目录,支持递归创建 +func (fs *FileSystem) CreateDirectory(ctx context.Context, fullPath string) (*model.Folder, error) { + if fullPath == "/" { + return nil, ErrRootProtected + } + // 获取要创建目录的父路径和目录名 fullPath = path.Clean(fullPath) base := path.Dir(fullPath) @@ -340,18 +344,26 @@ func (fs *FileSystem) CreateDirectory(ctx context.Context, fullPath string) erro // 检查目录名是否合法 if !fs.ValidateLegalName(ctx, dir) { - return ErrIllegalObjectName + return nil, ErrIllegalObjectName } // 父目录是否存在 isExist, parent := fs.IsPathExist(base) if !isExist { - return ErrPathNotExist + // 递归创建父目录 + if _, ok := ctx.Value(fsctx.IgnoreConflictCtx).(bool); !ok { + ctx = context.WithValue(ctx, fsctx.IgnoreConflictCtx, true) + } + newParent, err := fs.CreateDirectory(ctx, base) + if err != nil { + return nil, err + } + parent = newParent } // 是否有同名文件 if ok, _ := fs.IsChildFileExist(parent, dir); ok { - return ErrFileExisted + return nil, ErrFileExisted } // 创建目录 @@ -363,9 +375,12 @@ func (fs *FileSystem) CreateDirectory(ctx context.Context, fullPath string) erro _, err := newFolder.Create() if err != nil { - return ErrFolderExisted + if _, ok := ctx.Value(fsctx.IgnoreConflictCtx).(bool); !ok { + return nil, ErrFolderExisted + } + } - return nil + return &newFolder, nil } // SaveTo 将别人分享的文件转存到目标路径下 diff --git a/pkg/filesystem/manage_test.go b/pkg/filesystem/manage_test.go index 6196fd9..6980e27 100644 --- a/pkg/filesystem/manage_test.go +++ b/pkg/filesystem/manage_test.go @@ -146,12 +146,12 @@ func TestFileSystem_CreateDirectory(t *testing.T) { ctx := context.Background() // 目录名非法 - err := fs.CreateDirectory(ctx, "/ad/a+?") + _, 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") + _, err = fs.CreateDirectory(ctx, "/ad/ab") asserts.Equal(ErrPathNotExist, err) asserts.NoError(mock.ExpectationsWereMet()) @@ -166,7 +166,7 @@ func TestFileSystem_CreateDirectory(t *testing.T) { 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") + _, err = fs.CreateDirectory(ctx, "/ad/ab") asserts.Equal(ErrFileExisted, err) asserts.NoError(mock.ExpectationsWereMet()) @@ -183,7 +183,7 @@ func TestFileSystem_CreateDirectory(t *testing.T) { mock.ExpectBegin() mock.ExpectExec("INSERT(.+)").WillReturnError(errors.New("s")) mock.ExpectRollback() - err = fs.CreateDirectory(ctx, "/ad/ab") + _, err = fs.CreateDirectory(ctx, "/ad/ab") asserts.Error(err) asserts.NoError(mock.ExpectationsWereMet()) @@ -201,7 +201,7 @@ func TestFileSystem_CreateDirectory(t *testing.T) { mock.ExpectBegin() mock.ExpectExec("INSERT(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit() - err = fs.CreateDirectory(ctx, "/ad/ab") + _, err = fs.CreateDirectory(ctx, "/ad/ab") asserts.NoError(err) asserts.NoError(mock.ExpectationsWereMet()) } diff --git a/pkg/filesystem/upload.go b/pkg/filesystem/upload.go index 96eb702..9f45bcb 100644 --- a/pkg/filesystem/upload.go +++ b/pkg/filesystem/upload.go @@ -9,6 +9,7 @@ import ( "github.com/HFO4/cloudreve/pkg/serializer" "github.com/HFO4/cloudreve/pkg/util" "github.com/gin-gonic/gin" + "io" "os" "path" ) @@ -186,8 +187,45 @@ func (fs *FileSystem) GetUploadToken(ctx context.Context, path string, size uint return &credential, nil } +// UploadFromStream 从文件流上传文件 +func (fs *FileSystem) UploadFromStream(ctx context.Context, src io.ReadCloser, dst string, size uint64) error { + // 构建文件头 + fileName := path.Base(dst) + filePath := path.Dir(dst) + fileData := local.FileStream{ + File: src, + Size: size, + Name: fileName, + VirtualPath: filePath, + } + + // 给文件系统分配钩子 + fs.Lock.Lock() + if fs.Hooks == nil { + fs.Use("BeforeUpload", HookValidateFile) + fs.Use("BeforeUpload", HookValidateCapacity) + fs.Use("AfterUploadCanceled", HookDeleteTempFile) + fs.Use("AfterUploadCanceled", HookGiveBackCapacity) + fs.Use("AfterUpload", GenericAfterUpload) + fs.Use("AfterValidateFailed", HookDeleteTempFile) + fs.Use("AfterValidateFailed", HookGiveBackCapacity) + fs.Use("AfterUploadFailed", HookGiveBackCapacity) + } + fs.Lock.Unlock() + + // 开始上传 + return fs.Upload(ctx, fileData) +} + // UploadFromPath 将本机已有文件上传到用户的文件系统 func (fs *FileSystem) UploadFromPath(ctx context.Context, src, dst string) error { + // 重设存储策略 + fs.Policy = &fs.User.Policy + err := fs.DispatchHandler() + if err != nil { + return err + } + file, err := os.Open(src) if err != nil { return err @@ -201,26 +239,6 @@ func (fs *FileSystem) UploadFromPath(ctx context.Context, src, dst string) error } size := fi.Size() - // 构建文件头 - fileName := path.Base(dst) - filePath := path.Dir(dst) - fileData := local.FileStream{ - File: file, - Size: uint64(size), - Name: fileName, - VirtualPath: filePath, - } - - // 给文件系统分配钩子 - fs.Use("BeforeUpload", HookValidateFile) - fs.Use("BeforeUpload", HookValidateCapacity) - fs.Use("AfterUploadCanceled", HookDeleteTempFile) - fs.Use("AfterUploadCanceled", HookGiveBackCapacity) - fs.Use("AfterUpload", GenericAfterUpload) - fs.Use("AfterValidateFailed", HookDeleteTempFile) - fs.Use("AfterValidateFailed", HookGiveBackCapacity) - fs.Use("AfterUploadFailed", HookGiveBackCapacity) - // 开始上传 - return fs.Upload(ctx, fileData) + return fs.UploadFromStream(ctx, file, dst, uint64(size)) } diff --git a/pkg/task/decompress.go b/pkg/task/decompress.go new file mode 100644 index 0000000..7d766ef --- /dev/null +++ b/pkg/task/decompress.go @@ -0,0 +1,127 @@ +package task + +import ( + "context" + "encoding/json" + model "github.com/HFO4/cloudreve/models" + "github.com/HFO4/cloudreve/pkg/filesystem" +) + +// DecompressTask 文件压缩任务 +type DecompressTask struct { + User *model.User + TaskModel *model.Task + TaskProps DecompressProps + Err *JobError + + zipPath string +} + +// DecompressProps 压缩任务属性 +type DecompressProps struct { + Src string `json:"src"` + Dst string `json:"dst"` +} + +// Props 获取任务属性 +func (job *DecompressTask) Props() string { + res, _ := json.Marshal(job.TaskProps) + return string(res) +} + +// Type 获取任务状态 +func (job *DecompressTask) Type() int { + return DecompressTaskType +} + +// Creator 获取创建者ID +func (job *DecompressTask) Creator() uint { + return job.User.ID +} + +// Model 获取任务的数据库模型 +func (job *DecompressTask) Model() *model.Task { + return job.TaskModel +} + +// SetStatus 设定状态 +func (job *DecompressTask) SetStatus(status int) { + job.TaskModel.SetStatus(status) +} + +// SetError 设定任务失败信息 +func (job *DecompressTask) SetError(err *JobError) { + job.Err = err + res, _ := json.Marshal(job.Err) + job.TaskModel.SetError(string(res)) +} + +// SetErrorMsg 设定任务失败信息 +func (job *DecompressTask) SetErrorMsg(msg string, err error) { + jobErr := &JobError{Msg: msg} + if err != nil { + jobErr.Error = err.Error() + } + job.SetError(jobErr) +} + +// GetError 返回任务失败信息 +func (job *DecompressTask) GetError() *JobError { + return job.Err +} + +// Do 开始执行任务 +func (job *DecompressTask) Do() { + // 创建文件系统 + fs, err := filesystem.NewFileSystem(job.User) + if err != nil { + job.SetErrorMsg("无法创建文件系统", err) + return + } + defer fs.Recycle() + + err = fs.Decompress(context.Background(), job.TaskProps.Src, job.TaskProps.Dst) + if err != nil { + job.SetErrorMsg("解压缩失败", err) + return + } + +} + +// NewDecompressTask 新建压缩任务 +func NewDecompressTask(user *model.User, src, dst string) (Job, error) { + newTask := &DecompressTask{ + User: user, + TaskProps: DecompressProps{ + Src: src, + Dst: dst, + }, + } + + record, err := Record(newTask) + if err != nil { + return nil, err + } + newTask.TaskModel = record + + return newTask, nil +} + +// NewDecompressTaskFromModel 从数据库记录中恢复压缩任务 +func NewDecompressTaskFromModel(task *model.Task) (Job, error) { + user, err := model.GetUserByID(task.UserID) + if err != nil { + return nil, err + } + newTask := &DecompressTask{ + User: &user, + TaskModel: task, + } + + err = json.Unmarshal([]byte(task.Props), &newTask.TaskProps) + if err != nil { + return nil, err + } + + return newTask, nil +} diff --git a/pkg/task/job.go b/pkg/task/job.go index 057483d..ede0be7 100644 --- a/pkg/task/job.go +++ b/pkg/task/job.go @@ -9,6 +9,8 @@ import ( const ( // CompressTaskType 压缩任务 CompressTaskType = iota + // DecompressTaskType 解压缩任务 + DecompressTaskType ) // 任务状态 @@ -27,8 +29,10 @@ const ( // 任务进度 const ( + // PendingProgress 等待中 + PendingProgress = iota // Compressing 压缩中 - CompressingProgress = iota + CompressingProgress // Decompressing 解压缩中 DecompressingProgress // Downloading 下载中 @@ -51,7 +55,8 @@ type Job interface { // JobError 任务失败信息 type JobError struct { - Msg string + Msg string `json:"msg,omitempty"` + Error string `json:"error,omitempty"` } // Record 将任务记录到数据库中 @@ -92,6 +97,8 @@ func GetJobFromModel(task *model.Task) (Job, error) { switch task.Type { case CompressTaskType: return NewCompressTaskFromModel(task) + case DecompressTaskType: + return NewDecompressTaskFromModel(task) default: return nil, ErrUnknownTaskType } diff --git a/pkg/util/path.go b/pkg/util/path.go index 8fd35b7..a1b78cb 100644 --- a/pkg/util/path.go +++ b/pkg/util/path.go @@ -1,6 +1,9 @@ package util -import "strings" +import ( + "path" + "strings" +) // DotPathToStandardPath 将","分割的路径转换为标准路径 func DotPathToStandardPath(path string) string { @@ -37,3 +40,8 @@ func SplitPath(path string) []string { pathSplit[0] = "/" return pathSplit } + +// FormSlash 将path中的反斜杠'\'替换为'/' +func FormSlash(old string) string { + return path.Clean(strings.ReplaceAll(old, "\\", "/")) +} diff --git a/pkg/webdav/webdav.go b/pkg/webdav/webdav.go index d171b84..6d25f8c 100644 --- a/pkg/webdav/webdav.go +++ b/pkg/webdav/webdav.go @@ -326,6 +326,7 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request, fs *filesyst exist, originFile := fs.IsFileExist(reqPath) if exist { // 已存在,为更新操作 + fs.Use("BeforeUpload", filesystem.HookValidateFile) fs.Use("BeforeUpload", filesystem.HookResetPolicy) fs.Use("BeforeUpload", filesystem.HookChangeCapacity) @@ -382,7 +383,7 @@ func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request, fs *filesy if r.ContentLength > 0 { return http.StatusUnsupportedMediaType, nil } - if err := fs.CreateDirectory(ctx, reqPath); err != nil { + if _, err := fs.CreateDirectory(ctx, reqPath); err != nil { return http.StatusConflict, err } return http.StatusCreated, nil diff --git a/routers/controllers/file.go b/routers/controllers/file.go index b059b94..f18738a 100644 --- a/routers/controllers/file.go +++ b/routers/controllers/file.go @@ -58,6 +58,17 @@ func Compress(c *gin.Context) { } } +// Decompress 创建文件解压缩任务 +func Decompress(c *gin.Context) { + var service explorer.ItemDecompressService + if err := c.ShouldBindJSON(&service); err == nil { + res := service.CreateDecompressTask(c) + c.JSON(200, res) + } else { + c.JSON(200, ErrorResponse(err)) + } +} + // AnonymousGetContent 匿名获取文件资源 func AnonymousGetContent(c *gin.Context) { // 创建上下文 diff --git a/routers/router.go b/routers/router.go index f846335..b24ac73 100644 --- a/routers/router.go +++ b/routers/router.go @@ -269,6 +269,8 @@ func InitMasterRouter() *gin.Engine { file.POST("archive", controllers.Archive) // 创建文件压缩任务 file.POST("compress", controllers.Compress) + // 创建文件解压缩任务 + file.POST("decompress", controllers.Decompress) } // 目录 diff --git a/service/explorer/directory.go b/service/explorer/directory.go index e932905..3ee5064 100644 --- a/service/explorer/directory.go +++ b/service/explorer/directory.go @@ -51,7 +51,7 @@ func (service *DirectoryService) CreateDirectory(c *gin.Context) serializer.Resp defer cancel() // 创建目录 - err = fs.CreateDirectory(ctx, service.Path) + _, err = fs.CreateDirectory(ctx, service.Path) if err != nil { return serializer.Err(serializer.CodeCreateFolderFailed, err.Error(), err) } diff --git a/service/explorer/objects.go b/service/explorer/objects.go index 4080593..3a023b1 100644 --- a/service/explorer/objects.go +++ b/service/explorer/objects.go @@ -15,6 +15,7 @@ import ( "math" "net/url" "path" + "strings" "time" ) @@ -44,6 +45,59 @@ type ItemCompressService struct { Name string `json:"name" binding:"required,min=1,max=255"` } +// ItemDecompressService 文件解压缩任务服务 +type ItemDecompressService struct { + Src string `json:"src" binding:"exists"` + Dst string `json:"dst" binding:"required,min=1,max=65535"` +} + +// CreateDecompressTask 创建文件解压缩任务 +func (service *ItemDecompressService) CreateDecompressTask(c *gin.Context) serializer.Response { + // 创建文件系统 + fs, err := filesystem.NewFileSystemFromContext(c) + if err != nil { + return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err) + } + defer fs.Recycle() + + // 检查用户组权限 + if !fs.User.Group.OptionsSerialized.ArchiveTask { + return serializer.Err(serializer.CodeGroupNotAllowed, "当前用户组无法进行此操作", nil) + } + + // 存放目录是否存在 + if exist, _ := fs.IsPathExist(service.Dst); !exist { + return serializer.Err(serializer.CodeNotFound, "存放路径不存在", nil) + } + + // 压缩包是否存在 + exist, file := fs.IsFileExist(service.Src) + if !exist { + return serializer.Err(serializer.CodeNotFound, "文件不存在", nil) + } + + // 文件尺寸限制 + if fs.User.Group.OptionsSerialized.DecompressSize != 0 && file.Size > fs.User.Group. + OptionsSerialized.DecompressSize { + return serializer.Err(serializer.CodeParamErr, "文件太大", nil) + } + + // 必须是zip压缩包 + if !strings.HasSuffix(file.Name, ".zip") { + return serializer.Err(serializer.CodeParamErr, "只能解压 ZIP 格式的压缩文件", nil) + } + + // 创建任务 + job, err := task.NewDecompressTask(fs.User, service.Src, service.Dst) + if err != nil { + return serializer.Err(serializer.CodeNotSet, "任务创建失败", err) + } + task.TaskPoll.Submit(job) + + return serializer.Response{} + +} + // CreateCompressTask 创建文件压缩任务 func (service *ItemCompressService) CreateCompressTask(c *gin.Context) serializer.Response { // 创建文件系统 @@ -92,6 +146,12 @@ func (service *ItemCompressService) CreateCompressTask(c *gin.Context) serialize totalSize += files[i].Size } + // 文件尺寸限制 + if fs.User.Group.OptionsSerialized.DecompressSize != 0 && totalSize > fs.User.Group. + OptionsSerialized.CompressSize { + return serializer.Err(serializer.CodeParamErr, "文件太大", nil) + } + // 按照平均压缩率计算用户空间是否足够 compressRatio := 0.4 spaceNeeded := uint64(math.Round(float64(totalSize) * compressRatio)) @@ -105,8 +165,8 @@ func (service *ItemCompressService) CreateCompressTask(c *gin.Context) serialize if err != nil { return serializer.Err(serializer.CodeNotSet, "任务创建失败", err) } - task.TaskPoll.Submit(job) + return serializer.Response{} }