feat(core): persist tasks and recover them after restart

This commit is contained in:
krau
2026-08-25 15:05:55 +08:00
parent 54dc4caafe
commit 991f454096
14 changed files with 938 additions and 68 deletions
+85
View File
@@ -0,0 +1,85 @@
package tfile
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"github.com/krau/SaveAny-Bot/config"
"github.com/krau/SaveAny-Bot/core"
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
tfilepkg "github.com/krau/SaveAny-Bot/pkg/tfile"
"github.com/krau/SaveAny-Bot/storage"
)
type taskPayload struct {
ID string `json:"id"`
Storage string `json:"storage"`
Path string `json:"path"`
File tfilepkg.FilePayload `json:"file"`
ChatID int64 `json:"chat_id"`
MessageID int `json:"message_id"`
}
type taskCodec struct{}
func init() {
core.RegisterTaskCodec(tasktype.TaskTypeTgfiles, taskCodec{})
}
func (taskCodec) Marshal(task core.Executable) ([]byte, error) {
t, ok := task.(*Task)
if !ok {
return nil, fmt.Errorf("unexpected task type %T", task)
}
filePayload, ok := tfilepkg.FilePayloadOf(t.File)
if !ok {
return nil, fmt.Errorf("file %T is not serializable", t.File)
}
p := taskPayload{
ID: t.ID,
Storage: t.Storage.Name(),
Path: t.Path,
File: filePayload,
}
if progress, ok := t.Progress.(*Progress); ok {
p.ChatID = progress.ChatID
p.MessageID = progress.MessageID
}
return json.Marshal(p)
}
func (taskCodec) Unmarshal(data []byte) (core.Executable, error) {
var p taskPayload
if err := json.Unmarshal(data, &p); err != nil {
return nil, fmt.Errorf("invalid task payload: %w", err)
}
dler := core.DownloaderClient()
if dler == nil {
return nil, fmt.Errorf("no downloader client available")
}
file := tfilepkg.FileFromPayload(p.File, dler)
stor, err := storage.GetStorageByName(context.Background(), p.Storage)
if err != nil {
return nil, fmt.Errorf("storage %q: %w", p.Storage, err)
}
var progress ProgressTracker
if p.ChatID != 0 {
progress = NewProgressTrack(p.MessageID, p.ChatID)
}
localPath, err := filepath.Abs(filepath.Join(config.C().Temp.BasePath, fmt.Sprintf("%s_%s", p.ID, file.Name())))
if err != nil {
return nil, fmt.Errorf("failed to build cache path: %w", err)
}
return &Task{
ID: p.ID,
Ctx: context.Background(),
File: file,
Storage: stor,
Path: p.Path,
Progress: progress,
stream: false, // recovered tasks always download to cache first
localPath: localPath,
}, nil
}
+12 -23
View File
@@ -9,7 +9,6 @@ import (
"github.com/charmbracelet/log"
"github.com/duke-git/lancet/v2/retry"
"github.com/krau/SaveAny-Bot/common/tdler"
"github.com/krau/SaveAny-Bot/common/utils/fsutil"
"github.com/krau/SaveAny-Bot/common/utils/ioutil"
"github.com/krau/SaveAny-Bot/config"
@@ -19,8 +18,13 @@ import (
"github.com/krau/SaveAny-Bot/storage"
)
func (t *Task) Execute(ctx context.Context) error {
func (t *Task) Execute(ctx context.Context) (err error) {
logger := log.FromContext(ctx).WithPrefix(fmt.Sprintf("file[%s]", t.File.Name()))
defer func() {
if t.Progress != nil {
t.Progress.OnDone(ctx, t, err)
}
}()
if t.Progress != nil {
t.Progress.OnStart(ctx, t)
}
@@ -29,35 +33,16 @@ func (t *Task) Execute(ctx context.Context) error {
}
logger.Info("Starting file download")
localFile, err := fsutil.CreateFile(t.localPath)
if err != nil {
return fmt.Errorf("failed to create local file: %w", err)
}
defer func() {
if err := localFile.CloseAndRemove(); err != nil {
logger.Errorf("Failed to close local file: %v", err)
}
}()
wrAt := newWriterAt(ctx, localFile, t.Progress, t)
defer func() {
if t.Progress != nil {
t.Progress.OnDone(ctx, t, err)
}
}()
_, err = tdler.NewDownloader(t.File).Parallel(ctx, wrAt)
if err != nil {
if err := t.download(ctx); err != nil {
return fmt.Errorf("failed to download file: %w", err)
}
logger.Infof("File downloaded successfully")
if path.Ext(t.File.Name()) == "" {
ext := fsutil.DetectFileExt(t.localPath)
if ext != "" {
t.Path = t.Path + ext
}
}
var fileStat os.FileInfo
fileStat, err = os.Stat(t.localPath)
fileStat, err := os.Stat(t.localPath)
if err != nil {
return fmt.Errorf("failed to get file stat: %w", err)
}
@@ -97,6 +82,10 @@ func (t *Task) Execute(ctx context.Context) error {
if err != nil {
return fmt.Errorf("failed to save file after retries: %w", err)
}
// Cache file is kept on failure so a later restart can resume upload.
if err := os.Remove(t.localPath); err != nil {
logger.Errorf("Failed to remove cache file: %v", err)
}
return nil
}