Merge pull request #30 from krau/dev

feat: cancel task
This commit is contained in:
Krau
2025-02-27 22:34:58 +08:00
committed by GitHub
9 changed files with 153 additions and 37 deletions

View File

@@ -153,7 +153,6 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
Status: types.Pending, Status: types.Pending,
File: file, File: file,
StorageName: storageName, StorageName: storageName,
StoragePath: path.Join(),
FileChatID: record.ChatID, FileChatID: record.ChatID,
ReplyMessageID: record.ReplyMessageID, ReplyMessageID: record.ReplyMessageID,
FileMessageID: record.MessageID, FileMessageID: record.MessageID,
@@ -164,7 +163,7 @@ func AddToQueue(ctx *ext.Context, update *ext.Update) error {
task.StoragePath = path.Join(dir.Path, file.FileName) task.StoragePath = path.Join(dir.Path, file.FileName)
} }
queue.AddTask(task) queue.AddTask(&task)
entityBuilder := entity.Builder{} entityBuilder := entity.Builder{}
var entities []tg.MessageEntityClass var entities []tg.MessageEntityClass

27
bot/handle_cancel_task.go Normal file
View File

@@ -0,0 +1,27 @@
package bot
import (
"strings"
"github.com/celestix/gotgproto/dispatcher"
"github.com/celestix/gotgproto/ext"
"github.com/gotd/td/tg"
"github.com/krau/SaveAny-Bot/queue"
)
func cancelTask(ctx *ext.Context, update *ext.Update) error {
key := strings.Split(string(update.CallbackQuery.Data), " ")[1]
ok := queue.CancelTask(key)
if ok {
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
QueryID: update.CallbackQuery.QueryID,
Message: "任务已取消",
})
return dispatcher.EndGroups
}
ctx.AnswerCallback(&tg.MessagesSetBotCallbackAnswerRequest{
QueryID: update.CallbackQuery.QueryID,
Message: "任务取消失败",
})
return dispatcher.EndGroups
}

View File

@@ -22,5 +22,6 @@ func RegisterHandlers(dispatcher dispatcher.Dispatcher) {
dispatcher.AddHandler(handlers.NewMessage(linkRegexFilter, handleLinkMessage)) dispatcher.AddHandler(handlers.NewMessage(linkRegexFilter, handleLinkMessage))
dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("add"), AddToQueue)) dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("add"), AddToQueue))
dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("set_default"), setDefaultStorage)) dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("set_default"), setDefaultStorage))
dispatcher.AddHandler(handlers.NewCallbackQuery(filters.CallbackQuery.Prefix("cancel"), cancelTask))
dispatcher.AddHandler(handlers.NewMessage(filters.Message.Media, handleFileMessage)) dispatcher.AddHandler(handlers.NewMessage(filters.Message.Media, handleFileMessage))
} }

View File

