This commit is contained in:
2024-02-25 08:30:34 +08:00
commit 4947f39e74
273 changed files with 45396 additions and 0 deletions

175
pkg/task/compress.go Normal file
View File

@ -0,0 +1,175 @@
package task
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
)
// CompressTask 文件压缩任务
type CompressTask struct {
User *model.User
TaskModel *model.Task
TaskProps CompressProps
Err *JobError
zipPath string
}
// CompressProps 压缩任务属性
type CompressProps struct {
Dirs []uint `json:"dirs"`
Files []uint `json:"files"`
Dst string `json:"dst"`
}
// Props 获取任务属性
func (job *CompressTask) Props() string {
res, _ := json.Marshal(job.TaskProps)
return string(res)
}
// Type 获取任务状态
func (job *CompressTask) Type() int {
return CompressTaskType
}
// Creator 获取创建者ID
func (job *CompressTask) Creator() uint {
return job.User.ID
}
// Model 获取任务的数据库模型
func (job *CompressTask) Model() *model.Task {
return job.TaskModel
}
// SetStatus 设定状态
func (job *CompressTask) SetStatus(status int) {
job.TaskModel.SetStatus(status)
}
// SetError 设定任务失败信息
func (job *CompressTask) SetError(err *JobError) {
job.Err = err
res, _ := json.Marshal(job.Err)
job.TaskModel.SetError(string(res))
// 删除压缩文件
job.removeZipFile()
}
func (job *CompressTask) removeZipFile() {
if job.zipPath != "" {
if err := os.Remove(job.zipPath); err != nil {
util.Log().Warning("Failed to delete temp zip file %q: %s", job.zipPath, err)
}
}
}
// SetErrorMsg 设定任务失败信息
func (job *CompressTask) SetErrorMsg(msg string) {
job.SetError(&JobError{Msg: msg})
}
// GetError 返回任务失败信息
func (job *CompressTask) GetError() *JobError {
return job.Err
}
// Do 开始执行任务
func (job *CompressTask) Do() {
// 创建文件系统
fs, err := filesystem.NewFileSystem(job.User)
if err != nil {
job.SetErrorMsg(err.Error())
return
}
util.Log().Debug("Starting compress file...")
job.TaskModel.SetProgress(CompressingProgress)
// 创建临时压缩文件
saveFolder := "compress"
zipFilePath := filepath.Join(
util.RelativePath(model.GetSettingByName("temp_path")),
saveFolder,
fmt.Sprintf("archive_%d.zip", time.Now().UnixNano()),
)
zipFile, err := util.CreatNestedFile(zipFilePath)
if err != nil {
util.Log().Warning("%s", err)
job.SetErrorMsg(err.Error())
return
}
defer zipFile.Close()
// 开始压缩
ctx := context.Background()
err = fs.Compress(ctx, zipFile, job.TaskProps.Dirs, job.TaskProps.Files, false)
if err != nil {
job.SetErrorMsg(err.Error())
return
}
job.zipPath = zipFilePath
zipFile.Close()
util.Log().Debug("Compressed file saved to %q, start uploading it...", zipFile)
job.TaskModel.SetProgress(TransferringProgress)
// 上传文件
err = fs.UploadFromPath(ctx, zipFilePath, job.TaskProps.Dst, 0)
if err != nil {
job.SetErrorMsg(err.Error())
return
}
job.removeZipFile()
}
// NewCompressTask 新建压缩任务
func NewCompressTask(user *model.User, dst string, dirs, files []uint) (Job, error) {
newTask := &CompressTask{
User: user,
TaskProps: CompressProps{
Dirs: dirs,
Files: files,
Dst: dst,
},
}
record, err := Record(newTask)
if err != nil {
return nil, err
}
newTask.TaskModel = record
return newTask, nil
}
// NewRelocateTaskFromModel 从数据库记录中恢复迁移任务
func NewCompressTaskFromModel(task *model.Task) (Job, error) {
user, err := model.GetActiveUserByID(task.UserID)
if err != nil {
return nil, err
}
newTask := &CompressTask{
User: &user,
TaskModel: task,
}
err = json.Unmarshal([]byte(task.Props), &newTask.TaskProps)
if err != nil {
return nil, err
}
return newTask, nil
}

