* feat: support general users view and cancel own tasks Add a creator attribute to the upload, copy and offline download tasks, so that a GENERAL task creator can view and cancel them. BREAKING CHANGE: 1. A new internal package `task` including the struct `TaskWithCreator` which embeds `tache.Base` is created, and the past dependence on `tache.Task` will all be transferred to dependence on this package. 2. The API `/admin/task` can now also be accessed via `/task`, and the old endpoint is retained to ensure compatibility with legacy automation scripts. Closes #7398 * fix(deps): update github.com/xhofe/tache to v0.1.3
This commit is contained in:
@ -1,6 +1,8 @@
|
||||
package handles
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/task"
|
||||
"math"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/fs"
|
||||
@ -12,15 +14,17 @@ import (
|
||||
)
|
||||
|
||||
type TaskInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
State tache.State `json:"state"`
|
||||
Status string `json:"status"`
|
||||
Progress float64 `json:"progress"`
|
||||
Error string `json:"error"`
|
||||
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"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func getTaskInfo[T tache.TaskWithInfo](task T) TaskInfo {
|
||||
func getTaskInfo[T task.TaskInfoWithCreator](task T) TaskInfo {
|
||||
errMsg := ""
|
||||
if task.GetErr() != nil {
|
||||
errMsg = task.GetErr().Error()
|
||||
@ -30,62 +34,142 @@ func getTaskInfo[T tache.TaskWithInfo](task T) TaskInfo {
|
||||
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(),
|
||||
State: task.GetState(),
|
||||
Status: task.GetStatus(),
|
||||
Progress: progress,
|
||||
Error: errMsg,
|
||||
ID: task.GetID(),
|
||||
Name: task.GetName(),
|
||||
Creator: creatorName,
|
||||
CreatorRole: creatorRole,
|
||||
State: task.GetState(),
|
||||
Status: task.GetStatus(),
|
||||
Progress: progress,
|
||||
Error: errMsg,
|
||||
}
|
||||
}
|
||||
|
||||
func getTaskInfos[T tache.TaskWithInfo](tasks []T) []TaskInfo {
|
||||
func getTaskInfos[T task.TaskInfoWithCreator](tasks []T) []TaskInfo {
|
||||
return utils.MustSliceConvert(tasks, getTaskInfo[T])
|
||||
}
|
||||
|
||||
func taskRoute[T tache.TaskWithInfo](g *gin.RouterGroup, manager *tache.Manager[T]) {
|
||||
g.GET("/undone", func(c *gin.Context) {
|
||||
common.SuccessResp(c, getTaskInfos(manager.GetByState(tache.StatePending, tache.StateRunning,
|
||||
tache.StateCanceling, tache.StateErrored, tache.StateFailing, tache.StateWaitingRetry, tache.StateBeforeRetry)))
|
||||
})
|
||||
g.GET("/done", func(c *gin.Context) {
|
||||
common.SuccessResp(c, getTaskInfos(manager.GetByState(tache.StateCanceled, tache.StateFailed, tache.StateSucceeded)))
|
||||
})
|
||||
g.POST("/info", func(c *gin.Context) {
|
||||
tid := c.Query("tid")
|
||||
task, ok := manager.GetByID(tid)
|
||||
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.TaskInfoWithCreator](manager *tache.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 taskRoute[T task.TaskInfoWithCreator](g *gin.RouterGroup, manager *tache.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", func(c *gin.Context) {
|
||||
tid := c.Query("tid")
|
||||
manager.Cancel(tid)
|
||||
}))
|
||||
g.POST("/cancel", getTargetedHandler(manager, func(c *gin.Context, task T) {
|
||||
manager.Cancel(task.GetID())
|
||||
common.SuccessResp(c)
|
||||
})
|
||||
g.POST("/delete", func(c *gin.Context) {
|
||||
tid := c.Query("tid")
|
||||
manager.Remove(tid)
|
||||
}))
|
||||
g.POST("/delete", getTargetedHandler(manager, func(c *gin.Context, task T) {
|
||||
manager.Remove(task.GetID())
|
||||
common.SuccessResp(c)
|
||||
})
|
||||
g.POST("/retry", func(c *gin.Context) {
|
||||
tid := c.Query("tid")
|
||||
manager.Retry(tid)
|
||||
}))
|
||||
g.POST("/retry", getTargetedHandler(manager, func(c *gin.Context, task T) {
|
||||
manager.Retry(task.GetID())
|
||||
common.SuccessResp(c)
|
||||
})
|
||||
}))
|
||||
g.POST("/clear_done", func(c *gin.Context) {
|
||||
manager.RemoveByState(tache.StateCanceled, tache.StateFailed, tache.StateSucceeded)
|
||||
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) {
|
||||
manager.RemoveByState(tache.StateSucceeded)
|
||||
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) {
|
||||
manager.RetryAllFailed()
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user