@@ -264,7 +264,7 @@ func HandleSilentAddTask(ctx *ext.Context, update *ext.Update, user *dao.User, t
}) })
return dispatcher.EndGroups return dispatcher.EndGroups
} }
queue.AddTask(*task) queue.AddTask(task)
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{ ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
Message: fmt.Sprintf("已添加到队列: %s\n当前排队任务数: %d", task.FileName(), queue.Len()), Message: fmt.Sprintf("已添加到队列: %s\n当前排队任务数: %d", task.FileName(), queue.Len()),
ID: task.ReplyMessageID, ID: task.ReplyMessageID,

View File

@@ -22,13 +22,12 @@ func worker(queue *queue.TaskQueue, semaphore chan struct{}) {
switch task.Status { switch task.Status {
case types.Pending: case types.Pending:
logger.L.Infof("Processing task: %s", task.String()) logger.L.Infof("Processing task: %s", task.String())
if err := processPendingTask(&task); err != nil { if err := processPendingTask(task); err != nil {
logger.L.Errorf("Failed to do task: %s", err)
task.Error = err task.Error = err
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
logger.L.Debugf("Task canceled: %s", task.String())
task.Status = types.Canceled task.Status = types.Canceled
} else { } else {
logger.L.Errorf("Failed to do task: %s", err)
task.Status = types.Failed task.Status = types.Failed
} }
} else { } else {
@@ -37,23 +36,43 @@ func worker(queue *queue.TaskQueue, semaphore chan struct{}) {
queue.AddTask(task) queue.AddTask(task)
case types.Succeeded: case types.Succeeded:
logger.L.Infof("Task succeeded: %s", task.String()) logger.L.Infof("Task succeeded: %s", task.String())
task.Ctx.(*ext.Context).EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{ extCtx, ok := task.Ctx.(*ext.Context)
Message: fmt.Sprintf("文件保存成功\n [%s]: %s", task.StorageName, task.StoragePath), if !ok {
ID: task.ReplyMessageID, logger.L.Errorf("Context is not *ext.Context: %T", task.Ctx)
}) } else {
extCtx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
Message: fmt.Sprintf("文件保存成功\n [%s]: %s", task.StorageName, task.StoragePath),
ID: task.ReplyMessageID,
})
}
case types.Failed: case types.Failed:
logger.L.Errorf("Task failed: %s", task.String()) logger.L.Errorf("Task failed: %s", task.String())
task.Ctx.(*ext.Context).EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{ extCtx, ok := task.Ctx.(*ext.Context)
Message: "文件保存失败\n" + task.Error.Error(), if !ok {
ID: task.ReplyMessageID, logger.L.Errorf("Context is not *ext.Context: %T", task.Ctx)
}) } else {
extCtx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
Message: "文件保存失败\n" + task.Error.Error(),
ID: task.ReplyMessageID,
})
}
case types.Canceled: case types.Canceled:
logger.L.Infof("Task canceled: %s", task.String()) logger.L.Infof("Task canceled: %s", task.String())
extCtx, ok := task.Ctx.(*ext.Context)
if !ok {
logger.L.Errorf("Context is not *ext.Context: %T", task.Ctx)
} else {
extCtx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
Message: "任务已取消",
ID: task.ReplyMessageID,
})
}
default: default:
logger.L.Errorf("Unknown task status: %s", task.Status) logger.L.Errorf("Unknown task status: %s", task.Status)
} }
<-semaphore <-semaphore
logger.L.Debugf("Task done: %s", task.String()) logger.L.Debugf("Task done: %s; status: %s", task.String(), task.Status)
queue.DoneTask(task)
} }
} }

View File

@@ -1,6 +1,7 @@
package core package core
import ( import (
"context"
"fmt" "fmt"
"path/filepath" "path/filepath"
"time" "time"
@@ -48,11 +49,15 @@ func processPendingTask(task *types.Task) error {
return fmt.Errorf("context is not *ext.Context: %T", task.Ctx) return fmt.Errorf("context is not *ext.Context: %T", task.Ctx)
} }
cancelCtx, cancel := context.WithCancel(ctx)
task.Cancel = cancel
text, entities := buildProgressMessageEntity(task, 0, task.StartTime, 0) text, entities := buildProgressMessageEntity(task, 0, task.StartTime, 0)
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{ ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
Message: text, Message: text,
Entities: entities, Entities: entities,
ID: task.ReplyMessageID, ID: task.ReplyMessageID,
ReplyMarkup: getCancelTaskMarkup(task),
}) })
progressCallback := buildProgressCallback(ctx, task, getProgressUpdateCount(task.File.FileSize)) progressCallback := buildProgressCallback(ctx, task, getProgressUpdateCount(task.File.FileSize))
@@ -63,7 +68,7 @@ func processPendingTask(task *types.Task) error {
defer dest.Close() defer dest.Close()
task.StartTime = time.Now() task.StartTime = time.Now()
downloadBuider := Downloader.Download(bot.Client.API(), task.File.Location).WithThreads(getTaskThreads(task.File.FileSize)) downloadBuider := Downloader.Download(bot.Client.API(), task.File.Location).WithThreads(getTaskThreads(task.File.FileSize))
_, err = downloadBuider.Parallel(ctx, dest) _, err = downloadBuider.Parallel(cancelCtx, dest)
if err != nil { if err != nil {
return fmt.Errorf("下载文件失败: %w", err) return fmt.Errorf("下载文件失败: %w", err)
} }
@@ -78,5 +83,5 @@ func processPendingTask(task *types.Task) error {
ID: task.ReplyMessageID, ID: task.ReplyMessageID,
}) })
return saveFileWithRetry(task, taskStorage, cacheDestPath) return saveFileWithRetry(cancelCtx, task, taskStorage, cacheDestPath)
} }