131
pkg/task/decompress.go Normal file
View File

@ -0,0 +1,131 @@
package task
import (
"context"
"encoding/json"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/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"`
Encoding string `json:"encoding"`
}
// 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("Failed to create filesystem.", err)
return
}
job.TaskModel.SetProgress(DecompressingProgress)
err = fs.Decompress(context.Background(), job.TaskProps.Src, job.TaskProps.Dst, job.TaskProps.Encoding)
if err != nil {
job.SetErrorMsg("Failed to decompress file.", err)
return
}
}
// NewDecompressTask 新建压缩任务
func NewDecompressTask(user *model.User, src, dst, encoding string) (Job, error) {
newTask := &DecompressTask{
User: user,
TaskProps: DecompressProps{
Src: src,
Dst: dst,
Encoding: encoding,
},
}
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.GetActiveUserByID(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
}

8
pkg/task/errors.go Normal file
View File

@ -0,0 +1,8 @@
package task
import "errors"
var (
// ErrUnknownTaskType 未知任务类型
ErrUnknownTaskType = errors.New("unknown task type")
)

220
pkg/task/import.go Normal file
View File

@ -0,0 +1,220 @@
package task
import (
"context"
"encoding/json"
"path"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
)
// ImportTask 导入务
type ImportTask struct {
User *model.User
TaskModel *model.Task
TaskProps ImportProps
Err *JobError
}
// ImportProps 导入任务属性
type ImportProps struct {
PolicyID uint `json:"policy_id"` // 存储策略ID
Src string `json:"src"` // 原始路径
Recursive bool `json:"is_recursive"` // 是否递归导入
Dst string `json:"dst"` // 目的目录
}
// Props 获取任务属性
func (job *ImportTask) Props() string {
res, _ := json.Marshal(job.TaskProps)
return string(res)
}
// Type 获取任务状态
func (job *ImportTask) Type() int {
return ImportTaskType
}
// Creator 获取创建者ID
func (job *ImportTask) Creator() uint {
return job.User.ID
}
// Model 获取任务的数据库模型
func (job *ImportTask) Model() *model.Task {
return job.TaskModel
}
// SetStatus 设定状态
func (job *ImportTask) SetStatus(status int) {
job.TaskModel.SetStatus(status)
}
// SetError 设定任务失败信息
func (job *ImportTask) SetError(err *JobError) {
job.Err = err
res, _ := json.Marshal(job.Err)
job.TaskModel.SetError(string(res))
}
// SetErrorMsg 设定任务失败信息
func (job *ImportTask) SetErrorMsg(msg string, err error) {
jobErr := &JobError{Msg: msg}
if err != nil {
jobErr.Error = err.Error()
}
job.SetError(jobErr)
}
// GetError 返回任务失败信息
func (job *ImportTask) GetError() *JobError {
return job.Err
}
// Do 开始执行任务
func (job *ImportTask) Do() {
ctx := context.Background()
// 查找存储策略
policy, err := model.GetPolicyByID(job.TaskProps.PolicyID)
if err != nil {
job.SetErrorMsg("Policy not exist.", err)
return
}
// 创建文件系统
fs, err := filesystem.NewFileSystem(job.User)
if err != nil {
job.SetErrorMsg(err.Error(), nil)
return
}
defer fs.Recycle()
fs.Policy = &policy
if err := fs.DispatchHandler(); err != nil {
job.SetErrorMsg("Failed to dispatch policy.", err)
return
}
// 注册钩子
fs.Use("BeforeAddFile", filesystem.HookValidateFile)
fs.Use("BeforeAddFile", filesystem.HookValidateCapacity)
// 列取目录、对象
job.TaskModel.SetProgress(ListingProgress)
coxIgnoreConflict := context.WithValue(context.Background(), fsctx.IgnoreDirectoryConflictCtx,
true)
objects, err := fs.Handler.List(ctx, job.TaskProps.Src, job.TaskProps.Recursive)
if err != nil {
job.SetErrorMsg("Failed to list files.", err)
return
}
job.TaskModel.SetProgress(InsertingProgress)
// 虚拟目录路径与folder对象ID的对应
pathCache := make(map[string]*model.Folder, len(objects))
// 插入目录记录到用户文件系统
for _, object := range objects {
if object.IsDir {
// 创建目录
virtualPath := path.Join(job.TaskProps.Dst, object.RelativePath)
folder, err := fs.CreateDirectory(coxIgnoreConflict, virtualPath)
if err != nil {
util.Log().Warning("Importing task cannot create user directory %q: %s", virtualPath, err)
} else if folder.ID > 0 {
pathCache[virtualPath] = folder
}
}
}
// 插入文件记录到用户文件系统
for _, object := range objects {
if !object.IsDir {
// 创建文件信息
virtualPath := path.Dir(path.Join(job.TaskProps.Dst, object.RelativePath))
fileHeader := fsctx.FileStream{
Size: object.Size,
VirtualPath: virtualPath,
Name: object.Name,
SavePath: object.Source,
}
// 查找父目录
parentFolder := &model.Folder{}
if parent, ok := pathCache[virtualPath]; ok {
parentFolder = parent
} else {
folder, err := fs.CreateDirectory(context.Background(), virtualPath)
if err != nil {
util.Log().Warning("Importing task cannot create user directory %q: %s",
virtualPath, err)
continue
}
parentFolder = folder
}
// 插入文件记录
_, err := fs.AddFile(context.Background(), parentFolder, &fileHeader)
if err != nil {
util.Log().Warning("Importing task cannot insert user file %q: %s",
object.RelativePath, err)
if err == filesystem.ErrInsufficientCapacity {
job.SetErrorMsg("Insufficient storage capacity.", err)
return
}
}
}
}
}
// NewImportTask 新建导入任务
func NewImportTask(user, policy uint, src, dst string, recursive bool) (Job, error) {
creator, err := model.GetActiveUserByID(user)
if err != nil {
return nil, err
}
newTask := &ImportTask{
User: &creator,
TaskProps: ImportProps{
PolicyID: policy,
Recursive: recursive,
Src: src,
Dst: dst,
},
}
record, err := Record(newTask)
if err != nil {
return nil, err
}
newTask.TaskModel = record
return newTask, nil
}
// NewImportTaskFromModel 从数据库记录中恢复导入任务
func NewImportTaskFromModel(task *model.Task) (Job, error) {
user, err := model.GetActiveUserByID(task.UserID)
if err != nil {
return nil, err
}
newTask := &ImportTask{
User: &user,
TaskModel: task,
}
err = json.Unmarshal([]byte(task.Props), &newTask.TaskProps)
if err != nil {
return nil, err
}
return newTask, nil
}

127
pkg/task/job.go Normal file
View File

@ -0,0 +1,127 @@
package task
import (
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
)
// 任务类型
const (
// CompressTaskType 压缩任务
CompressTaskType = iota
// DecompressTaskType 解压缩任务
DecompressTaskType
// TransferTaskType 中转任务
TransferTaskType
// ImportTaskType 导入任务
ImportTaskType
// RelocateTaskType 存储策略迁移任务
RelocateTaskType
// RecycleTaskType 回收任务
RecycleTaskType
)
// 任务状态
const (
// Queued 排队中
Queued = iota
// Processing 处理中
Processing
// Error 失败
Error
// Canceled 取消
Canceled
// Complete 完成
Complete
)
// 任务进度
const (
// PendingProgress 等待中
PendingProgress = iota
// Compressing 压缩中
CompressingProgress
// Decompressing 解压缩中
DecompressingProgress
// Downloading 下载中
DownloadingProgress
// Transferring 转存中
TransferringProgress
// ListingProgress 索引中
ListingProgress
// InsertingProgress 插入中
InsertingProgress
)
// Job 任务接口
type Job interface {
Type() int // 返回任务类型
Creator() uint // 返回创建者ID
Props() string // 返回序列化后的任务属性
Model() *model.Task // 返回对应的数据库模型
SetStatus(int) // 设定任务状态
Do() // 开始执行任务
SetError(*JobError) // 设定任务失败信息
GetError() *JobError // 获取任务执行结果返回nil表示成功完成执行
}
// JobError 任务失败信息
type JobError struct {
Msg string `json:"msg,omitempty"`
Error string `json:"error,omitempty"`
}
// Record 将任务记录到数据库中
func Record(job Job) (*model.Task, error) {
record := model.Task{
Status: Queued,
Type: job.Type(),
UserID: job.Creator(),
Progress: 0,
Error: "",
Props: job.Props(),
}
_, err := record.Create()
return &record, err
}
// Resume 从数据库中恢复未完成任务
func Resume(p Pool) {
tasks := model.GetTasksByStatus(Queued, Processing)
if len(tasks) == 0 {
return
}
util.Log().Info("Resume %d unfinished task(s) from database.", len(tasks))
for i := 0; i < len(tasks); i++ {
job, err := GetJobFromModel(&tasks[i])
if err != nil {
util.Log().Warning("Failed to resume task: %s", err)
continue
}
if job != nil {
p.Submit(job)
}
}
}
// GetJobFromModel 从数据库给定模型获取任务
func GetJobFromModel(task *model.Task) (Job, error) {
switch task.Type {
case CompressTaskType:
return NewCompressTaskFromModel(task)
case DecompressTaskType:
return NewDecompressTaskFromModel(task)
case TransferTaskType:
return NewTransferTaskFromModel(task)
case ImportTaskType:
return NewImportTaskFromModel(task)
case RelocateTaskType:
return NewRelocateTaskFromModel(task)
case RecycleTaskType:
return NewRecycleTaskFromModel(task)
default:
return nil, ErrUnknownTaskType
}
}

68
pkg/task/pool.go Normal file
View File

@ -0,0 +1,68 @@
package task
import (
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/conf"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
)
// TaskPoll 要使用的任务池
var TaskPoll Pool
type Pool interface {
Add(num int)
Submit(job Job)
}
// AsyncPool 带有最大配额的任务池
type AsyncPool struct {
// 容量
idleWorker chan int
}
// Add 增加可用Worker数量
func (pool *AsyncPool) Add(num int) {
for i := 0; i < num; i++ {
pool.idleWorker <- 1
}
}
// ObtainWorker 阻塞直到获取新的Worker
func (pool *AsyncPool) obtainWorker() Worker {
select {
case <-pool.idleWorker:
// 有空闲Worker名额时返回新Worker
return &GeneralWorker{}
}
}
// FreeWorker 添加空闲Worker
func (pool *AsyncPool) freeWorker() {
pool.Add(1)
}
// Submit 开始提交任务
func (pool *AsyncPool) Submit(job Job) {
go func() {
util.Log().Debug("Waiting for Worker.")
worker := pool.obtainWorker()
util.Log().Debug("Worker obtained.")
worker.Do(job)
util.Log().Debug("Worker released.")
pool.freeWorker()
}()
}
// Init 初始化任务池
func Init() {
maxWorker := model.GetIntSetting("max_worker_num", 10)
TaskPoll = &AsyncPool{
idleWorker: make(chan int, maxWorker),
}
TaskPoll.Add(maxWorker)
util.Log().Info("Initialize task queue with WorkerNum = %d", maxWorker)
if conf.SystemConfig.Mode == "master" {
Resume(TaskPoll)
}
}

130
pkg/task/recycle.go Normal file
View File

@ -0,0 +1,130 @@
package task
import (
"encoding/json"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
)
// RecycleTask 文件回收任务
type RecycleTask struct {
User *model.User
TaskModel *model.Task
TaskProps RecycleProps
Err *JobError
}
// RecycleProps 回收任务属性
type RecycleProps struct {
// 下载任务 GID
DownloadGID string `json:"download_gid"`
}
// Props 获取任务属性
func (job *RecycleTask) Props() string {
res, _ := json.Marshal(job.TaskProps)
return string(res)
}
// Type 获取任务状态
func (job *RecycleTask) Type() int {
return RecycleTaskType
}
// Creator 获取创建者ID
func (job *RecycleTask) Creator() uint {
return job.User.ID
}
// Model 获取任务的数据库模型
func (job *RecycleTask) Model() *model.Task {
return job.TaskModel
}
// SetStatus 设定状态
func (job *RecycleTask) SetStatus(status int) {
job.TaskModel.SetStatus(status)
}
// SetError 设定任务失败信息
func (job *RecycleTask) SetError(err *JobError) {
job.Err = err
res, _ := json.Marshal(job.Err)
job.TaskModel.SetError(string(res))
}
// SetErrorMsg 设定任务失败信息
func (job *RecycleTask) SetErrorMsg(msg string, err error) {
jobErr := &JobError{Msg: msg}
if err != nil {
jobErr.Error = err.Error()
}
job.SetError(jobErr)
}
// GetError 返回任务失败信息
func (job *RecycleTask) GetError() *JobError {
return job.Err
}
// Do 开始执行任务
func (job *RecycleTask) Do() {
download, err := model.GetDownloadByGid(job.TaskProps.DownloadGID, job.User.ID)
if err != nil {
util.Log().Warning("Recycle task %d cannot found download record.", job.TaskModel.ID)
job.SetErrorMsg("Cannot found download task.", err)
return
}
nodeID := download.GetNodeID()
node := cluster.Default.GetNodeByID(nodeID)
if node == nil {
util.Log().Warning("Recycle task %d cannot found node.", job.TaskModel.ID)
job.SetErrorMsg("Invalid slave node.", nil)
return
}
err = node.GetAria2Instance().DeleteTempFile(download)
if err != nil {
util.Log().Warning("Failed to delete transfer temp folder %q: %s", download.Parent, err)
job.SetErrorMsg("Failed to recycle files.", err)
return
}
}
// NewRecycleTask 新建回收任务
func NewRecycleTask(download *model.Download) (Job, error) {
newTask := &RecycleTask{
User: download.GetOwner(),
TaskProps: RecycleProps{
DownloadGID: download.GID,
},
}
record, err := Record(newTask)
if err != nil {
return nil, err
}
newTask.TaskModel = record
return newTask, nil
}
// NewRecycleTaskFromModel 从数据库记录中恢复回收任务
func NewRecycleTaskFromModel(task *model.Task) (Job, error) {
user, err := model.GetActiveUserByID(task.UserID)
if err != nil {
return nil, err
}
newTask := &RecycleTask{
User: &user,
TaskModel: task,
}
err = json.Unmarshal([]byte(task.Props), &newTask.TaskProps)
if err != nil {
return nil, err
}
return newTask, nil
}

176
pkg/task/relocate.go Normal file
View File

@ -0,0 +1,176 @@
package task
import (
"context"
"encoding/json"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
)
// RelocateTask 存储策略迁移任务
type RelocateTask struct {
User *model.User
TaskModel *model.Task
TaskProps RelocateProps
Err *JobError
}
// RelocateProps 存储策略迁移任务属性
type RelocateProps struct {
Dirs []uint `json:"dirs"`
Files []uint `json:"files"`
DstPolicyID uint `json:"dst_policy_id"`
}
// Props 获取任务属性
func (job *RelocateTask) Props() string {
res, _ := json.Marshal(job.TaskProps)
return string(res)
}
// Type 获取任务状态
func (job *RelocateTask) Type() int {
return RelocateTaskType
}
// Creator 获取创建者ID
func (job *RelocateTask) Creator() uint {
return job.User.ID
}
// Model 获取任务的数据库模型
func (job *RelocateTask) Model() *model.Task {
return job.TaskModel
}
// SetStatus 设定状态
func (job *RelocateTask) SetStatus(status int) {
job.TaskModel.SetStatus(status)
}
// SetError 设定任务失败信息
func (job *RelocateTask) SetError(err *JobError) {
job.Err = err
res, _ := json.Marshal(job.Err)
job.TaskModel.SetError(string(res))
}
// SetErrorMsg 设定任务失败信息
func (job *RelocateTask) SetErrorMsg(msg string) {
job.SetError(&JobError{Msg: msg})
}
// GetError 返回任务失败信息
func (job *RelocateTask) GetError() *JobError {
return job.Err
}
// Do 开始执行任务
func (job *RelocateTask) Do() {
// 创建文件系统
fs, err := filesystem.NewFileSystem(job.User)
if err != nil {
job.SetErrorMsg(err.Error())
return
}
job.TaskModel.SetProgress(ListingProgress)
util.Log().Debug("Start migration task.")
// ----------------------------
// 索引出所有待迁移的文件
// ----------------------------
targetFiles := make([]model.File, 0, len(job.TaskProps.Files))
// 索引用户选择的单独的文件
outerFiles, err := model.GetFilesByIDs(job.TaskProps.Files, job.User.ID)
if err != nil {
job.SetError(&JobError{
Msg: "Failed to index files.",
Error: err.Error(),
})
return
}
targetFiles = append(targetFiles, outerFiles...)
// 索引用户选择目录下的所有递归子文件
subFolders, err := model.GetRecursiveChildFolder(job.TaskProps.Dirs, job.User.ID, true)
if err != nil {
job.SetError(&JobError{
Msg: "Failed to index child folders.",
Error: err.Error(),
})
return
}
subFiles, err := model.GetChildFilesOfFolders(&subFolders)
if err != nil {
job.SetError(&JobError{
Msg: "Failed to index child files.",
Error: err.Error(),
})
return
}
targetFiles = append(targetFiles, subFiles...)
// 查找目标存储策略
policy, err := model.GetPolicyByID(job.TaskProps.DstPolicyID)
if err != nil {
job.SetError(&JobError{
Msg: "Invalid policy.",
Error: err.Error(),
})
return
}
// 开始转移文件
job.TaskModel.SetProgress(TransferringProgress)
ctx := context.Background()
err = fs.Relocate(ctx, targetFiles, &policy)
if err != nil {
job.SetErrorMsg(err.Error())
return
}
return
}
// NewRelocateTask 新建转移任务
func NewRelocateTask(user *model.User, dstPolicyID uint, dirs, files []uint) (Job, error) {
newTask := &RelocateTask{
User: user,
TaskProps: RelocateProps{
Dirs: dirs,
Files: files,
DstPolicyID: dstPolicyID,
},
}
record, err := Record(newTask)
if err != nil {
return nil, err
}
newTask.TaskModel = record
return newTask, nil
}
// NewCompressTaskFromModel 从数据库记录中恢复压缩任务
func NewRelocateTaskFromModel(task *model.Task) (Job, error) {
user, err := model.GetActiveUserByID(task.UserID)
if err != nil {
return nil, err
}
newTask := &RelocateTask{
User: &user,
TaskModel: task,
}
err = json.Unmarshal([]byte(task.Props), &newTask.TaskProps)
if err != nil {
return nil, err
}
return newTask, nil
}

View File

@ -0,0 +1,138 @@
package slavetask
import (
"context"
"os"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/mq"
"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
"github.com/cloudreve/Cloudreve/v3/pkg/task"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
)
// TransferTask 文件中转任务
type TransferTask struct {
Err *task.JobError
Req *serializer.SlaveTransferReq
MasterID string
}
// Props 获取任务属性
func (job *TransferTask) Props() string {
return ""
}
// Type 获取任务类型
func (job *TransferTask) Type() int {
return 0
}
// Creator 获取创建者ID
func (job *TransferTask) Creator() uint {
return 0
}
// Model 获取任务的数据库模型
func (job *TransferTask) Model() *model.Task {
return nil
}
// SetStatus 设定状态
func (job *TransferTask) SetStatus(status int) {
}
// SetError 设定任务失败信息
func (job *TransferTask) SetError(err *task.JobError) {
job.Err = err
}
// SetErrorMsg 设定任务失败信息
func (job *TransferTask) SetErrorMsg(msg string, err error) {
jobErr := &task.JobError{Msg: msg}
if err != nil {
jobErr.Error = err.Error()
}
job.SetError(jobErr)
notifyMsg := mq.Message{
TriggeredBy: job.MasterID,
Event: serializer.SlaveTransferFailed,
Content: serializer.SlaveTransferResult{
Error: err.Error(),
},
}
if err := cluster.DefaultController.SendNotification(job.MasterID, job.Req.Hash(job.MasterID), notifyMsg); err != nil {
util.Log().Warning("Failed to send transfer failure notification to master node: %s", err)
}
}
// GetError 返回任务失败信息
func (job *TransferTask) GetError() *task.JobError {
return job.Err
}
// Do 开始执行任务
func (job *TransferTask) Do() {
fs, err := filesystem.NewAnonymousFileSystem()
if err != nil {
job.SetErrorMsg("Failed to initialize anonymous filesystem.", err)
return
}
fs.Policy = job.Req.Policy
if err := fs.DispatchHandler(); err != nil {
job.SetErrorMsg("Failed to dispatch policy.", err)
return
}
master, err := cluster.DefaultController.GetMasterInfo(job.MasterID)
if err != nil {
job.SetErrorMsg("Cannot found master node ID.", err)
return
}
fs.SwitchToShadowHandler(master.Instance, master.URL.String(), master.ID)
file, err := os.Open(util.RelativePath(job.Req.Src))
if err != nil {
job.SetErrorMsg("Failed to read source file.", err)
return
}
defer file.Close()
// 获取源文件大小
fi, err := file.Stat()
if err != nil {
job.SetErrorMsg("Failed to get source file size.", err)
return
}
size := fi.Size()
err = fs.Handler.Put(context.Background(), &fsctx.FileStream{
File: file,
SavePath: job.Req.Dst,
Size: uint64(size),
})
if err != nil {
job.SetErrorMsg("Upload failed.", err)
return
}
msg := mq.Message{
TriggeredBy: job.MasterID,
Event: serializer.SlaveTransferSuccess,
Content: serializer.SlaveTransferResult{},
}
if err := cluster.DefaultController.SendNotification(job.MasterID, job.Req.Hash(job.MasterID), msg); err != nil {
util.Log().Warning("Failed to send transfer success notification to master node: %s", err)
}
}

192
pkg/task/tranfer.go Normal file
View File

@ -0,0 +1,192 @@
package task
import (
"context"
"encoding/json"
"fmt"
"path"
"path/filepath"
"strings"
model "github.com/cloudreve/Cloudreve/v3/models"
"github.com/cloudreve/Cloudreve/v3/pkg/cluster"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
)
// TransferTask 文件中转任务
type TransferTask struct {
User *model.User
TaskModel *model.Task
TaskProps TransferProps
Err *JobError
zipPath string
}
// TransferProps 中转任务属性
type TransferProps struct {
Src []string `json:"src"` // 原始文件
SrcSizes map[string]uint64 `json:"src_size"` // 原始文件的大小信息,从机转存时使用
Parent string `json:"parent"` // 父目录
Dst string `json:"dst"` // 目的目录ID
// 将会保留原始文件的目录结构Src 除去 Parent 开头作为最终路径
TrimPath bool `json:"trim_path"`
// 负责处理中专任务的节点ID
NodeID uint `json:"node_id"`
}
// Props 获取任务属性
func (job *TransferTask) Props() string {
res, _ := json.Marshal(job.TaskProps)
return string(res)
}
// Type 获取任务状态
func (job *TransferTask) Type() int {
return TransferTaskType
}
// Creator 获取创建者ID
func (job *TransferTask) Creator() uint {
return job.User.ID
}
// Model 获取任务的数据库模型
func (job *TransferTask) Model() *model.Task {
return job.TaskModel
}
// SetStatus 设定状态
func (job *TransferTask) SetStatus(status int) {
job.TaskModel.SetStatus(status)
}
// SetError 设定任务失败信息
func (job *TransferTask) SetError(err *JobError) {
job.Err = err
res, _ := json.Marshal(job.Err)
job.TaskModel.SetError(string(res))
}
// SetErrorMsg 设定任务失败信息
func (job *TransferTask) SetErrorMsg(msg string, err error) {
jobErr := &JobError{Msg: msg}
if err != nil {
jobErr.Error = err.Error()
}
job.SetError(jobErr)
}
// GetError 返回任务失败信息
func (job *TransferTask) GetError() *JobError {
return job.Err
}
// Do 开始执行任务
func (job *TransferTask) Do() {
// 创建文件系统
fs, err := filesystem.NewFileSystem(job.User)
if err != nil {
job.SetErrorMsg(err.Error(), nil)
return
}
defer fs.Recycle()
successCount := 0
errorList := make([]string, 0, len(job.TaskProps.Src))
for _, file := range job.TaskProps.Src {
dst := path.Join(job.TaskProps.Dst, filepath.Base(file))
if job.TaskProps.TrimPath {
// 保留原始目录
trim := util.FormSlash(job.TaskProps.Parent)
src := util.FormSlash(file)
dst = path.Join(job.TaskProps.Dst, strings.TrimPrefix(src, trim))
}
if job.TaskProps.NodeID > 1 {
// 指定为从机中转
// 获取从机节点
node := cluster.Default.GetNodeByID(job.TaskProps.NodeID)
if node == nil {
job.SetErrorMsg("Invalid slave node.", nil)
}
// 切换为从机节点处理上传
fs.SetPolicyFromPath(path.Dir(dst))
fs.SwitchToSlaveHandler(node)
err = fs.UploadFromStream(context.Background(), &fsctx.FileStream{
File: nil,
Size: job.TaskProps.SrcSizes[file],
Name: path.Base(dst),
VirtualPath: path.Dir(dst),
Src: file,
}, false)
} else {
// 主机节点中转
err = fs.UploadFromPath(context.Background(), file, dst, 0)
}
if err != nil {
errorList = append(errorList, err.Error())
} else {
successCount++
job.TaskModel.SetProgress(successCount)
}
}
if len(errorList) > 0 {
job.SetErrorMsg("Failed to transfer one or more file(s).", fmt.Errorf(strings.Join(errorList, "\n")))
}
}
// NewTransferTask 新建中转任务
func NewTransferTask(user uint, src []string, dst, parent string, trim bool, node uint, sizes map[string]uint64) (Job, error) {
creator, err := model.GetActiveUserByID(user)
if err != nil {
return nil, err
}
newTask := &TransferTask{
User: &creator,
TaskProps: TransferProps{
Src: src,
Parent: parent,
Dst: dst,
TrimPath: trim,
NodeID: node,
SrcSizes: sizes,
},
}
record, err := Record(newTask)
if err != nil {
return nil, err
}
newTask.TaskModel = record
return newTask, nil
}
// NewTransferTaskFromModel 从数据库记录中恢复中转任务
func NewTransferTaskFromModel(task *model.Task) (Job, error) {
user, err := model.GetActiveUserByID(task.UserID)
if err != nil {
return nil, err
}
newTask := &TransferTask{
User: &user,
TaskModel: task,
}
err = json.Unmarshal([]byte(task.Props), &newTask.TaskProps)
if err != nil {
return nil, err
}
return newTask, nil
}

44
pkg/task/worker.go Normal file
View File

@ -0,0 +1,44 @@
package task
import (
"fmt"
"github.com/cloudreve/Cloudreve/v3/pkg/util"
)
// Worker 处理任务的对象
type Worker interface {
Do(Job) // 执行任务
}
// GeneralWorker 通用Worker
type GeneralWorker struct {
}
// Do 执行任务
func (worker *GeneralWorker) Do(job Job) {
util.Log().Debug("Start executing task.")
job.SetStatus(Processing)
defer func() {
// 致命错误捕获
if err := recover(); err != nil {
util.Log().Debug("Failed to execute task: %s", err)
job.SetError(&JobError{Msg: "Fatal error.", Error: fmt.Sprintf("%s", err)})
job.SetStatus(Error)
}
}()
// 开始执行任务
job.Do()
// 任务执行失败
if err := job.GetError(); err != nil {
util.Log().Debug("Failed to execute task.")
job.SetStatus(Error)
return
}
util.Log().Debug("Task finished.")
// 执行完成
job.SetStatus(Complete)
}