alist/server/handles/task.go

226 lines
6.9 KiB
Go

package handles
import (
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/task"
"math"
"time"
"github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/offline_download/tool"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
"github.com/gin-gonic/gin"
"github.com/xhofe/tache"
)
type TaskInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Creator string `json:"creator"`
CreatorRole int `json:"creator_role"`
State tache.State `json:"state"`
Status string `json:"status"`
Progress float64 `json:"progress"`
StartTime *time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time"`
TotalBytes int64 `json:"total_bytes"`
Error string `json:"error"`
}
func getTaskInfo[T task.TaskExtensionInfo](task T) TaskInfo {
errMsg := ""
if task.GetErr() != nil {
errMsg = task.GetErr().Error()
}
progress := task.GetProgress()
// if progress is NaN, set it to 100
if math.IsNaN(progress) {
progress = 100
}
creatorName := ""
creatorRole := -1
if task.GetCreator() != nil {
creatorName = task.GetCreator().Username
creatorRole = task.GetCreator().Role
}
return TaskInfo{
ID: task.GetID(),
Name: task.GetName(),
Creator: creatorName,
CreatorRole: creatorRole,
State: task.GetState(),
Status: task.GetStatus(),
Progress: progress,
StartTime: task.GetStartTime(),
EndTime: task.GetEndTime(),
TotalBytes: task.GetTotalBytes(),
Error: errMsg,
}
}
func getTaskInfos[T task.TaskExtensionInfo](tasks []T) []TaskInfo {
return utils.MustSliceConvert(tasks, getTaskInfo[T])
}
func argsContains[T comparable](v T, slice ...T) bool {
return utils.SliceContains(slice, v)
}
func getUserInfo(c *gin.Context) (bool, uint, bool) {
if user, ok := c.Value("user").(*model.User); ok {
return user.IsAdmin(), user.ID, true
} else {
return false, 0, false
}
}
func getTargetedHandler[T task.TaskExtensionInfo](manager task.Manager[T], callback func(c *gin.Context, task T)) gin.HandlerFunc {
return func(c *gin.Context) {
isAdmin, uid, ok := getUserInfo(c)
if !ok {
// if there is no bug, here is unreachable
common.ErrorStrResp(c, "user invalid", 401)
return
}
t, ok := manager.GetByID(c.Query("tid"))
if !ok {
common.ErrorStrResp(c, "task not found", 404)
return
}
if !isAdmin && uid != t.GetCreator().ID {
// to avoid an attacker using error messages to guess valid TID, return a 404 rather than a 403
common.ErrorStrResp(c, "task not found", 404)
return
}
callback(c, t)
}
}
func getBatchHandler[T task.TaskExtensionInfo](manager task.Manager[T], callback func(task T)) gin.HandlerFunc {
return func(c *gin.Context) {
isAdmin, uid, ok := getUserInfo(c)
if !ok {
common.ErrorStrResp(c, "user invalid", 401)
return
}
var tids []string
if err := c.ShouldBind(&tids); err != nil {
common.ErrorStrResp(c, "invalid request format", 400)
return
}
retErrs := make(map[string]string)
for _, tid := range tids {
t, ok := manager.GetByID(tid)
if !ok || (!isAdmin && uid != t.GetCreator().ID) {
retErrs[tid] = "task not found"
continue
}
callback(t)
}
common.SuccessResp(c, retErrs)
}
}
func taskRoute[T task.TaskExtensionInfo](g *gin.RouterGroup, manager task.Manager[T]) {
g.GET("/undone", func(c *gin.Context) {
isAdmin, uid, ok := getUserInfo(c)
if !ok {
// if there is no bug, here is unreachable
common.ErrorStrResp(c, "user invalid", 401)
return
}
common.SuccessResp(c, getTaskInfos(manager.GetByCondition(func(task T) bool {
// avoid directly passing the user object into the function to reduce closure size
return (isAdmin || uid == task.GetCreator().ID) &&
argsContains(task.GetState(), tache.StatePending, tache.StateRunning, tache.StateCanceling,
tache.StateErrored, tache.StateFailing, tache.StateWaitingRetry, tache.StateBeforeRetry)
})))
})
g.GET("/done", func(c *gin.Context) {
isAdmin, uid, ok := getUserInfo(c)
if !ok {
// if there is no bug, here is unreachable
common.ErrorStrResp(c, "user invalid", 401)
return
}
common.SuccessResp(c, getTaskInfos(manager.GetByCondition(func(task T) bool {
return (isAdmin || uid == task.GetCreator().ID) &&
argsContains(task.GetState(), tache.StateCanceled, tache.StateFailed, tache.StateSucceeded)
})))
})
g.POST("/info", getTargetedHandler(manager, func(c *gin.Context, task T) {
common.SuccessResp(c, getTaskInfo(task))
}))
g.POST("/cancel", getTargetedHandler(manager, func(c *gin.Context, task T) {
manager.Cancel(task.GetID())
common.SuccessResp(c)
}))
g.POST("/delete", getTargetedHandler(manager, func(c *gin.Context, task T) {
manager.Remove(task.GetID())
common.SuccessResp(c)
}))
g.POST("/retry", getTargetedHandler(manager, func(c *gin.Context, task T) {
manager.Retry(task.GetID())
common.SuccessResp(c)
}))
g.POST("/cancel_some", getBatchHandler(manager, func(task T) {
manager.Cancel(task.GetID())
}))
g.POST("/delete_some", getBatchHandler(manager, func(task T) {
manager.Remove(task.GetID())
}))
g.POST("/retry_some", getBatchHandler(manager, func(task T) {
manager.Retry(task.GetID())
}))
g.POST("/clear_done", func(c *gin.Context) {
isAdmin, uid, ok := getUserInfo(c)
if !ok {
// if there is no bug, here is unreachable
common.ErrorStrResp(c, "user invalid", 401)
return
}
manager.RemoveByCondition(func(task T) bool {
return (isAdmin || uid == task.GetCreator().ID) &&
argsContains(task.GetState(), tache.StateCanceled, tache.StateFailed, tache.StateSucceeded)
})
common.SuccessResp(c)
})
g.POST("/clear_succeeded", func(c *gin.Context) {
isAdmin, uid, ok := getUserInfo(c)
if !ok {
// if there is no bug, here is unreachable
common.ErrorStrResp(c, "user invalid", 401)
return
}
manager.RemoveByCondition(func(task T) bool {
return (isAdmin || uid == task.GetCreator().ID) && task.GetState() == tache.StateSucceeded
})
common.SuccessResp(c)
})
g.POST("/retry_failed", func(c *gin.Context) {
isAdmin, uid, ok := getUserInfo(c)
if !ok {
// if there is no bug, here is unreachable
common.ErrorStrResp(c, "user invalid", 401)
return
}
tasks := manager.GetByCondition(func(task T) bool {
return (isAdmin || uid == task.GetCreator().ID) && task.GetState() == tache.StateFailed
})
for _, t := range tasks {
manager.Retry(t.GetID())
}
common.SuccessResp(c)
})
}
func SetupTaskRoute(g *gin.RouterGroup) {
taskRoute(g.Group("/upload"), fs.UploadTaskManager)
taskRoute(g.Group("/copy"), fs.CopyTaskManager)
taskRoute(g.Group("/offline_download"), tool.DownloadTaskManager)
taskRoute(g.Group("/offline_download_transfer"), tool.TransferTaskManager)
taskRoute(g.Group("/decompress"), fs.ArchiveDownloadTaskManager)
taskRoute(g.Group("/decompress_upload"), fs.ArchiveContentUploadTaskManager)
}