View File

@@ -1,6 +1,7 @@
package core package core
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"path" "path"
@@ -19,13 +20,21 @@ import (
"github.com/krau/SaveAny-Bot/types" "github.com/krau/SaveAny-Bot/types"
) )
func saveFileWithRetry(task *types.Task, taskStorage storage.Storage, localFilePath string) error { func saveFileWithRetry(ctx context.Context, task *types.Task, taskStorage storage.Storage, localFilePath string) error {
for i := 0; i <= config.Cfg.Retry; i++ { for i := 0; i <= config.Cfg.Retry; i++ {
if err := taskStorage.Save(task.Ctx, localFilePath, task.StoragePath); err != nil { if err := ctx.Err(); err != nil {
return fmt.Errorf("context canceled while saving file: %w", err)
}
if err := taskStorage.Save(ctx, localFilePath, task.StoragePath); err != nil {
if i == config.Cfg.Retry { if i == config.Cfg.Retry {
return fmt.Errorf("failed to save file: %w", err) return fmt.Errorf("failed to save file: %w", err)
} }
logger.L.Errorf("Failed to save file: %s, retrying...", err) logger.L.Errorf("Failed to save file: %s, retrying...", err)
select {
case <-ctx.Done():
return fmt.Errorf("context canceled during retry delay: %w", ctx.Err())
case <-time.After(time.Duration(i*500) * time.Millisecond):
}
continue continue
} }
return nil return nil
@@ -56,7 +65,7 @@ func processPhoto(task *types.Task, taskStorage storage.Storage, cachePath strin
logger.L.Infof("Downloaded file: %s", cachePath) logger.L.Infof("Downloaded file: %s", cachePath)
return saveFileWithRetry(task, taskStorage, cachePath) return saveFileWithRetry(task.Ctx, task, taskStorage, cachePath)
} }
// func getProgressBar(progress float64, updateCount int) string { // func getProgressBar(progress float64, updateCount int) string {
@@ -139,13 +148,20 @@ func buildProgressCallback(ctx *ext.Context, task *types.Task, updateCount int)
} }
text, entities := buildProgressMessageEntity(task, bytesRead, task.StartTime, progress) text, entities := buildProgressMessageEntity(task, bytesRead, task.StartTime, progress)
ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{ ctx.EditMessage(task.ReplyChatID, &tg.MessagesEditMessageRequest{
Message: text, Message: text,
Entities: entities, Entities: entities,
ID: task.ReplyMessageID, ID: task.ReplyMessageID,
ReplyMarkup: getCancelTaskMarkup(task),
}) })
} }
} }
func getCancelTaskMarkup(task *types.Task) *tg.ReplyInlineMarkup {
return &tg.ReplyInlineMarkup{
Rows: []tg.KeyboardButtonRow{{Buttons: []tg.KeyboardButtonClass{&tg.KeyboardButtonCallback{Text: "取消任务", Data: fmt.Appendf(nil, "cancel %s", task.Key())}}}},
}
}
func fixTaskFileExt(task *types.Task, localFilePath string) { func fixTaskFileExt(task *types.Task, localFilePath string) {
if path.Ext(task.FileName()) == "" { if path.Ext(task.FileName()) == "" {
mimeType, err := mimetype.DetectFile(localFilePath) mimeType, err := mimetype.DetectFile(localFilePath)

View File

@@ -8,30 +8,65 @@ import (
) )
type TaskQueue struct { type TaskQueue struct {
list *list.List list *list.List
cond *sync.Cond cond *sync.Cond
mutex *sync.Mutex mutex *sync.Mutex
activeMap map[string]*types.Task
} }
func (q *TaskQueue) AddTask(task types.Task) { func (q *TaskQueue) AddTask(task *types.Task) {
q.mutex.Lock() q.mutex.Lock()
defer q.mutex.Unlock() defer q.mutex.Unlock()
q.list.PushBack(task) q.list.PushBack(task)
q.cond.Signal() q.cond.Signal()
if task.Status != types.Pending {
delete(q.activeMap, task.Key())
}
} }
func (q *TaskQueue) GetTask() types.Task { func (q *TaskQueue) GetTask() *types.Task {
q.mutex.Lock() q.mutex.Lock()
defer q.mutex.Unlock() defer q.mutex.Unlock()
for q.list.Len() == 0 { for q.list.Len() == 0 {
q.cond.Wait() q.cond.Wait()
} }
e := q.list.Front() e := q.list.Front()
task := e.Value.(types.Task) task := e.Value.(*types.Task)
q.list.Remove(e) q.list.Remove(e)
if task.Status == types.Pending {
q.activeMap[task.Key()] = task
}
return task return task
} }
func (q *TaskQueue) DoneTask(task *types.Task) {
q.mutex.Lock()
defer q.mutex.Unlock()
delete(q.activeMap, task.Key())
}
func (q *TaskQueue) CancelTask(key string) bool {
q.mutex.Lock()
defer q.mutex.Unlock()
if task, ok := q.activeMap[key]; ok {
if task.Cancel != nil {
task.Cancel()
return true
}
}
for e := q.list.Front(); e != nil; e = e.Next() {
task := e.Value.(*types.Task)
if task.Key() == key {
if task.Cancel != nil {
task.Cancel()
}
q.list.Remove(e)
return true
}
}
return false
}
func (q *TaskQueue) Len() int { func (q *TaskQueue) Len() int {
q.mutex.Lock() q.mutex.Lock()
defer q.mutex.Unlock() defer q.mutex.Unlock()
@@ -47,20 +82,29 @@ func init() {
func NewQueue() *TaskQueue { func NewQueue() *TaskQueue {
m := &sync.Mutex{} m := &sync.Mutex{}
return &TaskQueue{ return &TaskQueue{
list: list.New(), list: list.New(),
cond: sync.NewCond(m), cond: sync.NewCond(m),
mutex: m, mutex: m,
activeMap: make(map[string]*types.Task),
} }
} }
func AddTask(task types.Task) { func AddTask(task *types.Task) {
Queue.AddTask(task) Queue.AddTask(task)
} }
func GetTask() types.Task { func GetTask() *types.Task {
return Queue.GetTask() return Queue.GetTask()
} }
func Len() int { func Len() int {
return Queue.Len() return Queue.Len()
} }
func CancelTask(key string) bool {
return Queue.CancelTask(key)
}
func DoneTask(task *types.Task) {
Queue.DoneTask(task)
}

View File

@@ -36,6 +36,7 @@ var StorageTypeDisplay = map[StorageType]string{
type Task struct { type Task struct {
Ctx context.Context Ctx context.Context
Cancel context.CancelFunc
Error error Error error
Status TaskStatus Status TaskStatus
File *File File *File
@@ -52,6 +53,10 @@ type Task struct {
UserID int64 UserID int64
} }
func (t Task) Key() string {
return fmt.Sprintf("%d:%d", t.FileChatID, t.FileMessageID)
}
func (t Task) String() string { func (t Task) String() string {
return fmt.Sprintf("[%d:%d]:%s", t.FileChatID, t.FileMessageID, t.File.FileName) return fmt.Sprintf("[%d:%d]:%s", t.FileChatID, t.FileMessageID, t.File.FileName)
} }