Feat: decompression task
This commit is contained in:
parent
e722c33cd5
commit
91e202c7e6
19 changed files with 440 additions and 52 deletions
|
@ -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 获取用户离线下载设备
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 初始化一个文件系统
|
||||
|
|
|
@ -31,4 +31,6 @@ const (
|
|||
ShareKeyCtx
|
||||
// LimitParentCtx 限制父目录
|
||||
LimitParentCtx
|
||||
// IgnoreConflictCtx 忽略重名冲突
|
||||
IgnoreConflictCtx
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
|
|
|
@ -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 将别人分享的文件转存到目标路径下
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
127
pkg/task/decompress.go
Normal file
127
pkg/task/decompress.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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, "\\", "/"))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
// 创建上下文
|
||||
|
|
|
@ -269,6 +269,8 @@ func InitMasterRouter() *gin.Engine {
|
|||
file.POST("archive", controllers.Archive)
|
||||
// 创建文件压缩任务
|
||||
file.POST("compress", controllers.Compress)
|
||||
// 创建文件解压缩任务
|
||||
file.POST("decompress", controllers.Decompress)
|
||||
}
|
||||
|
||||
// 目录
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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{}
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue