mirror of
https://github.com/krau/SaveAny-Bot.git
synced 2026-06-28 02:31:34 +08:00
Compare commits
10 Commits
v0.55.2
...
feat/watch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27fe1ebe49 | ||
|
|
88b170acaa | ||
|
|
8059e27978 | ||
|
|
bfab4c85c8 | ||
|
|
62e4a08e28 | ||
|
|
0982abe7bc | ||
|
|
e74839b8e9 | ||
|
|
58ce8275b2 | ||
|
|
8f67f778a3 | ||
|
|
159dba6224 |
@@ -66,6 +66,19 @@ func (f *TaskFactory) CreateTask(req *CreateTaskRequest) (*CreateTaskResponse, e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *TaskFactory) registerAndEnqueueTask(task core.Executable, taskType tasktype.TaskType, storageName, path, webhook string) error {
|
||||||
|
taskID := task.TaskID()
|
||||||
|
RegisterTask(taskID, string(taskType), storageName, path, task.Title(), webhook)
|
||||||
|
|
||||||
|
err := core.AddTask(f.ctx, NewExecutableWrapper(task))
|
||||||
|
if err != nil {
|
||||||
|
DeleteTask(taskID)
|
||||||
|
return fmt.Errorf("failed to add task: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// createDirectLinksTask 创建直链下载任务
|
// createDirectLinksTask 创建直链下载任务
|
||||||
func (f *TaskFactory) createDirectLinksTask(taskID string, createdAt time.Time, req *CreateTaskRequest, stor storage.Storage) (*CreateTaskResponse, error) {
|
func (f *TaskFactory) createDirectLinksTask(taskID string, createdAt time.Time, req *CreateTaskRequest, stor storage.Storage) (*CreateTaskResponse, error) {
|
||||||
var params DirectLinksParams
|
var params DirectLinksParams
|
||||||
@@ -79,8 +92,9 @@ func (f *TaskFactory) createDirectLinksTask(taskID string, createdAt time.Time,
|
|||||||
|
|
||||||
task := directlinks.NewTask(taskID, f.ctx, params.URLs, stor, req.Path, nil)
|
task := directlinks.NewTask(taskID, f.ctx, params.URLs, stor, req.Path, nil)
|
||||||
|
|
||||||
if err := core.AddTask(f.ctx, task); err != nil {
|
err := f.registerAndEnqueueTask(task, tasktype.TaskTypeDirectlinks, req.Storage, req.Path, req.Webhook)
|
||||||
return nil, fmt.Errorf("failed to add task: %w", err)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CreateTaskResponse{
|
return &CreateTaskResponse{
|
||||||
@@ -104,8 +118,9 @@ func (f *TaskFactory) createYTDLPTask(taskID string, createdAt time.Time, req *C
|
|||||||
|
|
||||||
task := ytdlp.NewTask(taskID, f.ctx, params.URLs, params.Flags, stor, req.Path, nil)
|
task := ytdlp.NewTask(taskID, f.ctx, params.URLs, params.Flags, stor, req.Path, nil)
|
||||||
|
|
||||||
if err := core.AddTask(f.ctx, task); err != nil {
|
err := f.registerAndEnqueueTask(task, tasktype.TaskTypeYtdlp, req.Storage, req.Path, req.Webhook)
|
||||||
return nil, fmt.Errorf("failed to add task: %w", err)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CreateTaskResponse{
|
return &CreateTaskResponse{
|
||||||
@@ -146,8 +161,9 @@ func (f *TaskFactory) createAria2Task(taskID string, createdAt time.Time, req *C
|
|||||||
|
|
||||||
task := aria2dl.NewTask(taskID, f.ctx, gid, params.URLs, aria2Client, stor, req.Path, nil)
|
task := aria2dl.NewTask(taskID, f.ctx, gid, params.URLs, aria2Client, stor, req.Path, nil)
|
||||||
|
|
||||||
if err := core.AddTask(f.ctx, task); err != nil {
|
err = f.registerAndEnqueueTask(task, tasktype.TaskTypeAria2, req.Storage, req.Path, req.Webhook)
|
||||||
return nil, fmt.Errorf("failed to add task: %w", err)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CreateTaskResponse{
|
return &CreateTaskResponse{
|
||||||
@@ -190,8 +206,9 @@ func (f *TaskFactory) createParsedTask(taskID string, createdAt time.Time, req *
|
|||||||
|
|
||||||
task := parsed.NewTask(taskID, f.ctx, stor, req.Path, item, nil)
|
task := parsed.NewTask(taskID, f.ctx, stor, req.Path, item, nil)
|
||||||
|
|
||||||
if err := core.AddTask(f.ctx, task); err != nil {
|
err = f.registerAndEnqueueTask(task, tasktype.TaskTypeParseditem, req.Storage, req.Path, req.Webhook)
|
||||||
return nil, fmt.Errorf("failed to add task: %w", err)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CreateTaskResponse{
|
return &CreateTaskResponse{
|
||||||
@@ -223,15 +240,15 @@ func (f *TaskFactory) createTGFilesTask(taskID string, createdAt time.Time, req
|
|||||||
return nil, fmt.Errorf("no files found in provided links")
|
return nil, fmt.Errorf("no files found in provided links")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var task core.Executable
|
||||||
|
|
||||||
if len(files) == 1 {
|
if len(files) == 1 {
|
||||||
// 单个文件任务
|
// 单个文件任务
|
||||||
tfileTask, err := tfile.NewTGFileTask(taskID, f.ctx, files[0], stor, req.Path, nil)
|
tfileTask, err := tfile.NewTGFileTask(taskID, f.ctx, files[0], stor, req.Path, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create tfile task: %w", err)
|
return nil, fmt.Errorf("failed to create tfile task: %w", err)
|
||||||
}
|
}
|
||||||
if err := core.AddTask(f.ctx, tfileTask); err != nil {
|
task = tfileTask
|
||||||
return nil, fmt.Errorf("failed to add task: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 批量文件任务
|
// 批量文件任务
|
||||||
elems := make([]batchtfile.TaskElement, 0, len(files))
|
elems := make([]batchtfile.TaskElement, 0, len(files))
|
||||||
@@ -243,10 +260,12 @@ func (f *TaskFactory) createTGFilesTask(taskID string, createdAt time.Time, req
|
|||||||
elems = append(elems, *elem)
|
elems = append(elems, *elem)
|
||||||
}
|
}
|
||||||
|
|
||||||
task := batchtfile.NewBatchTGFileTask(taskID, f.ctx, elems, nil, true)
|
task = batchtfile.NewBatchTGFileTask(taskID, f.ctx, elems, nil, true)
|
||||||
if err := core.AddTask(f.ctx, task); err != nil {
|
}
|
||||||
return nil, fmt.Errorf("failed to add task: %w", err)
|
|
||||||
}
|
err = f.registerAndEnqueueTask(task, tasktype.TaskTypeTgfiles, req.Storage, req.Path, req.Webhook)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CreateTaskResponse{
|
return &CreateTaskResponse{
|
||||||
@@ -281,8 +300,9 @@ func (f *TaskFactory) createTPHPicsTask(taskID string, createdAt time.Time, req
|
|||||||
client := telegraph.NewClient()
|
client := telegraph.NewClient()
|
||||||
task := tphtask.NewTask(taskID, f.ctx, phPath, pics, stor, req.Path, client, nil)
|
task := tphtask.NewTask(taskID, f.ctx, phPath, pics, stor, req.Path, client, nil)
|
||||||
|
|
||||||
if err := core.AddTask(f.ctx, task); err != nil {
|
err = f.registerAndEnqueueTask(task, tasktype.TaskTypeTphpics, req.Storage, req.Path, req.Webhook)
|
||||||
return nil, fmt.Errorf("failed to add task: %w", err)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CreateTaskResponse{
|
return &CreateTaskResponse{
|
||||||
@@ -342,8 +362,9 @@ func (f *TaskFactory) createTransferTask(taskID string, createdAt time.Time, req
|
|||||||
|
|
||||||
task := transfer.NewTransferTask(taskID, f.ctx, elems, nil, true)
|
task := transfer.NewTransferTask(taskID, f.ctx, elems, nil, true)
|
||||||
|
|
||||||
if err := core.AddTask(f.ctx, task); err != nil {
|
err = f.registerAndEnqueueTask(task, tasktype.TaskTypeTransfer, params.TargetStorage, params.TargetPath, req.Webhook)
|
||||||
return nil, fmt.Errorf("failed to add task: %w", err)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CreateTaskResponse{
|
return &CreateTaskResponse{
|
||||||
|
|||||||
@@ -30,16 +30,21 @@ func NewServer(ctx context.Context) *Server {
|
|||||||
mux.HandleFunc("/health", handlers.HealthCheckHandler)
|
mux.HandleFunc("/health", handlers.HealthCheckHandler)
|
||||||
|
|
||||||
// API v1 路由
|
// API v1 路由
|
||||||
mux.HandleFunc("/api/v1/tasks", handlers.CreateTaskHandler)
|
mux.HandleFunc("/api/v1/tasks", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
handlers.ListTasksHandler(w, r)
|
||||||
|
case http.MethodPost:
|
||||||
|
handlers.CreateTaskHandler(w, r)
|
||||||
|
default:
|
||||||
|
MethodNotAllowedHandler(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
mux.HandleFunc("/api/v1/tasks/", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/v1/tasks/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
// 根据方法和路径分发
|
// 根据方法和路径分发
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
if r.URL.Path == "/api/v1/tasks" {
|
handlers.GetTaskHandler(w, r)
|
||||||
handlers.ListTasksHandler(w, r)
|
|
||||||
} else {
|
|
||||||
handlers.GetTaskHandler(w, r)
|
|
||||||
}
|
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
handlers.CancelTaskHandler(w, r)
|
handlers.CancelTaskHandler(w, r)
|
||||||
default:
|
default:
|
||||||
|
|||||||
58
api/wrapper.go
Normal file
58
api/wrapper.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/core"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExecutableWrapper wraps core.Executable to track task status in the API store and send webhooks.
|
||||||
|
type ExecutableWrapper struct {
|
||||||
|
inner core.Executable
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExecutableWrapper(inner core.Executable) *ExecutableWrapper {
|
||||||
|
return &ExecutableWrapper{inner: inner}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ExecutableWrapper) Type() tasktype.TaskType { return w.inner.Type() }
|
||||||
|
func (w *ExecutableWrapper) Title() string { return w.inner.Title() }
|
||||||
|
func (w *ExecutableWrapper) TaskID() string { return w.inner.TaskID() }
|
||||||
|
|
||||||
|
func (w *ExecutableWrapper) Execute(ctx context.Context) error {
|
||||||
|
taskID := w.inner.TaskID()
|
||||||
|
|
||||||
|
if info, ok := GetTask(taskID); ok {
|
||||||
|
info.UpdateStatus(TaskStatusRunning)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := w.inner.Execute(ctx)
|
||||||
|
|
||||||
|
info, ok := GetTask(taskID)
|
||||||
|
if !ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var status TaskStatus
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
status = TaskStatusCancelled
|
||||||
|
info.UpdateStatus(TaskStatusCancelled)
|
||||||
|
} else {
|
||||||
|
status = TaskStatusFailed
|
||||||
|
info.SetError(err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
status = TaskStatusCompleted
|
||||||
|
info.UpdateStatus(TaskStatusCompleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Webhook != "" {
|
||||||
|
payload := CreateWebhookPayload(taskID, info.Type, status, info.Storage, info.Path, err)
|
||||||
|
SendWebhook(ctx, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -73,14 +73,16 @@ func handleAddCallback(ctx *ext.Context, update *ext.Update) error {
|
|||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
dirPath = dir.Path
|
dirPath = dir.Path
|
||||||
|
} else if data.SelectedDirPath != "" {
|
||||||
|
dirPath = data.SelectedDirPath
|
||||||
}
|
}
|
||||||
|
|
||||||
switch data.TaskType {
|
switch data.TaskType {
|
||||||
case tasktype.TaskTypeTgfiles:
|
case tasktype.TaskTypeTgfiles:
|
||||||
if data.AsBatch {
|
if data.AsBatch {
|
||||||
return shortcut.CreateAndAddBatchTGFileTaskWithEdit(ctx, userID, selectedStorage, dirPath, data.Files, msgID)
|
return shortcut.CreateAndAddBatchTGFileTaskWithEdit(ctx, userID, selectedStorage, dirPath, data.Files, msgID, data.ConflictStrategy)
|
||||||
}
|
}
|
||||||
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, userID, selectedStorage, dirPath, data.Files[0], msgID)
|
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, userID, selectedStorage, dirPath, data.Files[0], msgID, data.ConflictStrategy)
|
||||||
case tasktype.TaskTypeTphpics:
|
case tasktype.TaskTypeTphpics:
|
||||||
return shortcut.CreateAndAddtelegraphWithEdit(ctx, userID, data.TphPageNode, data.TphDirPath, data.TphPics, selectedStorage, msgID)
|
return shortcut.CreateAndAddtelegraphWithEdit(ctx, userID, data.TphPageNode, data.TphDirPath, data.TphPics, selectedStorage, msgID)
|
||||||
case tasktype.TaskTypeParseditem:
|
case tasktype.TaskTypeParseditem:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/celestix/gotgproto/dispatcher"
|
"github.com/celestix/gotgproto/dispatcher"
|
||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/conflictutil"
|
||||||
"github.com/krau/SaveAny-Bot/common/i18n"
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
@@ -26,6 +27,10 @@ func handleConfigCmd(ctx *ext.Context, update *ext.Update) error {
|
|||||||
Text: i18n.T(i18nk.BotMsgConfigButtonFilenameStrategy),
|
Text: i18n.T(i18nk.BotMsgConfigButtonFilenameStrategy),
|
||||||
Data: fmt.Appendf(nil, "%s %s", tcbdata.TypeConfig, "fnamest"),
|
Data: fmt.Appendf(nil, "%s %s", tcbdata.TypeConfig, "fnamest"),
|
||||||
},
|
},
|
||||||
|
&tg.KeyboardButtonCallback{
|
||||||
|
Text: i18n.T(i18nk.BotMsgConfigButtonConflictStrategy),
|
||||||
|
Data: fmt.Appendf(nil, "%s %s", tcbdata.TypeConfig, "conflictst"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -51,6 +56,8 @@ func handleConfigCallback(ctx *ext.Context, update *ext.Update) error {
|
|||||||
switch args[1] {
|
switch args[1] {
|
||||||
case "fnamest":
|
case "fnamest":
|
||||||
return handleConfigFnameSTCallback(ctx, update)
|
return handleConfigFnameSTCallback(ctx, update)
|
||||||
|
case "conflictst":
|
||||||
|
return handleConfigConflictSTCallback(ctx, update)
|
||||||
default:
|
default:
|
||||||
return invaildDataAnswer()
|
return invaildDataAnswer()
|
||||||
}
|
}
|
||||||
@@ -110,6 +117,55 @@ func handleConfigFnameSTCallback(ctx *ext.Context, update *ext.Update) error {
|
|||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleConfigConflictSTCallback(ctx *ext.Context, update *ext.Update) error {
|
||||||
|
userID := update.CallbackQuery.GetUserID()
|
||||||
|
user, err := database.GetUserByChatID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
args := strings.Fields(string(update.CallbackQuery.Data))
|
||||||
|
if len(args) == 3 {
|
||||||
|
selected := args[2]
|
||||||
|
if !tcbdata.IsConflictStrategy(selected) {
|
||||||
|
return fmt.Errorf("invalid conflict strategy: %s", selected)
|
||||||
|
}
|
||||||
|
user.ConflictStrategy = selected
|
||||||
|
if err := database.UpdateUser(ctx, user); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
|
ID: update.CallbackQuery.GetMsgID(),
|
||||||
|
Message: i18n.T(i18nk.BotMsgConfigInfoConflictStrategySet, map[string]any{
|
||||||
|
"Strategy": conflictutil.Display(selected),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := tcbdata.ConflictStrategyValues()
|
||||||
|
rows := make([]tg.KeyboardButtonRow, 0, len(opts))
|
||||||
|
for _, opt := range opts {
|
||||||
|
rows = append(rows, tg.KeyboardButtonRow{
|
||||||
|
Buttons: []tg.KeyboardButtonClass{
|
||||||
|
&tg.KeyboardButtonCallback{
|
||||||
|
Text: conflictutil.Display(opt),
|
||||||
|
Data: fmt.Appendf(nil, "%s %s %s", tcbdata.TypeConfig, "conflictst", opt),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
markup := &tg.ReplyInlineMarkup{Rows: rows}
|
||||||
|
currentSt := conflictutil.EffectiveStrategy(user)
|
||||||
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
|
ID: update.CallbackQuery.GetMsgID(),
|
||||||
|
Message: i18n.T(i18nk.BotMsgConfigPromptSelectConflictStrategy, map[string]any{
|
||||||
|
"Strategy": conflictutil.Display(currentSt),
|
||||||
|
}),
|
||||||
|
ReplyMarkup: markup,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
func handleConfigFnameTmpl(ctx *ext.Context, update *ext.Update) error {
|
func handleConfigFnameTmpl(ctx *ext.Context, update *ext.Update) error {
|
||||||
userID := update.GetUserChat().GetID()
|
userID := update.GetUserChat().GetID()
|
||||||
user, err := database.GetUserByChatID(ctx, userID)
|
user, err := database.GetUserByChatID(ctx, userID)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/shortcut"
|
||||||
"github.com/krau/SaveAny-Bot/common/i18n"
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
||||||
@@ -53,9 +53,13 @@ func handleGroupMediaMessage(ctx *ext.Context, update *ext.Update, message *tg.M
|
|||||||
if !supported {
|
if !supported {
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
file, err := tfile.FromMediaMessage(media, ctx.Raw, message, tfile.WithNameIfEmpty(
|
userId := update.GetUserChat().GetID()
|
||||||
tgutil.GenFileNameFromMessage(*message),
|
userDB, err := database.GetUserByChatID(ctx, userId)
|
||||||
))
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tfOpts := mediautil.TfileOptions(ctx, userDB, message)
|
||||||
|
file, err := tfile.FromMediaMessage(media, ctx.Raw, message, tfOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to get file from media: %s", err)
|
logger.Errorf("Failed to get file from media: %s", err)
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
|
|||||||
55
client/bot/handlers/utils/conflictutil/conflict.go
Normal file
55
client/bot/handlers/utils/conflictutil/conflict.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package conflictutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
|
||||||
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxConflictLines = 10
|
||||||
|
|
||||||
|
func EffectiveStrategy(user *database.User) string {
|
||||||
|
if user != nil && tcbdata.IsConflictStrategy(user.ConflictStrategy) {
|
||||||
|
return user.ConflictStrategy
|
||||||
|
}
|
||||||
|
return tcbdata.ConflictStrategyRename
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveStrategy(user *database.User, override string) string {
|
||||||
|
if tcbdata.IsConflictStrategy(override) {
|
||||||
|
return override
|
||||||
|
}
|
||||||
|
return EffectiveStrategy(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Display(strategy string) string {
|
||||||
|
switch strategy {
|
||||||
|
case tcbdata.ConflictStrategyRename:
|
||||||
|
return i18n.T(i18nk.BotMsgConfigConflictStrategyRename, nil)
|
||||||
|
case tcbdata.ConflictStrategyAsk:
|
||||||
|
return i18n.T(i18nk.BotMsgConfigConflictStrategyAsk, nil)
|
||||||
|
case tcbdata.ConflictStrategyOverwrite:
|
||||||
|
return i18n.T(i18nk.BotMsgConfigConflictStrategyOverwrite, nil)
|
||||||
|
case tcbdata.ConflictStrategySkip:
|
||||||
|
return i18n.T(i18nk.BotMsgConfigConflictStrategySkip, nil)
|
||||||
|
default:
|
||||||
|
return strategy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatPaths(conflicts []string) string {
|
||||||
|
if len(conflicts) <= maxConflictLines {
|
||||||
|
return strings.Join(conflicts, "\n")
|
||||||
|
}
|
||||||
|
return strings.Join(conflicts[:maxConflictLines], "\n") + "\n" + i18n.T(i18nk.BotMsgCommonPromptConflictMoreFiles, map[string]any{
|
||||||
|
"Count": len(conflicts) - maxConflictLines,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatPath(storageName, storagePath string) string {
|
||||||
|
return fmt.Sprintf("[%s]:%s", storageName, storagePath)
|
||||||
|
}
|
||||||
@@ -38,6 +38,8 @@ func BuildAddSelectStorageKeyboard(stors []storage.Storage, adddata tcbdata.Add)
|
|||||||
data := tcbdata.Add{
|
data := tcbdata.Add{
|
||||||
TaskType: taskType,
|
TaskType: taskType,
|
||||||
SelectedStorName: storage.Name(),
|
SelectedStorName: storage.Name(),
|
||||||
|
SelectedDirPath: adddata.SelectedDirPath,
|
||||||
|
ConflictStrategy: adddata.ConflictStrategy,
|
||||||
|
|
||||||
Files: adddata.Files,
|
Files: adddata.Files,
|
||||||
AsBatch: len(adddata.Files) > 1,
|
AsBatch: len(adddata.Files) > 1,
|
||||||
@@ -109,6 +111,38 @@ func BuildAddOneSelectStorageMessage(ctx context.Context, stors []storage.Storag
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BuildConflictStrategyMarkup(adddata tcbdata.Add) (*tg.ReplyInlineMarkup, error) {
|
||||||
|
type option struct {
|
||||||
|
text string
|
||||||
|
strategy string
|
||||||
|
}
|
||||||
|
options := []option{
|
||||||
|
{text: i18n.T(i18nk.BotMsgCommonButtonConflictRename, nil), strategy: tcbdata.ConflictStrategyRename},
|
||||||
|
{text: i18n.T(i18nk.BotMsgCommonButtonConflictOverwrite, nil), strategy: tcbdata.ConflictStrategyOverwrite},
|
||||||
|
{text: i18n.T(i18nk.BotMsgCommonButtonConflictSkip, nil), strategy: tcbdata.ConflictStrategySkip},
|
||||||
|
}
|
||||||
|
buttons := make([]tg.KeyboardButtonClass, 0, len(options))
|
||||||
|
for _, opt := range options {
|
||||||
|
data := adddata
|
||||||
|
data.ConflictStrategy = opt.strategy
|
||||||
|
dataid := xid.New().String()
|
||||||
|
if err := cache.Set(dataid, data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
buttons = append(buttons, &tg.KeyboardButtonCallback{
|
||||||
|
Text: opt.text,
|
||||||
|
Data: fmt.Appendf(nil, "%s %s", tcbdata.TypeAdd, dataid),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
rows := make([]tg.KeyboardButtonRow, 0, len(buttons))
|
||||||
|
for _, button := range buttons {
|
||||||
|
rows = append(rows, tg.KeyboardButtonRow{
|
||||||
|
Buttons: []tg.KeyboardButtonClass{button},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return &tg.ReplyInlineMarkup{Rows: rows}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Builds the inline keyboard for setting default storage
|
// Builds the inline keyboard for setting default storage
|
||||||
func BuildSetDefaultStorageMarkup(
|
func BuildSetDefaultStorageMarkup(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/celestix/gotgproto/ext"
|
"github.com/celestix/gotgproto/ext"
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/gotd/td/tg"
|
"github.com/gotd/td/tg"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/conflictutil"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
|
||||||
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/ruleutil"
|
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/ruleutil"
|
||||||
"github.com/krau/SaveAny-Bot/common/i18n"
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
@@ -17,14 +18,17 @@ import (
|
|||||||
"github.com/krau/SaveAny-Bot/core/tasks/batchtfile"
|
"github.com/krau/SaveAny-Bot/core/tasks/batchtfile"
|
||||||
tftask "github.com/krau/SaveAny-Bot/core/tasks/tfile"
|
tftask "github.com/krau/SaveAny-Bot/core/tasks/tfile"
|
||||||
"github.com/krau/SaveAny-Bot/database"
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
"github.com/krau/SaveAny-Bot/pkg/tfile"
|
||||||
"github.com/krau/SaveAny-Bot/storage"
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 创建一个 tfile.TGFileTask 并添加到任务队列中, 以编辑消息的方式反馈结果
|
// 创建一个 tfile.TGFileTask 并添加到任务队列中, 以编辑消息的方式反馈结果
|
||||||
func CreateAndAddTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor storage.Storage, dirPath string, file tfile.TGFileMessage, trackMsgID int) error {
|
func CreateAndAddTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor storage.Storage, dirPath string, file tfile.TGFileMessage, trackMsgID int, conflictStrategy ...string) error {
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
|
strategy := selectedConflictStrategy(conflictStrategy)
|
||||||
user, err := database.GetUserByChatID(ctx, userID)
|
user, err := database.GetUserByChatID(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to get user by chat ID: %s", err)
|
logger.Errorf("Failed to get user by chat ID: %s", err)
|
||||||
@@ -36,6 +40,7 @@ func CreateAndAddTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor storage
|
|||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
strategy = conflictutil.ResolveStrategy(user, strategy)
|
||||||
if user.ApplyRule && user.Rules != nil {
|
if user.ApplyRule && user.Rules != nil {
|
||||||
matched, matchedStorageName, matchedDirPath := ruleutil.ApplyRule(ctx, user.Rules, ruleutil.NewInput(file))
|
matched, matchedStorageName, matchedDirPath := ruleutil.ApplyRule(ctx, user.Rules, ruleutil.NewInput(file))
|
||||||
if !matched {
|
if !matched {
|
||||||
@@ -60,7 +65,26 @@ func CreateAndAddTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor storage
|
|||||||
}
|
}
|
||||||
startCreateTask:
|
startCreateTask:
|
||||||
storagePath := path.Join(dirPath, file.Name())
|
storagePath := path.Join(dirPath, file.Name())
|
||||||
|
if strategy == tcbdata.ConflictStrategyAsk || strategy == tcbdata.ConflictStrategySkip {
|
||||||
|
exists := stor.Exists(ctx, storagePath)
|
||||||
|
if exists && strategy == tcbdata.ConflictStrategyAsk {
|
||||||
|
return promptTGFileConflictStrategy(ctx, userID, stor.Name(), dirPath, []tfile.TGFileMessage{file}, false, []string{conflictutil.FormatPath(stor.Name(), storagePath)}, trackMsgID)
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
|
ID: trackMsgID,
|
||||||
|
Message: i18n.T(i18nk.BotMsgCommonInfoAllConflictFilesSkipped, map[string]any{
|
||||||
|
"Skipped": file.Name(),
|
||||||
|
}),
|
||||||
|
ReplyMarkup: nil,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
}
|
||||||
injectCtx := tgutil.ExtWithContext(ctx.Context, ctx)
|
injectCtx := tgutil.ExtWithContext(ctx.Context, ctx)
|
||||||
|
if strategy == tcbdata.ConflictStrategyOverwrite {
|
||||||
|
injectCtx = storage.WithOverwrite(injectCtx)
|
||||||
|
}
|
||||||
taskid := xid.New().String()
|
taskid := xid.New().String()
|
||||||
task, err := tftask.NewTGFileTask(taskid, injectCtx, file, stor, storagePath,
|
task, err := tftask.NewTGFileTask(taskid, injectCtx, file, stor, storagePath,
|
||||||
tftask.NewProgressTrack(
|
tftask.NewProgressTrack(
|
||||||
@@ -97,8 +121,9 @@ startCreateTask:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建一个 batchtfile.BatchTGFileTask 并添加到任务队列中, 以编辑消息的方式反馈结果
|
// 创建一个 batchtfile.BatchTGFileTask 并添加到任务队列中, 以编辑消息的方式反馈结果
|
||||||
func CreateAndAddBatchTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor storage.Storage, dirPath string, files []tfile.TGFileMessage, trackMsgID int) error {
|
func CreateAndAddBatchTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor storage.Storage, dirPath string, files []tfile.TGFileMessage, trackMsgID int, conflictStrategy ...string) error {
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
|
strategy := selectedConflictStrategy(conflictStrategy)
|
||||||
user, err := database.GetUserByChatID(ctx, userID)
|
user, err := database.GetUserByChatID(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to get user by chat ID: %s", err)
|
logger.Errorf("Failed to get user by chat ID: %s", err)
|
||||||
@@ -110,6 +135,7 @@ func CreateAndAddBatchTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor st
|
|||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
strategy = conflictutil.ResolveStrategy(user, strategy)
|
||||||
|
|
||||||
useRule := user.ApplyRule && user.Rules != nil
|
useRule := user.ApplyRule && user.Rules != nil
|
||||||
|
|
||||||
@@ -128,14 +154,17 @@ func CreateAndAddBatchTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor st
|
|||||||
return storname, dirP
|
return storname, dirP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
skipped := make([]string, 0)
|
||||||
|
conflicts := make([]string, 0)
|
||||||
elems := make([]batchtfile.TaskElement, 0, len(files))
|
elems := make([]batchtfile.TaskElement, 0, len(files))
|
||||||
type albumFile struct {
|
type albumFile struct {
|
||||||
file tfile.TGFileMessage
|
file tfile.TGFileMessage
|
||||||
storage storage.Storage
|
storage storage.Storage
|
||||||
|
dirPath string
|
||||||
}
|
}
|
||||||
albumFiles := make(map[int64][]albumFile, 0)
|
albumFiles := make(map[int64][]albumFile, 0)
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
storName, dirPath := applyRule(file)
|
storName, matchedDirPath := applyRule(file)
|
||||||
fileStor := stor
|
fileStor := stor
|
||||||
if storName != stor.Name() && storName != "" {
|
if storName != stor.Name() && storName != "" {
|
||||||
fileStor, err = storage.GetStorageByUserIDAndName(ctx, user.ChatID, storName)
|
fileStor, err = storage.GetStorageByUserIDAndName(ctx, user.ChatID, storName)
|
||||||
@@ -150,8 +179,19 @@ func CreateAndAddBatchTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor st
|
|||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !dirPath.NeedNewForAlbum() {
|
if !matchedDirPath.NeedNewForAlbum() {
|
||||||
storPath := path.Join(dirPath.String(), file.Name())
|
storPath := path.Join(matchedDirPath.String(), file.Name())
|
||||||
|
if strategy == tcbdata.ConflictStrategyAsk || strategy == tcbdata.ConflictStrategySkip {
|
||||||
|
exists := fileStor.Exists(ctx, storPath)
|
||||||
|
if exists && strategy == tcbdata.ConflictStrategyAsk {
|
||||||
|
conflicts = append(conflicts, conflictutil.FormatPath(fileStor.Name(), storPath))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
skipped = append(skipped, file.Name())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
elem, err := batchtfile.NewTaskElement(fileStor, storPath, file)
|
elem, err := batchtfile.NewTaskElement(fileStor, storPath, file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to create task element: %s", err)
|
logger.Errorf("Failed to create task element: %s", err)
|
||||||
@@ -170,12 +210,17 @@ func CreateAndAddBatchTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor st
|
|||||||
logger.Warnf("File %s is not in a group, skipping album handling", file.Name())
|
logger.Warnf("File %s is not in a group, skipping album handling", file.Name())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
fileDirPath := matchedDirPath.String()
|
||||||
|
if matchedDirPath.NeedNewForAlbum() {
|
||||||
|
fileDirPath = dirPath
|
||||||
|
}
|
||||||
if _, ok := albumFiles[groupId]; !ok {
|
if _, ok := albumFiles[groupId]; !ok {
|
||||||
albumFiles[groupId] = make([]albumFile, 0)
|
albumFiles[groupId] = make([]albumFile, 0)
|
||||||
}
|
}
|
||||||
albumFiles[groupId] = append(albumFiles[groupId], albumFile{
|
albumFiles[groupId] = append(albumFiles[groupId], albumFile{
|
||||||
file: file,
|
file: file,
|
||||||
storage: fileStor,
|
storage: fileStor,
|
||||||
|
dirPath: fileDirPath,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,7 +233,18 @@ func CreateAndAddBatchTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor st
|
|||||||
albumDir := strings.TrimSuffix(path.Base(afiles[0].file.Name()), path.Ext(afiles[0].file.Name()))
|
albumDir := strings.TrimSuffix(path.Base(afiles[0].file.Name()), path.Ext(afiles[0].file.Name()))
|
||||||
albumStor := afiles[0].storage
|
albumStor := afiles[0].storage
|
||||||
for _, af := range afiles {
|
for _, af := range afiles {
|
||||||
afstorPath := path.Join(dirPath, albumDir, af.file.Name())
|
afstorPath := path.Join(af.dirPath, albumDir, af.file.Name())
|
||||||
|
if strategy == tcbdata.ConflictStrategyAsk || strategy == tcbdata.ConflictStrategySkip {
|
||||||
|
exists := albumStor.Exists(ctx, afstorPath)
|
||||||
|
if exists && strategy == tcbdata.ConflictStrategyAsk {
|
||||||
|
conflicts = append(conflicts, conflictutil.FormatPath(albumStor.Name(), afstorPath))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
skipped = append(skipped, af.file.Name())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
elem, err := batchtfile.NewTaskElement(albumStor, afstorPath, af.file)
|
elem, err := batchtfile.NewTaskElement(albumStor, afstorPath, af.file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Failed to create task element for album file: %s", err)
|
logger.Errorf("Failed to create task element for album file: %s", err)
|
||||||
@@ -204,9 +260,26 @@ func CreateAndAddBatchTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strategy == tcbdata.ConflictStrategyAsk && len(conflicts) > 0 {
|
||||||
|
return promptTGFileConflictStrategy(ctx, userID, stor.Name(), dirPath, files, true, conflicts, trackMsgID)
|
||||||
|
}
|
||||||
|
|
||||||
injectCtx := tgutil.ExtWithContext(ctx.Context, ctx)
|
injectCtx := tgutil.ExtWithContext(ctx.Context, ctx)
|
||||||
|
if strategy == tcbdata.ConflictStrategyOverwrite {
|
||||||
|
injectCtx = storage.WithOverwrite(injectCtx)
|
||||||
|
}
|
||||||
|
if len(elems) == 0 {
|
||||||
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
|
ID: trackMsgID,
|
||||||
|
Message: i18n.T(i18nk.BotMsgCommonInfoAllConflictFilesSkipped, map[string]any{
|
||||||
|
"Skipped": strings.Join(skipped, "\n"),
|
||||||
|
}),
|
||||||
|
ReplyMarkup: nil,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
taskid := xid.New().String()
|
taskid := xid.New().String()
|
||||||
task := batchtfile.NewBatchTGFileTask(taskid, injectCtx, elems, batchtfile.NewProgressTracker(trackMsgID, userID), true)
|
task := batchtfile.NewBatchTGFileTask(taskid, injectCtx, elems, batchtfile.NewProgressTrackerWithSkipped(trackMsgID, userID, skipped), true)
|
||||||
if err := core.AddTask(injectCtx, task); err != nil {
|
if err := core.AddTask(injectCtx, task); err != nil {
|
||||||
logger.Errorf("Failed to add batch task: %s", err)
|
logger.Errorf("Failed to add batch task: %s", err)
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
@@ -218,11 +291,48 @@ func CreateAndAddBatchTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor st
|
|||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
ID: trackMsgID,
|
ID: trackMsgID,
|
||||||
Message: i18n.T(i18nk.BotMsgCommonInfoBatchTasksAdded, map[string]any{
|
Message: buildBatchAddedMessage(len(elems), skipped),
|
||||||
"Count": len(files),
|
|
||||||
}),
|
|
||||||
ReplyMarkup: nil,
|
ReplyMarkup: nil,
|
||||||
})
|
})
|
||||||
return dispatcher.EndGroups
|
return dispatcher.EndGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func promptTGFileConflictStrategy(ctx *ext.Context, userID int64, storageName, dirPath string, files []tfile.TGFileMessage, asBatch bool, conflicts []string, trackMsgID int) error {
|
||||||
|
markup, err := msgelem.BuildConflictStrategyMarkup(tcbdata.Add{
|
||||||
|
TaskType: tasktype.TaskTypeTgfiles,
|
||||||
|
SelectedStorName: storageName,
|
||||||
|
SettedDir: true,
|
||||||
|
SelectedDirPath: dirPath,
|
||||||
|
Files: files,
|
||||||
|
AsBatch: asBatch,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
|
||||||
|
ID: trackMsgID,
|
||||||
|
Message: i18n.T(i18nk.BotMsgCommonPromptSelectConflictStrategy, map[string]any{"Files": conflictutil.FormatPaths(conflicts)}),
|
||||||
|
ReplyMarkup: markup,
|
||||||
|
})
|
||||||
|
return dispatcher.EndGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectedConflictStrategy(strategies []string) string {
|
||||||
|
if len(strategies) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strategies[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildBatchAddedMessage(count int, skipped []string) string {
|
||||||
|
if len(skipped) == 0 {
|
||||||
|
return i18n.T(i18nk.BotMsgCommonInfoBatchTasksAdded, map[string]any{
|
||||||
|
"Count": count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return i18n.T(i18nk.BotMsgCommonInfoBatchTasksAddedWithSkipped, map[string]any{
|
||||||
|
"Count": count,
|
||||||
|
"Skipped": strings.Join(skipped, "\n"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
// https://github.com/iyear/tdl/blob/master/core/tclient/tclient.go
|
// https://github.com/iyear/tdl/blob/master/core/tclient/tclient.go
|
||||||
func NewDefaultMiddlewares(ctx context.Context, timeout time.Duration) []telegram.Middleware {
|
func NewDefaultMiddlewares(ctx context.Context, timeout time.Duration) []telegram.Middleware {
|
||||||
return []telegram.Middleware{
|
return []telegram.Middleware{
|
||||||
recovery.New(ctx, newBackoff(timeout)),
|
recovery.New(ctx, func() backoff.BackOff { return newBackoff(timeout) }),
|
||||||
retry.New(config.C().Telegram.RpcRetry),
|
retry.New(config.C().Telegram.RpcRetry),
|
||||||
floodwait.NewSimpleWaiter(),
|
floodwait.NewSimpleWaiter(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,19 +14,28 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type recovery struct {
|
type recovery struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
backoff backoff.BackOff
|
newBackoff func() backoff.BackOff
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, backoff backoff.BackOff) telegram.Middleware {
|
// New returns a recovery middleware.
|
||||||
|
//
|
||||||
|
// newBackoff is a factory that must return a fresh backoff.BackOff on every call: backoff implementations in
|
||||||
|
// cenkalti/backoff/v4 (notably ExponentialBackOff) are not safe for concurrent
|
||||||
|
// use, and the Telegram client invokes RPCs from many goroutines in parallel.
|
||||||
|
//
|
||||||
|
// Sharing a single instance corrupts its internal counters, breaks the
|
||||||
|
// exponential interval, and defeats MaxElapsedTime - see issue #218.
|
||||||
|
func New(ctx context.Context, newBackoff func() backoff.BackOff) telegram.Middleware {
|
||||||
return &recovery{
|
return &recovery{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
backoff: backoff,
|
newBackoff: newBackoff,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *recovery) Handle(next tg.Invoker) telegram.InvokeFunc {
|
func (r *recovery) Handle(next tg.Invoker) telegram.InvokeFunc {
|
||||||
return func(ctx context.Context, input bin.Encoder, output bin.Decoder) error {
|
return func(ctx context.Context, input bin.Encoder, output bin.Decoder) error {
|
||||||
|
b := r.newBackoff()
|
||||||
|
|
||||||
return backoff.RetryNotify(func() error {
|
return backoff.RetryNotify(func() error {
|
||||||
if err := next.Invoke(ctx, input, output); err != nil {
|
if err := next.Invoke(ctx, input, output); err != nil {
|
||||||
@@ -38,7 +47,7 @@ func (r *recovery) Handle(next tg.Invoker) telegram.InvokeFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}, r.backoff, func(err error, duration time.Duration) {
|
}, b, func(err error, duration time.Duration) {
|
||||||
log.FromContext(ctx).Debug("Wait for connection recovery", "error", err, "duration", duration)
|
log.FromContext(ctx).Debug("Wait for connection recovery", "error", err, "duration", duration)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/krau/SaveAny-Bot/cmd/upload"
|
"github.com/krau/SaveAny-Bot/cmd/upload"
|
||||||
|
"github.com/krau/SaveAny-Bot/cmd/watch"
|
||||||
"github.com/krau/SaveAny-Bot/config"
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -18,6 +19,7 @@ var rootCmd = &cobra.Command{
|
|||||||
func init() {
|
func init() {
|
||||||
config.RegisterFlags(rootCmd)
|
config.RegisterFlags(rootCmd)
|
||||||
upload.Register(rootCmd)
|
upload.Register(rootCmd)
|
||||||
|
watch.Register(rootCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Execute(ctx context.Context) {
|
func Execute(ctx context.Context) {
|
||||||
|
|||||||
25
cmd/run.go
25
cmd/run.go
@@ -2,9 +2,9 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"slices"
|
"slices"
|
||||||
@@ -27,14 +27,27 @@ import (
|
|||||||
func Run(cmd *cobra.Command, _ []string) {
|
func Run(cmd *cobra.Command, _ []string) {
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
logger := log.NewWithOptions(os.Stdout, log.Options{
|
logger := log.NewWithOptions(os.Stdout, log.Options{
|
||||||
Level: log.DebugLevel,
|
Level: log.InfoLevel,
|
||||||
ReportTimestamp: true,
|
ReportTimestamp: true,
|
||||||
TimeFormat: time.TimeOnly,
|
TimeFormat: time.TimeOnly,
|
||||||
ReportCaller: true,
|
ReportCaller: true,
|
||||||
})
|
})
|
||||||
|
log.SetDefault(logger)
|
||||||
ctx = log.WithContext(ctx, logger)
|
ctx = log.WithContext(ctx, logger)
|
||||||
|
|
||||||
exitChan, err := initAll(ctx, cmd)
|
configFile := config.GetConfigFile(cmd)
|
||||||
|
if err := config.Init(ctx, configFile); err != nil {
|
||||||
|
logger.Fatal("Init failed", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
level, err := log.ParseLevel(strings.TrimSpace(config.C().Log.Level))
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Invalid log level, fallback to debug", "level", config.C().Log.Level, "error", err)
|
||||||
|
level = log.DebugLevel
|
||||||
|
}
|
||||||
|
logger.SetLevel(level)
|
||||||
|
|
||||||
|
exitChan, err := initAll(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal("Init failed", "error", err)
|
logger.Fatal("Init failed", "error", err)
|
||||||
}
|
}
|
||||||
@@ -51,11 +64,7 @@ func Run(cmd *cobra.Command, _ []string) {
|
|||||||
cleanCache()
|
cleanCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
func initAll(ctx context.Context, cmd *cobra.Command) (<-chan struct{}, error) {
|
func initAll(ctx context.Context) (<-chan struct{}, error) {
|
||||||
configFile := config.GetConfigFile(cmd)
|
|
||||||
if err := config.Init(ctx, configFile); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load config: %w", err)
|
|
||||||
}
|
|
||||||
cache.Init()
|
cache.Init()
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
i18n.Init(config.C().Lang)
|
i18n.Init(config.C().Lang)
|
||||||
|
|||||||
145
cmd/watch/cmd.go
Normal file
145
cmd/watch/cmd.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package watch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/krau/SaveAny-Bot/client/bot"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/cache"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/i18n"
|
||||||
|
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
|
||||||
|
"github.com/krau/SaveAny-Bot/config"
|
||||||
|
"github.com/krau/SaveAny-Bot/database"
|
||||||
|
stortype "github.com/krau/SaveAny-Bot/pkg/enums/storage"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var watchCmd = &cobra.Command{
|
||||||
|
Use: "watch",
|
||||||
|
Short: "watch a local directory and auto-upload changed files to storage",
|
||||||
|
Long: `Watch a local directory and automatically upload created or modified files
|
||||||
|
to the specified storage backend, preserving the relative directory structure.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
saveany-bot watch -p /data/inbox -s mystorage -d backup --recursive`,
|
||||||
|
RunE: runWatch,
|
||||||
|
}
|
||||||
|
|
||||||
|
func Register(root *cobra.Command) {
|
||||||
|
flags := watchCmd.Flags()
|
||||||
|
flags.StringP("path", "p", "", "local directory path to watch")
|
||||||
|
watchCmd.MarkFlagRequired("path")
|
||||||
|
flags.StringP("storage", "s", "", "storage name to upload to")
|
||||||
|
watchCmd.MarkFlagRequired("storage")
|
||||||
|
flags.StringP("dir", "d", "", "storage dir to upload to, default is the base_path of the storage")
|
||||||
|
flags.BoolP("recursive", "r", false, "watch subdirectories recursively")
|
||||||
|
flags.Bool("overwrite", false, "overwrite existing files on storage instead of skipping")
|
||||||
|
flags.Bool("initial-scan", false, "upload existing files in the directory on startup")
|
||||||
|
flags.Duration("debounce", 2*time.Second, "wait time after the last change before uploading a file")
|
||||||
|
flags.Int("upload-workers", 0, "number of concurrent uploads, default is config.workers")
|
||||||
|
flags.Duration("retry-delay", 3*time.Second, "delay between upload retries")
|
||||||
|
root.AddCommand(watchCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWatch(cmd *cobra.Command, _ []string) error {
|
||||||
|
watchPath, err := cmd.Flags().GetString("path")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
storName, err := cmd.Flags().GetString("storage")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
destDir, err := cmd.Flags().GetString("dir")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
recursive, err := cmd.Flags().GetBool("recursive")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
overwrite, err := cmd.Flags().GetBool("overwrite")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
initialScan, err := cmd.Flags().GetBool("initial-scan")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
debounce, err := cmd.Flags().GetDuration("debounce")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
uploadWorkers, err := cmd.Flags().GetInt("upload-workers")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
retryDelay, err := cmd.Flags().GetDuration("retry-delay")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := cmd.Context()
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
|
||||||
|
configFile := config.GetConfigFile(cmd)
|
||||||
|
if err := config.Init(ctx, configFile); err != nil {
|
||||||
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
|
}
|
||||||
|
i18n.Init(config.C().Lang)
|
||||||
|
cache.Init()
|
||||||
|
database.Init(ctx)
|
||||||
|
|
||||||
|
stor, err := storage.GetStorageByName(ctx, storName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get storage %q: %w", storName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Telegram storage needs the bot client and its ext context injected into ctx.
|
||||||
|
if stor.Type() == stortype.Telegram {
|
||||||
|
bot.Init(ctx)
|
||||||
|
ctx = tgutil.ExtWithContext(ctx, bot.ExtContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
if uploadWorkers < 1 {
|
||||||
|
uploadWorkers = config.C().Workers
|
||||||
|
}
|
||||||
|
|
||||||
|
uploader := NewUploader(ctx, UploaderOptions{
|
||||||
|
Storage: stor,
|
||||||
|
DestDir: destDir,
|
||||||
|
Overwrite: overwrite,
|
||||||
|
Workers: uploadWorkers,
|
||||||
|
Retry: config.C().Retry,
|
||||||
|
RetryDelay: retryDelay,
|
||||||
|
})
|
||||||
|
|
||||||
|
watcher, err := NewWatcher(ctx, WatcherOptions{
|
||||||
|
Root: watchPath,
|
||||||
|
Recursive: recursive,
|
||||||
|
Debounce: debounce,
|
||||||
|
Uploader: uploader,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
uploader.Close()
|
||||||
|
return fmt.Errorf("failed to create watcher: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if initialScan {
|
||||||
|
watcher.ScanExisting(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("watch started: %s -> storage %q dir %q", watchPath, storName, destDir)
|
||||||
|
|
||||||
|
// Run blocks until ctx is cancelled (e.g. SIGINT).
|
||||||
|
runErr := watcher.Run(ctx)
|
||||||
|
|
||||||
|
// Wait for in-flight uploads to finish before exiting.
|
||||||
|
logger.Info("waiting for in-flight uploads to finish...")
|
||||||
|
uploader.Close()
|
||||||
|
logger.Info("watch stopped")
|
||||||
|
|
||||||
|
return runErr
|
||||||
|
}
|
||||||
227
cmd/watch/uploader.go
Normal file
227
cmd/watch/uploader.go
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
package watch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/enums/ctxkey"
|
||||||
|
"github.com/krau/SaveAny-Bot/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type uploadJob struct {
|
||||||
|
// localPath is the absolute path of the local file.
|
||||||
|
localPath string
|
||||||
|
// relPath is relative to the watch root, used to preserve directory structure on storage.
|
||||||
|
relPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uploader uploads local files to the target storage via a worker pool.
|
||||||
|
// If a file changes while being uploaded, it is re-uploaded once after the
|
||||||
|
// current upload finishes, instead of being queued multiple times.
|
||||||
|
type Uploader struct {
|
||||||
|
stor storage.Storage
|
||||||
|
destDir string
|
||||||
|
overwrite bool
|
||||||
|
retry int
|
||||||
|
retryDelay time.Duration
|
||||||
|
logger *log.Logger
|
||||||
|
|
||||||
|
jobs chan uploadJob
|
||||||
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
// inflight maps in-progress (or queued) file paths. A true value means the
|
||||||
|
// file changed during upload and must be re-queued once done.
|
||||||
|
inflight map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploaderOptions struct {
|
||||||
|
Storage storage.Storage
|
||||||
|
DestDir string
|
||||||
|
Overwrite bool
|
||||||
|
Workers int
|
||||||
|
Retry int
|
||||||
|
RetryDelay time.Duration
|
||||||
|
QueueSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUploader creates and starts an Uploader. The caller must call Close when done.
|
||||||
|
func NewUploader(ctx context.Context, opts UploaderOptions) *Uploader {
|
||||||
|
if opts.Workers < 1 {
|
||||||
|
opts.Workers = 1
|
||||||
|
}
|
||||||
|
if opts.Retry < 1 {
|
||||||
|
opts.Retry = 1
|
||||||
|
}
|
||||||
|
if opts.RetryDelay <= 0 {
|
||||||
|
opts.RetryDelay = 3 * time.Second
|
||||||
|
}
|
||||||
|
if opts.QueueSize < opts.Workers {
|
||||||
|
opts.QueueSize = opts.Workers * 64
|
||||||
|
}
|
||||||
|
|
||||||
|
u := &Uploader{
|
||||||
|
stor: opts.Storage,
|
||||||
|
destDir: opts.DestDir,
|
||||||
|
overwrite: opts.Overwrite,
|
||||||
|
retry: opts.Retry,
|
||||||
|
retryDelay: opts.RetryDelay,
|
||||||
|
logger: log.FromContext(ctx).WithPrefix("uploader"),
|
||||||
|
jobs: make(chan uploadJob, opts.QueueSize),
|
||||||
|
inflight: make(map[string]bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < opts.Workers; i++ {
|
||||||
|
u.wg.Add(1)
|
||||||
|
go u.worker(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit enqueues an upload job. If the same file is already in flight, it is
|
||||||
|
// marked for re-upload instead of being queued again. Returns false if ctx is
|
||||||
|
// cancelled before the job can be enqueued.
|
||||||
|
func (u *Uploader) Submit(ctx context.Context, job uploadJob) bool {
|
||||||
|
u.mu.Lock()
|
||||||
|
if _, ok := u.inflight[job.localPath]; ok {
|
||||||
|
u.inflight[job.localPath] = true
|
||||||
|
u.mu.Unlock()
|
||||||
|
u.logger.Debugf("file %s already in flight, marked for re-upload", job.localPath)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
u.inflight[job.localPath] = false
|
||||||
|
u.mu.Unlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case u.jobs <- job:
|
||||||
|
return true
|
||||||
|
case <-ctx.Done():
|
||||||
|
u.mu.Lock()
|
||||||
|
delete(u.inflight, job.localPath)
|
||||||
|
u.mu.Unlock()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Uploader) worker(ctx context.Context) {
|
||||||
|
defer u.wg.Done()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case job, ok := <-u.jobs:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.process(ctx, job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Uploader) process(ctx context.Context, job uploadJob) {
|
||||||
|
if err := u.uploadWithRetry(ctx, job); err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
u.clearInflight(job.localPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.logger.Errorf("failed to upload %s after %d attempt(s): %v", job.localPath, u.retry, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-queue if the file changed again while it was being uploaded.
|
||||||
|
u.mu.Lock()
|
||||||
|
needReupload := u.inflight[job.localPath]
|
||||||
|
if needReupload {
|
||||||
|
u.inflight[job.localPath] = false
|
||||||
|
} else {
|
||||||
|
delete(u.inflight, job.localPath)
|
||||||
|
}
|
||||||
|
u.mu.Unlock()
|
||||||
|
|
||||||
|
if needReupload {
|
||||||
|
select {
|
||||||
|
case u.jobs <- job:
|
||||||
|
u.logger.Debugf("re-queued %s due to changes during upload", job.localPath)
|
||||||
|
case <-ctx.Done():
|
||||||
|
u.clearInflight(job.localPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Uploader) clearInflight(localPath string) {
|
||||||
|
u.mu.Lock()
|
||||||
|
delete(u.inflight, localPath)
|
||||||
|
u.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Uploader) uploadWithRetry(ctx context.Context, job uploadJob) error {
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 1; attempt <= u.retry; attempt++ {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
err := u.upload(ctx, job)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
u.logger.Warnf("upload %s failed (attempt %d/%d): %v", job.localPath, attempt, u.retry, err)
|
||||||
|
if attempt < u.retry {
|
||||||
|
select {
|
||||||
|
case <-time.After(u.retryDelay):
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Uploader) upload(ctx context.Context, job uploadJob) error {
|
||||||
|
file, err := os.Open(filepath.Clean(job.localPath))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
info, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to stat file: %w", err)
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
return fmt.Errorf("path is a directory, not a file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the relative directory structure on the storage side.
|
||||||
|
storagePath := path.Join(u.destDir, filepath.ToSlash(job.relPath))
|
||||||
|
|
||||||
|
uploadCtx := context.WithValue(ctx, ctxkey.ContentLength, info.Size())
|
||||||
|
if u.overwrite {
|
||||||
|
uploadCtx = storage.WithOverwrite(uploadCtx)
|
||||||
|
} else if u.stor.Exists(uploadCtx, storagePath) {
|
||||||
|
u.logger.Infof("skip existing file: %s", storagePath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u.logger.Infof("uploading %s -> %s (%d bytes)", job.localPath, storagePath, info.Size())
|
||||||
|
if err := u.stor.Save(uploadCtx, file, storagePath); err != nil {
|
||||||
|
return fmt.Errorf("failed to save to storage: %w", err)
|
||||||
|
}
|
||||||
|
u.logger.Infof("uploaded %s", storagePath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close stops accepting jobs and waits for in-flight uploads to finish.
|
||||||
|
func (u *Uploader) Close() {
|
||||||
|
close(u.jobs)
|
||||||
|
u.wg.Wait()
|
||||||
|
}
|
||||||
269
cmd/watch/watcher.go
Normal file
269
cmd/watch/watcher.go
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
package watch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Watcher watches a local directory and submits stable files to the Uploader.
|
||||||
|
//
|
||||||
|
// Write-completion detection: fsnotify emits Write events throughout a write.
|
||||||
|
// Watcher debounces per file and only uploads once the file size stays
|
||||||
|
// unchanged across a debounce window, avoiding uploads of partial files.
|
||||||
|
type Watcher struct {
|
||||||
|
root string
|
||||||
|
recursive bool
|
||||||
|
debounce time.Duration
|
||||||
|
uploader *Uploader
|
||||||
|
logger *log.Logger
|
||||||
|
|
||||||
|
fsw *fsnotify.Watcher
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
pending map[string]*time.Timer
|
||||||
|
// lastSize is the last observed file size, used to detect a stable write.
|
||||||
|
lastSize map[string]int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type WatcherOptions struct {
|
||||||
|
Root string
|
||||||
|
Recursive bool
|
||||||
|
Debounce time.Duration
|
||||||
|
Uploader *Uploader
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWatcher creates a Watcher.
|
||||||
|
func NewWatcher(ctx context.Context, opts WatcherOptions) (*Watcher, error) {
|
||||||
|
if opts.Debounce <= 0 {
|
||||||
|
opts.Debounce = 2 * time.Second
|
||||||
|
}
|
||||||
|
root, err := filepath.Abs(opts.Root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to resolve root path: %w", err)
|
||||||
|
}
|
||||||
|
info, err := os.Stat(root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to stat root path: %w", err)
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
return nil, fmt.Errorf("watch path must be a directory: %s", root)
|
||||||
|
}
|
||||||
|
|
||||||
|
fsw, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create fsnotify watcher: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w := &Watcher{
|
||||||
|
root: root,
|
||||||
|
recursive: opts.Recursive,
|
||||||
|
debounce: opts.Debounce,
|
||||||
|
uploader: opts.Uploader,
|
||||||
|
logger: log.FromContext(ctx).WithPrefix("watcher"),
|
||||||
|
fsw: fsw,
|
||||||
|
pending: make(map[string]*time.Timer),
|
||||||
|
lastSize: make(map[string]int64),
|
||||||
|
}
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run starts watching and blocks until ctx is cancelled.
|
||||||
|
func (w *Watcher) Run(ctx context.Context) error {
|
||||||
|
if err := w.addDir(w.root); err != nil {
|
||||||
|
w.fsw.Close()
|
||||||
|
return fmt.Errorf("failed to watch root: %w", err)
|
||||||
|
}
|
||||||
|
w.logger.Infof("watching %s (recursive=%v, debounce=%s)", w.root, w.recursive, w.debounce)
|
||||||
|
|
||||||
|
defer w.cleanup()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
w.logger.Info("stopping watcher")
|
||||||
|
return nil
|
||||||
|
case event, ok := <-w.fsw.Events:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
w.handleEvent(ctx, event)
|
||||||
|
case err, ok := <-w.fsw.Errors:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
w.logger.Errorf("watch error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) handleEvent(ctx context.Context, event fsnotify.Event) {
|
||||||
|
// Remove/Rename: cancel any pending upload for this path.
|
||||||
|
if event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename) {
|
||||||
|
w.cancelPending(event.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !event.Has(fsnotify.Create) && !event.Has(fsnotify.Write) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := os.Stat(event.Name)
|
||||||
|
if err != nil {
|
||||||
|
// File may have been removed or moved; ignore.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
// New directory: watch it recursively and scan files already inside.
|
||||||
|
if event.Has(fsnotify.Create) && w.recursive {
|
||||||
|
if err := w.addDir(event.Name); err != nil {
|
||||||
|
w.logger.Errorf("failed to watch new dir %s: %v", event.Name, err)
|
||||||
|
}
|
||||||
|
w.scanExisting(ctx, event.Name)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.scheduleUpload(ctx, event.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// scheduleUpload schedules a debounced upload for a file.
|
||||||
|
func (w *Watcher) scheduleUpload(ctx context.Context, file string) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
if t, ok := w.pending[file]; ok {
|
||||||
|
t.Stop()
|
||||||
|
}
|
||||||
|
w.pending[file] = time.AfterFunc(w.debounce, func() {
|
||||||
|
w.maybeUpload(ctx, file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybeUpload submits the upload once the debounce window passes and the file
|
||||||
|
// size is stable; otherwise it waits another window.
|
||||||
|
func (w *Watcher) maybeUpload(ctx context.Context, file string) {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := os.Stat(file)
|
||||||
|
if err != nil {
|
||||||
|
w.cancelPending(file)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
w.cancelPending(file)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
prevSize, seen := w.lastSize[file]
|
||||||
|
curSize := info.Size()
|
||||||
|
if !seen || prevSize != curSize {
|
||||||
|
// Size still changing: likely still being written, wait another window.
|
||||||
|
w.lastSize[file] = curSize
|
||||||
|
w.pending[file] = time.AfterFunc(w.debounce, func() {
|
||||||
|
w.maybeUpload(ctx, file)
|
||||||
|
})
|
||||||
|
w.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Size stable: treat write as complete.
|
||||||
|
delete(w.pending, file)
|
||||||
|
delete(w.lastSize, file)
|
||||||
|
w.mu.Unlock()
|
||||||
|
|
||||||
|
relPath, err := filepath.Rel(w.root, file)
|
||||||
|
if err != nil {
|
||||||
|
w.logger.Errorf("failed to compute relative path for %s: %v", file, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.uploader.Submit(ctx, uploadJob{localPath: file, relPath: relPath})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) cancelPending(file string) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
if t, ok := w.pending[file]; ok {
|
||||||
|
t.Stop()
|
||||||
|
delete(w.pending, file)
|
||||||
|
}
|
||||||
|
delete(w.lastSize, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addDir adds a directory to the watch list, recursively when enabled.
|
||||||
|
func (w *Watcher) addDir(dir string) error {
|
||||||
|
if !w.recursive {
|
||||||
|
return w.fsw.Add(dir)
|
||||||
|
}
|
||||||
|
return filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
w.logger.Warnf("skip path %s: %v", p, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if d.IsDir() {
|
||||||
|
if addErr := w.fsw.Add(p); addErr != nil {
|
||||||
|
w.logger.Warnf("failed to watch dir %s: %v", p, addErr)
|
||||||
|
} else {
|
||||||
|
w.logger.Debugf("watching dir %s", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanExisting submits files already present under dir (initial sync and new-dir backfill).
|
||||||
|
func (w *Watcher) scanExisting(ctx context.Context, dir string) {
|
||||||
|
walkFn := func(p string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
w.logger.Warnf("skip path %s: %v", p, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
if d.IsDir() {
|
||||||
|
if !w.recursive && p != dir {
|
||||||
|
return fs.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
relPath, relErr := filepath.Rel(w.root, p)
|
||||||
|
if relErr != nil {
|
||||||
|
w.logger.Errorf("failed to compute relative path for %s: %v", p, relErr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
w.uploader.Submit(ctx, uploadJob{localPath: p, relPath: relPath})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := filepath.WalkDir(dir, walkFn); err != nil && ctx.Err() == nil {
|
||||||
|
w.logger.Errorf("failed to scan dir %s: %v", dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanExisting triggers a one-time scan and upload of existing files under the watch root.
|
||||||
|
func (w *Watcher) ScanExisting(ctx context.Context) {
|
||||||
|
w.logger.Info("scanning existing files for initial sync")
|
||||||
|
w.scanExisting(ctx, w.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) cleanup() {
|
||||||
|
w.mu.Lock()
|
||||||
|
for _, t := range w.pending {
|
||||||
|
t.Stop()
|
||||||
|
}
|
||||||
|
w.pending = make(map[string]*time.Timer)
|
||||||
|
w.lastSize = make(map[string]int64)
|
||||||
|
w.mu.Unlock()
|
||||||
|
w.fsw.Close()
|
||||||
|
}
|
||||||
@@ -36,6 +36,9 @@ const (
|
|||||||
BotMsgCmdUpdate Key = "bot.msg.cmd.update"
|
BotMsgCmdUpdate Key = "bot.msg.cmd.update"
|
||||||
BotMsgCmdWatch Key = "bot.msg.cmd.watch"
|
BotMsgCmdWatch Key = "bot.msg.cmd.watch"
|
||||||
BotMsgCmdYtdlp Key = "bot.msg.cmd.ytdlp"
|
BotMsgCmdYtdlp Key = "bot.msg.cmd.ytdlp"
|
||||||
|
BotMsgCommonButtonConflictOverwrite Key = "bot.msg.common.button_conflict_overwrite"
|
||||||
|
BotMsgCommonButtonConflictRename Key = "bot.msg.common.button_conflict_rename"
|
||||||
|
BotMsgCommonButtonConflictSkip Key = "bot.msg.common.button_conflict_skip"
|
||||||
BotMsgCommonCancelButtonText Key = "bot.msg.common.cancel_button_text"
|
BotMsgCommonCancelButtonText Key = "bot.msg.common.cancel_button_text"
|
||||||
BotMsgCommonErrorBuildDirSelectKeyboardFailed Key = "bot.msg.common.error_build_dir_select_keyboard_failed"
|
BotMsgCommonErrorBuildDirSelectKeyboardFailed Key = "bot.msg.common.error_build_dir_select_keyboard_failed"
|
||||||
BotMsgCommonErrorBuildStorageSelectKeyboardFailed Key = "bot.msg.common.error_build_storage_select_keyboard_failed"
|
BotMsgCommonErrorBuildStorageSelectKeyboardFailed Key = "bot.msg.common.error_build_storage_select_keyboard_failed"
|
||||||
@@ -63,7 +66,10 @@ const (
|
|||||||
BotMsgCommonErrorTaskAddFailed Key = "bot.msg.common.error_task_add_failed"
|
BotMsgCommonErrorTaskAddFailed Key = "bot.msg.common.error_task_add_failed"
|
||||||
BotMsgCommonErrorTaskCreateFailed Key = "bot.msg.common.error_task_create_failed"
|
BotMsgCommonErrorTaskCreateFailed Key = "bot.msg.common.error_task_create_failed"
|
||||||
BotMsgCommonErrorUpdateUserInfoFailed Key = "bot.msg.common.error_update_user_info_failed"
|
BotMsgCommonErrorUpdateUserInfoFailed Key = "bot.msg.common.error_update_user_info_failed"
|
||||||
|
BotMsgCommonInfoAllConflictFilesSkipped Key = "bot.msg.common.info_all_conflict_files_skipped"
|
||||||
BotMsgCommonInfoBatchTasksAdded Key = "bot.msg.common.info_batch_tasks_added"
|
BotMsgCommonInfoBatchTasksAdded Key = "bot.msg.common.info_batch_tasks_added"
|
||||||
|
BotMsgCommonInfoBatchTasksAddedWithSkipped Key = "bot.msg.common.info_batch_tasks_added_with_skipped"
|
||||||
|
BotMsgCommonInfoConflictFilesSkipped Key = "bot.msg.common.info_conflict_files_skipped"
|
||||||
BotMsgCommonInfoDefaultStorageSet Key = "bot.msg.common.info_default_storage_set"
|
BotMsgCommonInfoDefaultStorageSet Key = "bot.msg.common.info_default_storage_set"
|
||||||
BotMsgCommonInfoDefaultStorageWithDirSet Key = "bot.msg.common.info_default_storage_with_dir_set"
|
BotMsgCommonInfoDefaultStorageWithDirSet Key = "bot.msg.common.info_default_storage_with_dir_set"
|
||||||
BotMsgCommonInfoFetchingFileInfo Key = "bot.msg.common.info_fetching_file_info"
|
BotMsgCommonInfoFetchingFileInfo Key = "bot.msg.common.info_fetching_file_info"
|
||||||
@@ -73,16 +79,25 @@ const (
|
|||||||
BotMsgCommonInfoSilentModeOff Key = "bot.msg.common.info_silent_mode_off"
|
BotMsgCommonInfoSilentModeOff Key = "bot.msg.common.info_silent_mode_off"
|
||||||
BotMsgCommonInfoSilentModeOn Key = "bot.msg.common.info_silent_mode_on"
|
BotMsgCommonInfoSilentModeOn Key = "bot.msg.common.info_silent_mode_on"
|
||||||
BotMsgCommonInfoTaskAdded Key = "bot.msg.common.info_task_added"
|
BotMsgCommonInfoTaskAdded Key = "bot.msg.common.info_task_added"
|
||||||
|
BotMsgCommonPromptConflictMoreFiles Key = "bot.msg.common.prompt_conflict_more_files"
|
||||||
|
BotMsgCommonPromptSelectConflictStrategy Key = "bot.msg.common.prompt_select_conflict_strategy"
|
||||||
BotMsgCommonPromptSelectDefaultDir Key = "bot.msg.common.prompt_select_default_dir"
|
BotMsgCommonPromptSelectDefaultDir Key = "bot.msg.common.prompt_select_default_dir"
|
||||||
BotMsgCommonPromptSelectDefaultStorage Key = "bot.msg.common.prompt_select_default_storage"
|
BotMsgCommonPromptSelectDefaultStorage Key = "bot.msg.common.prompt_select_default_storage"
|
||||||
BotMsgCommonPromptSelectDir Key = "bot.msg.common.prompt_select_dir"
|
BotMsgCommonPromptSelectDir Key = "bot.msg.common.prompt_select_dir"
|
||||||
BotMsgConfigButtonFilenameStrategy Key = "bot.msg.config.button_filename_strategy"
|
BotMsgConfigButtonFilenameStrategy Key = "bot.msg.config.button_filename_strategy"
|
||||||
|
BotMsgConfigButtonConflictStrategy Key = "bot.msg.config.button_conflict_strategy"
|
||||||
|
BotMsgConfigConflictStrategyAsk Key = "bot.msg.config.conflict_strategy_ask"
|
||||||
|
BotMsgConfigConflictStrategyOverwrite Key = "bot.msg.config.conflict_strategy_overwrite"
|
||||||
|
BotMsgConfigConflictStrategyRename Key = "bot.msg.config.conflict_strategy_rename"
|
||||||
|
BotMsgConfigConflictStrategySkip Key = "bot.msg.config.conflict_strategy_skip"
|
||||||
BotMsgConfigErrorInvalidCallbackData Key = "bot.msg.config.error_invalid_callback_data"
|
BotMsgConfigErrorInvalidCallbackData Key = "bot.msg.config.error_invalid_callback_data"
|
||||||
BotMsgConfigErrorInvalidTemplate Key = "bot.msg.config.error_invalid_template"
|
BotMsgConfigErrorInvalidTemplate Key = "bot.msg.config.error_invalid_template"
|
||||||
BotMsgConfigFnametmplHelp Key = "bot.msg.config.fnametmpl_help"
|
BotMsgConfigFnametmplHelp Key = "bot.msg.config.fnametmpl_help"
|
||||||
BotMsgConfigInfoCurrentTemplatePrefix Key = "bot.msg.config.info_current_template_prefix"
|
BotMsgConfigInfoCurrentTemplatePrefix Key = "bot.msg.config.info_current_template_prefix"
|
||||||
|
BotMsgConfigInfoConflictStrategySet Key = "bot.msg.config.info_conflict_strategy_set"
|
||||||
BotMsgConfigInfoFilenameStrategySet Key = "bot.msg.config.info_filename_strategy_set"
|
BotMsgConfigInfoFilenameStrategySet Key = "bot.msg.config.info_filename_strategy_set"
|
||||||
BotMsgConfigInfoTemplateUpdated Key = "bot.msg.config.info_template_updated"
|
BotMsgConfigInfoTemplateUpdated Key = "bot.msg.config.info_template_updated"
|
||||||
|
BotMsgConfigPromptSelectConflictStrategy Key = "bot.msg.config.prompt_select_conflict_strategy"
|
||||||
BotMsgConfigPromptSelectFilenameStrategy Key = "bot.msg.config.prompt_select_filename_strategy"
|
BotMsgConfigPromptSelectFilenameStrategy Key = "bot.msg.config.prompt_select_filename_strategy"
|
||||||
BotMsgConfigPromptSelectOption Key = "bot.msg.config.prompt_select_option"
|
BotMsgConfigPromptSelectOption Key = "bot.msg.config.prompt_select_option"
|
||||||
BotMsgDirButtonDefault Key = "bot.msg.dir.button_default"
|
BotMsgDirButtonDefault Key = "bot.msg.dir.button_default"
|
||||||
|
|||||||
@@ -112,9 +112,17 @@ bot:
|
|||||||
error_task_add_failed: "Failed to add task: {{.Error}}"
|
error_task_add_failed: "Failed to add task: {{.Error}}"
|
||||||
info_task_added: "Task added"
|
info_task_added: "Task added"
|
||||||
info_batch_tasks_added: "Batch tasks added, total {{.Count}} files"
|
info_batch_tasks_added: "Batch tasks added, total {{.Count}} files"
|
||||||
|
info_batch_tasks_added_with_skipped: "Batch tasks added, total {{.Count}} files\nSkipped conflicting files:\n{{.Skipped}}"
|
||||||
|
info_all_conflict_files_skipped: "All conflicting files were skipped:\n{{.Skipped}}"
|
||||||
|
info_conflict_files_skipped: "Skipped conflicting files:\n{{.Skipped}}"
|
||||||
error_task_create_failed: "Failed to create task: {{.Error}}"
|
error_task_create_failed: "Failed to create task: {{.Error}}"
|
||||||
error_get_dir_failed: "Failed to get directory: {{.Error}}"
|
error_get_dir_failed: "Failed to get directory: {{.Error}}"
|
||||||
prompt_select_dir: "Please select a directory to store to"
|
prompt_select_dir: "Please select a directory to store to"
|
||||||
|
prompt_select_conflict_strategy: "Files with the same name already exist. Please select a save strategy:\n{{.Files}}"
|
||||||
|
prompt_conflict_more_files: "...and {{.Count}} more files"
|
||||||
|
button_conflict_rename: "Rename"
|
||||||
|
button_conflict_overwrite: "Overwrite"
|
||||||
|
button_conflict_skip: "Skip"
|
||||||
prompt_select_default_dir: "Please select a default directory to save to"
|
prompt_select_default_dir: "Please select a default directory to save to"
|
||||||
info_default_storage_set: "Default storage set to: {{.Name}}"
|
info_default_storage_set: "Default storage set to: {{.Name}}"
|
||||||
info_default_storage_with_dir_set: "Default storage set to: {{.Name}}:/{{.Dir}}"
|
info_default_storage_with_dir_set: "Default storage set to: {{.Name}}:/{{.Dir}}"
|
||||||
@@ -266,10 +274,17 @@ bot:
|
|||||||
config:
|
config:
|
||||||
prompt_select_option: "Please select an option to configure"
|
prompt_select_option: "Please select an option to configure"
|
||||||
button_filename_strategy: "Filename strategy"
|
button_filename_strategy: "Filename strategy"
|
||||||
|
button_conflict_strategy: "Duplicate file strategy"
|
||||||
error_invalid_callback_data: "Invalid callback data"
|
error_invalid_callback_data: "Invalid callback data"
|
||||||
error_invalid_template: "Invalid template, please check syntax\n{{.Error}}"
|
error_invalid_template: "Invalid template, please check syntax\n{{.Error}}"
|
||||||
info_filename_strategy_set: "Filename strategy set to: {{.Strategy}}"
|
info_filename_strategy_set: "Filename strategy set to: {{.Strategy}}"
|
||||||
|
info_conflict_strategy_set: "Duplicate file strategy set to: {{.Strategy}}"
|
||||||
prompt_select_filename_strategy: "Please select filename strategy, current strategy: {{.Strategy}}"
|
prompt_select_filename_strategy: "Please select filename strategy, current strategy: {{.Strategy}}"
|
||||||
|
prompt_select_conflict_strategy: "Please select duplicate file strategy, current strategy: {{.Strategy}}"
|
||||||
|
conflict_strategy_rename: "Always rename"
|
||||||
|
conflict_strategy_ask: "Ask every time"
|
||||||
|
conflict_strategy_overwrite: "Always overwrite"
|
||||||
|
conflict_strategy_skip: "Always skip"
|
||||||
fnametmpl_help: |-
|
fnametmpl_help: |-
|
||||||
Use this command to set filename template, for example:
|
Use this command to set filename template, for example:
|
||||||
/fnametmpl Image_{{"{{.msgid}}"}}_{{"{{.msgdate}}"}}.jpg
|
/fnametmpl Image_{{"{{.msgid}}"}}_{{"{{.msgdate}}"}}.jpg
|
||||||
|
|||||||
@@ -113,9 +113,17 @@ bot:
|
|||||||
error_task_add_failed: "任务添加失败: {{.Error}}"
|
error_task_add_failed: "任务添加失败: {{.Error}}"
|
||||||
info_task_added: "任务已添加"
|
info_task_added: "任务已添加"
|
||||||
info_batch_tasks_added: "已添加批量任务, 共 {{.Count}} 个文件"
|
info_batch_tasks_added: "已添加批量任务, 共 {{.Count}} 个文件"
|
||||||
|
info_batch_tasks_added_with_skipped: "已添加批量任务, 共 {{.Count}} 个文件\n已跳过同名文件:\n{{.Skipped}}"
|
||||||
|
info_all_conflict_files_skipped: "全部同名文件已跳过:\n{{.Skipped}}"
|
||||||
|
info_conflict_files_skipped: "已跳过同名文件:\n{{.Skipped}}"
|
||||||
error_task_create_failed: "任务创建失败: {{.Error}}"
|
error_task_create_failed: "任务创建失败: {{.Error}}"
|
||||||
error_get_dir_failed: "获取目录失败: {{.Error}}"
|
error_get_dir_failed: "获取目录失败: {{.Error}}"
|
||||||
prompt_select_dir: "请选择要存储到的目录"
|
prompt_select_dir: "请选择要存储到的目录"
|
||||||
|
prompt_select_conflict_strategy: "检测到同名文件, 请选择保存策略:\n{{.Files}}"
|
||||||
|
prompt_conflict_more_files: "...还有 {{.Count}} 个文件"
|
||||||
|
button_conflict_rename: "重命名"
|
||||||
|
button_conflict_overwrite: "覆盖"
|
||||||
|
button_conflict_skip: "跳过"
|
||||||
prompt_select_default_dir: "请选择要保存到的默认文件夹"
|
prompt_select_default_dir: "请选择要保存到的默认文件夹"
|
||||||
info_default_storage_set: "已将默认存储位置设为: {{.Name}}"
|
info_default_storage_set: "已将默认存储位置设为: {{.Name}}"
|
||||||
info_default_storage_with_dir_set: "已将默认存储位置设为: {{.Name}}:/{{.Dir}}"
|
info_default_storage_with_dir_set: "已将默认存储位置设为: {{.Name}}:/{{.Dir}}"
|
||||||
@@ -267,10 +275,17 @@ bot:
|
|||||||
config:
|
config:
|
||||||
prompt_select_option: "请选择要配置的选项"
|
prompt_select_option: "请选择要配置的选项"
|
||||||
button_filename_strategy: "文件名策略"
|
button_filename_strategy: "文件名策略"
|
||||||
|
button_conflict_strategy: "重名文件保存策略"
|
||||||
error_invalid_callback_data: "无效的回调数据"
|
error_invalid_callback_data: "无效的回调数据"
|
||||||
error_invalid_template: "无效的模板, 请检查语法\n{{.Error}}"
|
error_invalid_template: "无效的模板, 请检查语法\n{{.Error}}"
|
||||||
info_filename_strategy_set: "已将文件名策略设置为: {{.Strategy}}"
|
info_filename_strategy_set: "已将文件名策略设置为: {{.Strategy}}"
|
||||||
|
info_conflict_strategy_set: "已将重名文件保存策略设置为: {{.Strategy}}"
|
||||||
prompt_select_filename_strategy: "请选择文件名策略, 当前策略: {{.Strategy}}"
|
prompt_select_filename_strategy: "请选择文件名策略, 当前策略: {{.Strategy}}"
|
||||||
|
prompt_select_conflict_strategy: "请选择重名文件保存策略, 当前策略: {{.Strategy}}"
|
||||||
|
conflict_strategy_rename: "始终重命名"
|
||||||
|
conflict_strategy_ask: "每次询问"
|
||||||
|
conflict_strategy_overwrite: "始终覆盖"
|
||||||
|
conflict_strategy_skip: "始终跳过"
|
||||||
fnametmpl_help: |-
|
fnametmpl_help: |-
|
||||||
使用该命令设置文件名模板, 示例:
|
使用该命令设置文件名模板, 示例:
|
||||||
/fnametmpl 图片_{{"{{.msgid}}"}}_{{"{{.msgdate}}"}}.jpg
|
/fnametmpl 图片_{{"{{.msgid}}"}}_{{"{{.msgdate}}"}}.jpg
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ retry = 3 # 下载失败重试次数
|
|||||||
threads = 4 # 单个任务下载使用的最大线程数
|
threads = 4 # 单个任务下载使用的最大线程数
|
||||||
stream = false # 使用流式传输模式, 建议仅在硬盘空间十分有限时使用.
|
stream = false # 使用流式传输模式, 建议仅在硬盘空间十分有限时使用.
|
||||||
|
|
||||||
|
[log]
|
||||||
|
# 日志级别, 可选: debug, info, warn, error, fatal
|
||||||
|
level = "debug"
|
||||||
|
|
||||||
[telegram]
|
[telegram]
|
||||||
# Bot Token
|
# Bot Token
|
||||||
# 更换 Bot Token 后请删除会话数据库文件 (默认路径为 data/session.db )
|
# 更换 Bot Token 后请删除会话数据库文件 (默认路径为 data/session.db )
|
||||||
@@ -73,4 +77,4 @@ blacklist = true
|
|||||||
[[users]]
|
[[users]]
|
||||||
id = 123456
|
id = 123456
|
||||||
storages = ["本机1"]
|
storages = ["本机1"]
|
||||||
blacklist = false # 使用白名单模式,此时,用户 123456 仅可使用标识名为 '本地1' 的存储
|
blacklist = false # 使用白名单模式,此时,用户 123456 仅可使用标识名为 '本地1' 的存储
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ func RegisterFlags(cmd *cobra.Command) {
|
|||||||
flags.Bool("stream", false, "enable stream mode")
|
flags.Bool("stream", false, "enable stream mode")
|
||||||
flags.Bool("no-clean-cache", false, "do not clean cache on exit")
|
flags.Bool("no-clean-cache", false, "do not clean cache on exit")
|
||||||
flags.String("proxy", "", "proxy URL (http, https, socks5, socks5h)")
|
flags.String("proxy", "", "proxy URL (http, https, socks5, socks5h)")
|
||||||
|
flags.String("log-level", "", "log level (trace/debug, info, warn, error, fatal)")
|
||||||
|
|
||||||
// Telegram 配置
|
// Telegram 配置
|
||||||
flags.String("telegram-token", "", "telegram bot token")
|
flags.String("telegram-token", "", "telegram bot token")
|
||||||
@@ -54,6 +55,7 @@ func bindFlags(cmd *cobra.Command) {
|
|||||||
viper.BindPFlag("stream", flags.Lookup("stream"))
|
viper.BindPFlag("stream", flags.Lookup("stream"))
|
||||||
viper.BindPFlag("no_clean_cache", flags.Lookup("no-clean-cache"))
|
viper.BindPFlag("no_clean_cache", flags.Lookup("no-clean-cache"))
|
||||||
viper.BindPFlag("proxy", flags.Lookup("proxy"))
|
viper.BindPFlag("proxy", flags.Lookup("proxy"))
|
||||||
|
viper.BindPFlag("log.level", flags.Lookup("log-level"))
|
||||||
|
|
||||||
// Telegram
|
// Telegram
|
||||||
viper.BindPFlag("telegram.token", flags.Lookup("telegram-token"))
|
viper.BindPFlag("telegram.token", flags.Lookup("telegram-token"))
|
||||||
|
|||||||
5
config/log.go
Normal file
5
config/log.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type logConfig struct {
|
||||||
|
Level string `toml:"level" mapstructure:"level" json:"level"`
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ type Config struct {
|
|||||||
Threads int `toml:"threads" mapstructure:"threads" json:"threads"`
|
Threads int `toml:"threads" mapstructure:"threads" json:"threads"`
|
||||||
Stream bool `toml:"stream" mapstructure:"stream" json:"stream"`
|
Stream bool `toml:"stream" mapstructure:"stream" json:"stream"`
|
||||||
Proxy string `toml:"proxy" mapstructure:"proxy" json:"proxy"`
|
Proxy string `toml:"proxy" mapstructure:"proxy" json:"proxy"`
|
||||||
|
Log logConfig `toml:"log" mapstructure:"log" json:"log"`
|
||||||
Aria2 aria2Config `toml:"aria2" mapstructure:"aria2" json:"aria2"`
|
Aria2 aria2Config `toml:"aria2" mapstructure:"aria2" json:"aria2"`
|
||||||
API apiConfig `toml:"api" mapstructure:"api" json:"api"`
|
API apiConfig `toml:"api" mapstructure:"api" json:"api"`
|
||||||
|
|
||||||
@@ -100,10 +101,11 @@ func Init(ctx context.Context, configFile ...string) error {
|
|||||||
|
|
||||||
defaultConfigs := map[string]any{
|
defaultConfigs := map[string]any{
|
||||||
// 基础配置
|
// 基础配置
|
||||||
"lang": "zh-Hans",
|
"lang": "zh-Hans",
|
||||||
"workers": 3,
|
"workers": 3,
|
||||||
"retry": 3,
|
"retry": 3,
|
||||||
"threads": 4,
|
"threads": 4,
|
||||||
|
"log.level": "debug",
|
||||||
|
|
||||||
// 缓存配置
|
// 缓存配置
|
||||||
"cache.ttl": 86400,
|
"cache.ttl": 86400,
|
||||||
@@ -135,12 +137,6 @@ func Init(ctx context.Context, configFile ...string) error {
|
|||||||
viper.SetDefault(key, value)
|
viper.SetDefault(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := viper.SafeWriteConfigAs("config.toml"); err != nil {
|
|
||||||
if _, ok := err.(viper.ConfigFileAlreadyExistsError); !ok {
|
|
||||||
return fmt.Errorf("error saving default config: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
fmt.Println("Error reading config file, ", err)
|
fmt.Println("Error reading config file, ", err)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ type Progress struct {
|
|||||||
ChatID int64
|
ChatID int64
|
||||||
start time.Time
|
start time.Time
|
||||||
lastUpdatePercent atomic.Int32
|
lastUpdatePercent atomic.Int32
|
||||||
|
skippedFiles []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Progress) OnStart(ctx context.Context, info TaskInfo) {
|
func (p *Progress) OnStart(ctx context.Context, info TaskInfo) {
|
||||||
@@ -151,6 +153,14 @@ func (p *Progress) OnDone(ctx context.Context, info TaskInfo, err error) {
|
|||||||
styling.Code(strconv.Itoa(info.Count())),
|
styling.Code(strconv.Itoa(info.Count())),
|
||||||
styling.Plain(i18n.T(i18nk.BotMsgProgressTotalSizePrefix, nil)),
|
styling.Plain(i18n.T(i18nk.BotMsgProgressTotalSizePrefix, nil)),
|
||||||
styling.Code(fmt.Sprintf("%.2f MB", float64(info.TotalSize())/(1024*1024))),
|
styling.Code(fmt.Sprintf("%.2f MB", float64(info.TotalSize())/(1024*1024))),
|
||||||
|
func() styling.StyledTextOption {
|
||||||
|
if len(p.skippedFiles) == 0 {
|
||||||
|
return styling.Plain("")
|
||||||
|
}
|
||||||
|
return styling.Plain("\n\n" + i18n.T(i18nk.BotMsgCommonInfoConflictFilesSkipped, map[string]any{
|
||||||
|
"Skipped": strings.Join(p.skippedFiles, "\n"),
|
||||||
|
}))
|
||||||
|
}(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,8 +183,13 @@ func (p *Progress) OnDone(ctx context.Context, info TaskInfo, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewProgressTracker(messageID int, chatID int64) ProgressTracker {
|
func NewProgressTracker(messageID int, chatID int64) ProgressTracker {
|
||||||
|
return NewProgressTrackerWithSkipped(messageID, chatID, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProgressTrackerWithSkipped(messageID int, chatID int64, skippedFiles []string) ProgressTracker {
|
||||||
return &Progress{
|
return &Progress{
|
||||||
MessageID: messageID,
|
MessageID: messageID,
|
||||||
ChatID: chatID,
|
ChatID: chatID,
|
||||||
|
skippedFiles: skippedFiles,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "github.com/ncruces/go-sqlite3/embed"
|
|
||||||
"github.com/ncruces/go-sqlite3/gormlite"
|
"github.com/ncruces/go-sqlite3/gormlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type User struct {
|
|||||||
WatchChats []WatchChat
|
WatchChats []WatchChat
|
||||||
FilenameStrategy string
|
FilenameStrategy string
|
||||||
FilenameTemplate string
|
FilenameTemplate string
|
||||||
|
ConflictStrategy string
|
||||||
}
|
}
|
||||||
|
|
||||||
type WatchChat struct {
|
type WatchChat struct {
|
||||||
|
|||||||
72
go.mod
72
go.mod
@@ -9,36 +9,36 @@ require (
|
|||||||
github.com/charmbracelet/bubbles v1.0.0
|
github.com/charmbracelet/bubbles v1.0.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/charmbracelet/log v0.4.2
|
github.com/charmbracelet/log v1.0.0
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13
|
github.com/gabriel-vasile/mimetype v1.4.13
|
||||||
github.com/goccy/go-yaml v1.19.2
|
github.com/goccy/go-yaml v1.19.2
|
||||||
github.com/gotd/contrib v0.21.1
|
github.com/gotd/contrib v0.21.1
|
||||||
github.com/gotd/td v0.142.0
|
github.com/gotd/td v0.143.0
|
||||||
github.com/johannesboyne/gofakes3 v0.0.0-20250916175020-ebf3e50324d3
|
github.com/johannesboyne/gofakes3 v0.0.0-20250916175020-ebf3e50324d3
|
||||||
github.com/krau/ffmpeg-go v0.6.0
|
github.com/krau/ffmpeg-go v0.6.0
|
||||||
github.com/lrstanley/go-ytdlp v1.3.2
|
github.com/lrstanley/go-ytdlp v1.3.5
|
||||||
github.com/minio/minio-go/v7 v7.0.98
|
github.com/minio/minio-go/v7 v7.0.100
|
||||||
github.com/playwright-community/playwright-go v0.5700.1
|
github.com/playwright-community/playwright-go v0.5700.1
|
||||||
github.com/rs/xid v1.6.0
|
github.com/rs/xid v1.6.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/unvgo/ghselfupdate v1.0.1
|
github.com/unvgo/ghselfupdate v1.0.1
|
||||||
github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c
|
github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c
|
||||||
golang.org/x/net v0.52.0
|
golang.org/x/net v0.53.0
|
||||||
golang.org/x/term v0.41.0
|
golang.org/x/term v0.42.0
|
||||||
golang.org/x/time v0.14.0
|
golang.org/x/time v0.15.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AnimeKaizoku/cacher v1.0.3 // indirect
|
github.com/AnimeKaizoku/cacher v1.0.3 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.4.0 // indirect
|
github.com/ProtonMail/go-crypto v1.4.1 // indirect
|
||||||
github.com/aws/smithy-go v1.24.0 // indirect
|
github.com/aws/smithy-go v1.24.0 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
github.com/charmbracelet/x/ansi v0.11.7 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
@@ -48,7 +48,7 @@ require (
|
|||||||
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
|
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
github.com/fatih/color v1.19.0 // indirect
|
||||||
github.com/ghodss/yaml v1.0.0 // indirect
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||||
github.com/go-faster/errors v0.7.1 // indirect
|
github.com/go-faster/errors v0.7.1 // indirect
|
||||||
@@ -56,14 +56,14 @@ require (
|
|||||||
github.com/go-faster/xor v1.0.0 // indirect
|
github.com/go-faster/xor v1.0.0 // indirect
|
||||||
github.com/go-faster/yaml v0.4.6 // indirect
|
github.com/go-faster/yaml v0.4.6 // indirect
|
||||||
github.com/go-ini/ini v1.67.0 // indirect
|
github.com/go-ini/ini v1.67.0 // indirect
|
||||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
|
||||||
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||||
github.com/go-stack/stack v1.8.1 // indirect
|
github.com/go-stack/stack v1.8.1 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/google/go-github/v30 v30.1.0 // indirect
|
github.com/google/go-github/v30 v30.1.0 // indirect
|
||||||
github.com/google/go-querystring v1.2.0 // indirect
|
github.com/google/go-querystring v1.2.0 // indirect
|
||||||
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect
|
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gotd/ige v0.2.2 // indirect
|
github.com/gotd/ige v0.2.2 // indirect
|
||||||
github.com/gotd/neo v0.1.5 // indirect
|
github.com/gotd/neo v0.1.5 // indirect
|
||||||
@@ -72,19 +72,20 @@ require (
|
|||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||||
github.com/minio/md5-simd v1.1.2 // indirect
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/ncruces/go-sqlite3-wasm v1.1.1-0.20260409221933-87e4b35a38d0 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/ncruces/julianday v1.0.0 // indirect
|
github.com/ncruces/julianday v1.0.0 // indirect
|
||||||
github.com/ogen-go/ogen v1.20.1 // indirect
|
github.com/ogen-go/ogen v1.20.3 // indirect
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
@@ -92,40 +93,39 @@ require (
|
|||||||
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
|
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
|
||||||
github.com/segmentio/asm v1.2.1 // indirect
|
github.com/segmentio/asm v1.2.1 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/tetratelabs/wazero v1.11.0 // indirect
|
|
||||||
github.com/tinylib/msgp v1.6.3 // indirect
|
github.com/tinylib/msgp v1.6.3 // indirect
|
||||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||||
go.opentelemetry.io/otel v1.41.0 // indirect
|
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.41.0 // indirect
|
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d // indirect
|
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.27.1 // indirect
|
go.uber.org/zap v1.27.1 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/crypto v0.49.0 // indirect
|
golang.org/x/crypto v0.50.0 // indirect
|
||||||
golang.org/x/mod v0.34.0 // indirect
|
golang.org/x/mod v0.35.0 // indirect
|
||||||
golang.org/x/tools v0.43.0 // indirect
|
golang.org/x/tools v0.44.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
modernc.org/libc v1.69.0 // indirect
|
modernc.org/libc v1.72.0 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
modernc.org/sqlite v1.46.1 // indirect
|
modernc.org/sqlite v1.48.2 // indirect
|
||||||
rsc.io/qr v0.2.0 // indirect
|
rsc.io/qr v0.2.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dgraph-io/ristretto/v2 v2.4.0
|
github.com/dgraph-io/ristretto/v2 v2.4.0
|
||||||
github.com/dop251/goja v0.0.0-20260226184354-913bd86fb70c
|
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c
|
||||||
github.com/duke-git/lancet/v2 v2.3.8
|
github.com/duke-git/lancet/v2 v2.3.9
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.4 // indirect
|
github.com/klauspost/compress v1.18.5 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/ncruces/go-sqlite3 v0.30.5
|
github.com/ncruces/go-sqlite3 v0.33.3 // indirect
|
||||||
github.com/ncruces/go-sqlite3/gormlite v0.30.2
|
github.com/ncruces/go-sqlite3/gormlite v0.33.3
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.6.1
|
github.com/nicksnyder/go-i18n/v2 v2.6.1
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
@@ -133,9 +133,9 @@ require (
|
|||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||||
golang.org/x/sync v0.20.0
|
golang.org/x/sync v0.20.0
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
golang.org/x/text v0.35.0
|
golang.org/x/text v0.36.0
|
||||||
gorm.io/gorm v1.31.1
|
gorm.io/gorm v1.31.1
|
||||||
)
|
)
|
||||||
|
|||||||
167
go.sum
167
go.sum
@@ -4,8 +4,8 @@ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk
|
|||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ=
|
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
|
||||||
github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
|
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
|
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
|
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
|
||||||
@@ -48,16 +48,16 @@ github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5f
|
|||||||
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||||
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||||
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
github.com/charmbracelet/log v1.0.0 h1:HVVVMmfOorfj3BA9i8X8UL69Hoz9lI0PYwXfJvOdRc4=
|
||||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
github.com/charmbracelet/log v1.0.0/go.mod h1:uYgY3SmLpwJWxmlrPwXvzVYujxis1vAKRV/0VQB7yWA=
|
||||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
||||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
@@ -82,16 +82,16 @@ github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa5
|
|||||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dop251/goja v0.0.0-20260226184354-913bd86fb70c h1:hIlkLbQ+tYoUqlG42LnxwGcohL5jaGqD8mGeJWavm8A=
|
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c h1:OcLmPfx1T1RmZVHHFwWMPaZDdRf0DBMZOFMVWJa7Pdk=
|
||||||
github.com/dop251/goja v0.0.0-20260226184354-913bd86fb70c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
||||||
github.com/duke-git/lancet/v2 v2.3.8 h1:dlkqn6Nj2LRWFuObNxttkMHxrFeaV6T26JR8jbEVbPg=
|
github.com/duke-git/lancet/v2 v2.3.9 h1:ZxUvfoEY7YbsGIeoXRxHWIkRCAt6VN7UBKWgCCqBB3U=
|
||||||
github.com/duke-git/lancet/v2 v2.3.8/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
|
github.com/duke-git/lancet/v2 v2.3.9/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
@@ -115,8 +115,8 @@ github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I=
|
|||||||
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
|
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
|
||||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
|
||||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||||
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
||||||
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
@@ -141,8 +141,8 @@ github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQF
|
|||||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||||
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||||
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||||
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw=
|
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg=
|
||||||
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gotd/contrib v0.21.1 h1:NSF+0YEnosQ34QEo2o4s6MA5YFDAor1LVvLhN1L3H1M=
|
github.com/gotd/contrib v0.21.1 h1:NSF+0YEnosQ34QEo2o4s6MA5YFDAor1LVvLhN1L3H1M=
|
||||||
@@ -151,10 +151,8 @@ github.com/gotd/ige v0.2.2 h1:XQ9dJZwBfDnOGSTxKXBGP4gMud3Qku2ekScRjDWWfEk=
|
|||||||
github.com/gotd/ige v0.2.2/go.mod h1:tuCRb+Y5Y3eNTo3ypIfNpQ4MFjrnONiL2jN2AKZXmb0=
|
github.com/gotd/ige v0.2.2/go.mod h1:tuCRb+Y5Y3eNTo3ypIfNpQ4MFjrnONiL2jN2AKZXmb0=
|
||||||
github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ=
|
github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ=
|
||||||
github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
|
github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
|
||||||
github.com/gotd/td v0.140.0 h1:trNBzTnhNtNwHsFp5qwKnNxQRAZJ6/BRE+uH3Lojauk=
|
github.com/gotd/td v0.143.0 h1:p0U/Nn92zXmAsahDn5CIVzay2kQ36lBBENT/FlWR2nQ=
|
||||||
github.com/gotd/td v0.140.0/go.mod h1:0ZkRxG7N+5ooG7/zdRXcnGautGPM6IKmyPQvdsAeF20=
|
github.com/gotd/td v0.143.0/go.mod h1:8GA5ecTI5iswLwBAlqf0u6/+j+BqSWUARSrX2Xk1usQ=
|
||||||
github.com/gotd/td v0.142.0 h1:hsH8zM7Pv98CkSMvrAEzVJurhntUziqKgf4VEofv5Zg=
|
|
||||||
github.com/gotd/td v0.142.0/go.mod h1:UHO5Gpwce9mH4zplp2qWo6AdzDjFVg7gK+ANMCztsi8=
|
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
|
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
|
||||||
@@ -167,8 +165,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
|||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/johannesboyne/gofakes3 v0.0.0-20250916175020-ebf3e50324d3 h1:2713fQZ560HxoNVgfJH41GKzjMjIG+DW4hH6nYXfXW8=
|
github.com/johannesboyne/gofakes3 v0.0.0-20250916175020-ebf3e50324d3 h1:2713fQZ560HxoNVgfJH41GKzjMjIG+DW4hH6nYXfXW8=
|
||||||
github.com/johannesboyne/gofakes3 v0.0.0-20250916175020-ebf3e50324d3/go.mod h1:S4S9jGBVlLri0OeqrSSbCGG5vsI6he06UJyuz1WT1EE=
|
github.com/johannesboyne/gofakes3 v0.0.0-20250916175020-ebf3e50324d3/go.mod h1:S4S9jGBVlLri0OeqrSSbCGG5vsI6he06UJyuz1WT1EE=
|
||||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
@@ -180,24 +178,24 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/krau/ffmpeg-go v0.6.0 h1:F4HWvOrKXQsfLsFTOnUfP0HY6WISJqOrsAFGSIzkKto=
|
github.com/krau/ffmpeg-go v0.6.0 h1:F4HWvOrKXQsfLsFTOnUfP0HY6WISJqOrsAFGSIzkKto=
|
||||||
github.com/krau/ffmpeg-go v0.6.0/go.mod h1:sa7/bWHB6fO9j4lhmxnWQ1U07o+dE1leFjhctotxU7A=
|
github.com/krau/ffmpeg-go v0.6.0/go.mod h1:sa7/bWHB6fO9j4lhmxnWQ1U07o+dE1leFjhctotxU7A=
|
||||||
github.com/lrstanley/go-ytdlp v1.3.2 h1:ktOav5X8+ZByuaQPFUF3uiPxofw0L5MoQtck6iIkWhI=
|
github.com/lrstanley/go-ytdlp v1.3.5 h1:eT+29mK3Lp+XPMQOH25+jVerrrjifYW1o3IkTYJ9SMs=
|
||||||
github.com/lrstanley/go-ytdlp v1.3.2/go.mod h1:VgjnTrvkTf+23JuySjyPq1iQ8ijSovBtTPpXH5XrLtI=
|
github.com/lrstanley/go-ytdlp v1.3.5/go.mod h1:VgjnTrvkTf+23JuySjyPq1iQ8ijSovBtTPpXH5XrLtI=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||||
github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0=
|
github.com/minio/minio-go/v7 v7.0.100 h1:ShkWi8Tyj9RtU57OQB2HIXKz4bFgtVib0bbT1sbtLI8=
|
||||||
github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=
|
github.com/minio/minio-go/v7 v7.0.100/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
@@ -206,20 +204,22 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
|
|||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/ncruces/go-sqlite3 v0.30.5 h1:6usmTQ6khriL8oWilkAZSJM/AIpAlVL2zFrlcpDldCE=
|
github.com/ncruces/go-sqlite3 v0.33.3 h1:6jCR3KuGvJSEwhaQrkHDGeIe2qCQ6nOUDNsPz7ZIotw=
|
||||||
github.com/ncruces/go-sqlite3 v0.30.5/go.mod h1:0I0JFflTKzfs3Ogfv8erP7CCoV/Z8uxigVDNOR0AQ5E=
|
github.com/ncruces/go-sqlite3 v0.33.3/go.mod h1:t2Osfw0wcKzJTgv2EvrkTtVLqlbKTA5Yvwb2ypAlBcY=
|
||||||
github.com/ncruces/go-sqlite3/gormlite v0.30.2 h1:FZ8mic14xTatssTkHCrelh9nPeFdXuzgMoNGkfuFbBU=
|
github.com/ncruces/go-sqlite3-wasm v1.1.1-0.20260409221933-87e4b35a38d0 h1:ymE9H30x1AyW5VfMNkJC9teuI2W1jjMsQS7kc6zl6Tg=
|
||||||
github.com/ncruces/go-sqlite3/gormlite v0.30.2/go.mod h1:W9WLBbqrrOIh2dqFZkeC/xKALG2LDIHY91jowahOdtI=
|
github.com/ncruces/go-sqlite3-wasm v1.1.1-0.20260409221933-87e4b35a38d0/go.mod h1:/H3+JykPsfSlvKbOxNSx9kKwm3ecqQGzyCs1e9KkNsU=
|
||||||
|
github.com/ncruces/go-sqlite3/gormlite v0.33.3 h1:JzLk8XymgvHvy60ib5MtNmd0fIYwGi7FUj2DpRFmnWQ=
|
||||||
|
github.com/ncruces/go-sqlite3/gormlite v0.33.3/go.mod h1:qDjzlaffXDGg5bhZs2VaaSY0Qb3rsiKq0O4pXkmQfHI=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
|
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
|
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
|
||||||
github.com/ogen-go/ogen v1.20.1 h1:AFpIeI2rS37TNIMRQTHhAkThICQpa1p+Pceu7HP7xsA=
|
github.com/ogen-go/ogen v1.20.3 h1:1tvJuJE0BnQ7Nukd6ykiTOP0ucfL0yrAjHUg3S1DCQk=
|
||||||
github.com/ogen-go/ogen v1.20.1/go.mod h1:eXQeqzIfw9qUjXdpqNtkX+XCvhlWNymqU1bm7S7y8iU=
|
github.com/ogen-go/ogen v1.20.3/go.mod h1:sJ1pJVp4S1RcSZlYIiMLo0QSMSt2pls4zfrc+hNKnzk=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
@@ -263,8 +263,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
|
|
||||||
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
|
|
||||||
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
||||||
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||||
@@ -280,12 +278,12 @@ go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
|||||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
|
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||||
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d h1:Ns9kd1Rwzw7t0BR8XMphenji4SmIoNZPn8zhYmaVKP8=
|
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d h1:Ns9kd1Rwzw7t0BR8XMphenji4SmIoNZPn8zhYmaVKP8=
|
||||||
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d/go.mod h1:92Uoe3l++MlthCm+koNi0tcUCX3anayogF0Pa/sp24k=
|
go.shabbyrobe.org/gocovmerge v0.0.0-20230507111327-fa4f82cfbf4d/go.mod h1:92Uoe3l++MlthCm+koNi0tcUCX3anayogF0Pa/sp24k=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
@@ -301,35 +299,27 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
|||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
|
||||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
|
||||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -339,42 +329,33 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
|
||||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
|
||||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@@ -390,10 +371,10 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
||||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
||||||
modernc.org/ccgo/v4 v4.31.0 h1:/bsaxqdgX3gy/0DboxcvWrc3NpzH+6wpFfI/ZaA/hrg=
|
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
||||||
modernc.org/ccgo/v4 v4.31.0/go.mod h1:jKe8kPBjIN/VdGTVqARTQ8N1gAziBmiISY8j5HoKwjg=
|
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
|
||||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
@@ -402,8 +383,8 @@ modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
|||||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.69.0 h1:YQJ5QMSReTgQ3QFmI0dudfjXIjCcYTUxcH8/9P9f0D8=
|
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
|
||||||
modernc.org/libc v1.69.0/go.mod h1:YfLLduUEbodNV2xLU5JOnRHBTAHVHsVW3bVYGw0ZCV4=
|
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
@@ -412,8 +393,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c=
|
||||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package ctxkey
|
package ctxkey
|
||||||
|
|
||||||
// ENUM(content-length)
|
// ENUM(content-length, overwrite-existing)
|
||||||
//
|
//
|
||||||
//go:generate go-enum --values --names --flag --nocase --noprefix
|
//go:generate go-enum --values --names --flag --nocase --noprefix
|
||||||
type ContextKey string
|
type ContextKey string
|
||||||
|
|||||||
@@ -14,12 +14,15 @@ import (
|
|||||||
const (
|
const (
|
||||||
// ContentLength is a ContextKey of type content-length.
|
// ContentLength is a ContextKey of type content-length.
|
||||||
ContentLength ContextKey = "content-length"
|
ContentLength ContextKey = "content-length"
|
||||||
|
// OverwriteExisting is a ContextKey of type overwrite-existing.
|
||||||
|
OverwriteExisting ContextKey = "overwrite-existing"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrInvalidContextKey = fmt.Errorf("not a valid ContextKey, try [%s]", strings.Join(_ContextKeyNames, ", "))
|
var ErrInvalidContextKey = fmt.Errorf("not a valid ContextKey, try [%s]", strings.Join(_ContextKeyNames, ", "))
|
||||||
|
|
||||||
var _ContextKeyNames = []string{
|
var _ContextKeyNames = []string{
|
||||||
string(ContentLength),
|
string(ContentLength),
|
||||||
|
string(OverwriteExisting),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContextKeyNames returns a list of possible string values of ContextKey.
|
// ContextKeyNames returns a list of possible string values of ContextKey.
|
||||||
@@ -33,6 +36,7 @@ func ContextKeyNames() []string {
|
|||||||
func ContextKeyValues() []ContextKey {
|
func ContextKeyValues() []ContextKey {
|
||||||
return []ContextKey{
|
return []ContextKey{
|
||||||
ContentLength,
|
ContentLength,
|
||||||
|
OverwriteExisting,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +53,8 @@ func (x ContextKey) IsValid() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var _ContextKeyValue = map[string]ContextKey{
|
var _ContextKeyValue = map[string]ContextKey{
|
||||||
"content-length": ContentLength,
|
"content-length": ContentLength,
|
||||||
|
"overwrite-existing": OverwriteExisting,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseContextKey attempts to convert a string to a ContextKey.
|
// ParseContextKey attempts to convert a string to a ContextKey.
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ func (c *Client) Put(ctx context.Context, key string, r io.Reader, size int64) e
|
|||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode >= 300 {
|
if resp.StatusCode >= 300 {
|
||||||
return fmt.Errorf("put object failed: %s", resp.Status)
|
return responseError("put object", resp)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -170,10 +170,21 @@ func signRequest(req *http.Request, region, accessKey, secretKey string, payload
|
|||||||
req.Header.Set("x-amz-date", amzDate)
|
req.Header.Set("x-amz-date", amzDate)
|
||||||
req.Header.Set("x-amz-content-sha256", payloadHash)
|
req.Header.Set("x-amz-content-sha256", payloadHash)
|
||||||
|
|
||||||
// Canonical headers
|
// Canonical headers. Host is required by SigV4, but Go stores it on
|
||||||
var headers []string
|
// Request.Host/URL.Host rather than in Request.Header.
|
||||||
|
headerValues := map[string]string{
|
||||||
|
"host": req.URL.Host,
|
||||||
|
}
|
||||||
|
if req.Host != "" {
|
||||||
|
headerValues["host"] = req.Host
|
||||||
|
}
|
||||||
for k := range req.Header {
|
for k := range req.Header {
|
||||||
headers = append(headers, strings.ToLower(k))
|
headerValues[strings.ToLower(k)] = strings.TrimSpace(req.Header.Get(k))
|
||||||
|
}
|
||||||
|
|
||||||
|
var headers []string
|
||||||
|
for k := range headerValues {
|
||||||
|
headers = append(headers, k)
|
||||||
}
|
}
|
||||||
sort.Strings(headers)
|
sort.Strings(headers)
|
||||||
|
|
||||||
@@ -181,7 +192,7 @@ func signRequest(req *http.Request, region, accessKey, secretKey string, payload
|
|||||||
for _, k := range headers {
|
for _, k := range headers {
|
||||||
canonicalHeaders.WriteString(k)
|
canonicalHeaders.WriteString(k)
|
||||||
canonicalHeaders.WriteString(":")
|
canonicalHeaders.WriteString(":")
|
||||||
canonicalHeaders.WriteString(strings.TrimSpace(req.Header.Get(k)))
|
canonicalHeaders.WriteString(headerValues[k])
|
||||||
canonicalHeaders.WriteString("\n")
|
canonicalHeaders.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +200,7 @@ func signRequest(req *http.Request, region, accessKey, secretKey string, payload
|
|||||||
|
|
||||||
canonicalRequest := strings.Join([]string{
|
canonicalRequest := strings.Join([]string{
|
||||||
req.Method,
|
req.Method,
|
||||||
req.URL.EscapedPath(),
|
canonicalURI(req.URL.Path),
|
||||||
req.URL.RawQuery,
|
req.URL.RawQuery,
|
||||||
canonicalHeaders.String(),
|
canonicalHeaders.String(),
|
||||||
signedHeaders,
|
signedHeaders,
|
||||||
@@ -219,3 +230,54 @@ func signRequest(req *http.Request, region, accessKey, secretKey string, payload
|
|||||||
req.Header.Set("Authorization", auth)
|
req.Header.Set("Authorization", auth)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func responseError(operation string, resp *http.Response) error {
|
||||||
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s failed: %s", operation, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
message := strings.TrimSpace(string(body))
|
||||||
|
if message == "" {
|
||||||
|
return fmt.Errorf("%s failed: %s", operation, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%s failed: %s: %s", operation, resp.Status, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalURI(path string) string {
|
||||||
|
if path == "" {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
for i := 0; i < len(path); i++ {
|
||||||
|
c := path[i]
|
||||||
|
if shouldEscapePathByte(c) {
|
||||||
|
b.WriteByte('%')
|
||||||
|
b.WriteByte("0123456789ABCDEF"[c>>4])
|
||||||
|
b.WriteByte("0123456789ABCDEF"[c&15])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.WriteByte(c)
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldEscapePathByte(c byte) bool {
|
||||||
|
if c >= 'A' && c <= 'Z' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if c >= 'a' && c <= 'z' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if c >= '0' && c <= '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch c {
|
||||||
|
case '-', '.', '_', '~', '/':
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,31 @@ const (
|
|||||||
TypeCancel = "cancel"
|
TypeCancel = "cancel"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConflictStrategyRename = "rename"
|
||||||
|
ConflictStrategyAsk = "ask"
|
||||||
|
ConflictStrategyOverwrite = "overwrite"
|
||||||
|
ConflictStrategySkip = "skip"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ConflictStrategyValues() []string {
|
||||||
|
return []string{
|
||||||
|
ConflictStrategyRename,
|
||||||
|
ConflictStrategyAsk,
|
||||||
|
ConflictStrategyOverwrite,
|
||||||
|
ConflictStrategySkip,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsConflictStrategy(strategy string) bool {
|
||||||
|
for _, value := range ConflictStrategyValues() {
|
||||||
|
if strategy == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// type TaskDataTGFiles struct {
|
// type TaskDataTGFiles struct {
|
||||||
// Files []tfile.TGFileMessage
|
// Files []tfile.TGFileMessage
|
||||||
// AsBatch bool
|
// AsBatch bool
|
||||||
@@ -34,6 +59,8 @@ type Add struct {
|
|||||||
SelectedStorName string
|
SelectedStorName string
|
||||||
DirID uint
|
DirID uint
|
||||||
SettedDir bool
|
SettedDir bool
|
||||||
|
SelectedDirPath string
|
||||||
|
ConflictStrategy string
|
||||||
// tfiles
|
// tfiles
|
||||||
Files []tfile.TGFileMessage
|
Files []tfile.TGFileMessage
|
||||||
AsBatch bool
|
AsBatch bool
|
||||||
|
|||||||
@@ -108,8 +108,10 @@ func (a *Alist) Save(ctx context.Context, reader io.Reader, storagePath string)
|
|||||||
ext := path.Ext(storagePath)
|
ext := path.Ext(storagePath)
|
||||||
base := strings.TrimSuffix(storagePath, ext)
|
base := strings.TrimSuffix(storagePath, ext)
|
||||||
candidate := storagePath
|
candidate := storagePath
|
||||||
for i := 1; a.Exists(ctx, candidate); i++ {
|
if overwrite, _ := ctx.Value(ctxkey.OverwriteExisting).(bool); !overwrite {
|
||||||
candidate = fmt.Sprintf("%s_%d%s", base, i, ext)
|
for i := 1; a.existsPath(ctx, candidate); i++ {
|
||||||
|
candidate = fmt.Sprintf("%s_%d%s", base, i, ext)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, a.baseURL+"/api/fs/put", reader)
|
req, err := http.NewRequestWithContext(ctx, http.MethodPut, a.baseURL+"/api/fs/put", reader)
|
||||||
@@ -158,6 +160,10 @@ func (a *Alist) JoinStoragePath(p string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Alist) Exists(ctx context.Context, storagePath string) bool {
|
func (a *Alist) Exists(ctx context.Context, storagePath string) bool {
|
||||||
|
return a.existsPath(ctx, a.JoinStoragePath(storagePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Alist) existsPath(ctx context.Context, storagePath string) bool {
|
||||||
// POST /api/fs/get
|
// POST /api/fs/get
|
||||||
/*
|
/*
|
||||||
body:
|
body:
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/enums/ctxkey"
|
||||||
|
)
|
||||||
|
|
||||||
type contextKey struct{}
|
type contextKey struct{}
|
||||||
|
|
||||||
@@ -20,3 +24,7 @@ func FromContext(ctx context.Context) Storage {
|
|||||||
}
|
}
|
||||||
return storage
|
return storage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithOverwrite(ctx context.Context) context.Context {
|
||||||
|
return context.WithValue(ctx, ctxkey.OverwriteExisting, true)
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
"github.com/duke-git/lancet/v2/fileutil"
|
"github.com/duke-git/lancet/v2/fileutil"
|
||||||
config "github.com/krau/SaveAny-Bot/config/storage"
|
config "github.com/krau/SaveAny-Bot/config/storage"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/enums/ctxkey"
|
||||||
storenum "github.com/krau/SaveAny-Bot/pkg/enums/storage"
|
storenum "github.com/krau/SaveAny-Bot/pkg/enums/storage"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/storagetypes"
|
"github.com/krau/SaveAny-Bot/pkg/storagetypes"
|
||||||
)
|
)
|
||||||
@@ -56,8 +57,10 @@ func (l *Local) Save(ctx context.Context, r io.Reader, storagePath string) error
|
|||||||
ext := filepath.Ext(storagePath)
|
ext := filepath.Ext(storagePath)
|
||||||
base := strings.TrimSuffix(storagePath, ext)
|
base := strings.TrimSuffix(storagePath, ext)
|
||||||
candidate := storagePath
|
candidate := storagePath
|
||||||
for i := 1; l.Exists(ctx, candidate); i++ {
|
if overwrite, _ := ctx.Value(ctxkey.OverwriteExisting).(bool); !overwrite {
|
||||||
candidate = fmt.Sprintf("%s_%d%s", base, i, ext)
|
for i := 1; l.existsPath(candidate); i++ {
|
||||||
|
candidate = fmt.Sprintf("%s_%d%s", base, i, ext)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
absPath, err := filepath.Abs(candidate)
|
absPath, err := filepath.Abs(candidate)
|
||||||
@@ -77,6 +80,10 @@ func (l *Local) Save(ctx context.Context, r io.Reader, storagePath string) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Local) Exists(ctx context.Context, storagePath string) bool {
|
func (l *Local) Exists(ctx context.Context, storagePath string) bool {
|
||||||
|
return l.existsPath(l.JoinStoragePath(storagePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Local) existsPath(storagePath string) bool {
|
||||||
absPath, err := filepath.Abs(storagePath)
|
absPath, err := filepath.Abs(storagePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -81,12 +81,14 @@ func (m *Minio) Save(ctx context.Context, r io.Reader, storagePath string) error
|
|||||||
ext := path.Ext(storagePath)
|
ext := path.Ext(storagePath)
|
||||||
base := strings.TrimSuffix(storagePath, ext)
|
base := strings.TrimSuffix(storagePath, ext)
|
||||||
candidate := storagePath
|
candidate := storagePath
|
||||||
for i := 1; m.Exists(ctx, candidate); i++ {
|
if overwrite, _ := ctx.Value(ctxkey.OverwriteExisting).(bool); !overwrite {
|
||||||
candidate = fmt.Sprintf("%s_%d%s", base, i, ext)
|
for i := 1; m.existsObject(ctx, candidate); i++ {
|
||||||
if i > 10 {
|
candidate = fmt.Sprintf("%s_%d%s", base, i, ext)
|
||||||
m.logger.Errorf("Too many attempts to find a unique filename for %s", storagePath)
|
if i > 10 {
|
||||||
candidate = fmt.Sprintf("%s_%s%s", base, xid.New().String(), ext)
|
m.logger.Errorf("Too many attempts to find a unique filename for %s", storagePath)
|
||||||
break
|
candidate = fmt.Sprintf("%s_%s%s", base, xid.New().String(), ext)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
size := int64(-1)
|
size := int64(-1)
|
||||||
@@ -106,6 +108,10 @@ func (m *Minio) Save(ctx context.Context, r io.Reader, storagePath string) error
|
|||||||
|
|
||||||
func (m *Minio) Exists(ctx context.Context, storagePath string) bool {
|
func (m *Minio) Exists(ctx context.Context, storagePath string) bool {
|
||||||
m.logger.Debugf("Checking if file exists at %s", storagePath)
|
m.logger.Debugf("Checking if file exists at %s", storagePath)
|
||||||
|
return m.existsObject(ctx, m.JoinStoragePath(storagePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Minio) existsObject(ctx context.Context, storagePath string) bool {
|
||||||
_, err := m.client.StatObject(ctx, m.config.BucketName, storagePath, minio.StatObjectOptions{})
|
_, err := m.client.StatObject(ctx, m.config.BucketName, storagePath, minio.StatObjectOptions{})
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
config "github.com/krau/SaveAny-Bot/config/storage"
|
config "github.com/krau/SaveAny-Bot/config/storage"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/enums/ctxkey"
|
||||||
storenum "github.com/krau/SaveAny-Bot/pkg/enums/storage"
|
storenum "github.com/krau/SaveAny-Bot/pkg/enums/storage"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/storagetypes"
|
"github.com/krau/SaveAny-Bot/pkg/storagetypes"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
@@ -107,12 +108,14 @@ func (r *Rclone) Save(ctx context.Context, reader io.Reader, storagePath string)
|
|||||||
ext := path.Ext(storagePath)
|
ext := path.Ext(storagePath)
|
||||||
base := strings.TrimSuffix(storagePath, ext)
|
base := strings.TrimSuffix(storagePath, ext)
|
||||||
candidate := storagePath
|
candidate := storagePath
|
||||||
for i := 1; r.Exists(ctx, candidate); i++ {
|
if overwrite, _ := ctx.Value(ctxkey.OverwriteExisting).(bool); !overwrite {
|
||||||
candidate = fmt.Sprintf("%s_%d%s", base, i, ext)
|
for i := 1; r.Exists(ctx, candidate); i++ {
|
||||||
if i > 100 {
|
candidate = fmt.Sprintf("%s_%d%s", base, i, ext)
|
||||||
r.logger.Errorf("Too many attempts to find a unique filename for %s", storagePath)
|
if i > 100 {
|
||||||
candidate = fmt.Sprintf("%s_%s%s", base, xid.New().String(), ext)
|
r.logger.Errorf("Too many attempts to find a unique filename for %s", storagePath)
|
||||||
break
|
candidate = fmt.Sprintf("%s_%s%s", base, xid.New().String(), ext)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,13 +70,15 @@ func (m *S3) Save(ctx context.Context, r io.Reader, storagePath string) error {
|
|||||||
base := strings.TrimSuffix(storagePath, ext)
|
base := strings.TrimSuffix(storagePath, ext)
|
||||||
candidate := storagePath
|
candidate := storagePath
|
||||||
|
|
||||||
// Unique filename
|
if overwrite, _ := ctx.Value(ctxkey.OverwriteExisting).(bool); !overwrite {
|
||||||
for i := 1; m.Exists(ctx, candidate); i++ {
|
// Unique filename
|
||||||
candidate = fmt.Sprintf("%s_%d%s", base, i, ext)
|
for i := 1; m.existsKey(ctx, candidate); i++ {
|
||||||
if i > 10 {
|
candidate = fmt.Sprintf("%s_%d%s", base, i, ext)
|
||||||
m.logger.Errorf("Too many attempts for unique filename: %s", storagePath)
|
if i > 10 {
|
||||||
candidate = fmt.Sprintf("%s_%s%s", base, xid.New().String(), ext)
|
m.logger.Errorf("Too many attempts for unique filename: %s", storagePath)
|
||||||
break
|
candidate = fmt.Sprintf("%s_%s%s", base, xid.New().String(), ext)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,5 +101,9 @@ func (m *S3) Save(ctx context.Context, r io.Reader, storagePath string) error {
|
|||||||
func (m *S3) Exists(ctx context.Context, storagePath string) bool {
|
func (m *S3) Exists(ctx context.Context, storagePath string) bool {
|
||||||
m.logger.Debugf("Checking if file exists at %s", storagePath)
|
m.logger.Debugf("Checking if file exists at %s", storagePath)
|
||||||
|
|
||||||
return m.client.Exists(ctx, storagePath)
|
return m.existsKey(ctx, m.JoinStoragePath(storagePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *S3) existsKey(ctx context.Context, key string) bool {
|
||||||
|
return m.client.Exists(ctx, key)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
config "github.com/krau/SaveAny-Bot/config/storage"
|
config "github.com/krau/SaveAny-Bot/config/storage"
|
||||||
|
"github.com/krau/SaveAny-Bot/pkg/enums/ctxkey"
|
||||||
storenum "github.com/krau/SaveAny-Bot/pkg/enums/storage"
|
storenum "github.com/krau/SaveAny-Bot/pkg/enums/storage"
|
||||||
"github.com/krau/SaveAny-Bot/pkg/storagetypes"
|
"github.com/krau/SaveAny-Bot/pkg/storagetypes"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
@@ -57,12 +58,14 @@ func (w *Webdav) Save(ctx context.Context, r io.Reader, storagePath string) erro
|
|||||||
ext := path.Ext(storagePath)
|
ext := path.Ext(storagePath)
|
||||||
base := strings.TrimSuffix(storagePath, ext)
|
base := strings.TrimSuffix(storagePath, ext)
|
||||||
candidate := storagePath
|
candidate := storagePath
|
||||||
for i := 1; w.Exists(ctx, candidate); i++ {
|
if overwrite, _ := ctx.Value(ctxkey.OverwriteExisting).(bool); !overwrite {
|
||||||
candidate = fmt.Sprintf("%s_%d%s", base, i, ext)
|
for i := 1; w.existsPath(ctx, candidate); i++ {
|
||||||
if i > 1000 {
|
candidate = fmt.Sprintf("%s_%d%s", base, i, ext)
|
||||||
w.logger.Errorf("Too many attempts to find a unique filename for %s", storagePath)
|
if i > 1000 {
|
||||||
candidate = fmt.Sprintf("%s_%s%s", base, xid.New().String(), ext)
|
w.logger.Errorf("Too many attempts to find a unique filename for %s", storagePath)
|
||||||
break
|
candidate = fmt.Sprintf("%s_%s%s", base, xid.New().String(), ext)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +82,10 @@ func (w *Webdav) Save(ctx context.Context, r io.Reader, storagePath string) erro
|
|||||||
|
|
||||||
func (w *Webdav) Exists(ctx context.Context, storagePath string) bool {
|
func (w *Webdav) Exists(ctx context.Context, storagePath string) bool {
|
||||||
w.logger.Debugf("Checking if file exists at %s", storagePath)
|
w.logger.Debugf("Checking if file exists at %s", storagePath)
|
||||||
|
return w.existsPath(ctx, w.JoinStoragePath(storagePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Webdav) existsPath(ctx context.Context, storagePath string) bool {
|
||||||
exists, err := w.client.Exists(ctx, storagePath)
|
exists, err := w.client.Exists(ctx, storagePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.logger.Errorf("Failed to check if file exists at %s: %v", storagePath, err)
|
w.logger.Errorf("Failed to check if file exists at %s: %v", storagePath, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user