Compare commits

..

6 Commits

Author SHA1 Message Date
krau
82e1efb518 refactor: update logging messages for transfer task execution and progress tracking 2026-01-19 21:34:57 +08:00
krau
9b52a3e0ce refactor: simplify storage path handling across various tasks and storage implementations 2026-01-19 21:27:53 +08:00
krau
6990543c9f feat: update transfer command to remove target path requirement and adjust usage instructions 2026-01-19 21:20:27 +08:00
krau
dd0dea8cb5 feat!: Refactor batch import task to transfer task
- Updated command usage in English and Simplified Chinese localization files to reflect changes in transfer command syntax.
- Removed batch import task implementation, replacing it with a new transfer task implementation.
- Introduced new task structure and progress tracking for file transfers.
- Updated task type enumeration to replace batch import with transfer.
- Added new fields in data structures to support transfer operations.
- Implemented file handling and progress reporting for the transfer task.
2026-01-19 21:14:01 +08:00
krau
3d20fbd0fe feat: implement transfer command for file transfers between storages 2026-01-19 21:01:50 +08:00
krau
e6d8cc775a feat: add yt-dlp to Dockerfile for enhanced media handling 2026-01-19 20:40:00 +08:00
30 changed files with 429 additions and 328 deletions

View File

@@ -26,7 +26,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
FROM alpine:latest FROM alpine:latest
RUN apk add --no-cache curl ffmpeg RUN apk add --no-cache curl ffmpeg yt-dlp
WORKDIR /app WORKDIR /app

View File

@@ -101,6 +101,8 @@ func handleAddCallback(ctx *ext.Context, update *ext.Update) error {
shortcut.CreateAndAddAria2TaskWithEdit(ctx, selectedStorage, dirPath, data.Aria2URIs, client, msgID, userID) shortcut.CreateAndAddAria2TaskWithEdit(ctx, selectedStorage, dirPath, data.Aria2URIs, client, msgID, userID)
case tasktype.TaskTypeYtdlp: case tasktype.TaskTypeYtdlp:
shortcut.CreateAndAddYtdlpTaskWithEdit(ctx, selectedStorage, dirPath, data.YtdlpURLs, data.YtdlpFlags, msgID, userID) shortcut.CreateAndAddYtdlpTaskWithEdit(ctx, selectedStorage, dirPath, data.YtdlpURLs, data.YtdlpFlags, msgID, userID)
case tasktype.TaskTypeTransfer:
return handleTransferCallback(ctx, userID, selectedStorage, dirPath, data, msgID)
default: default:
return fmt.Errorf("unexcept task type: %s", data.TaskType) return fmt.Errorf("unexcept task type: %s", data.TaskType)
} }

View File

@@ -1,182 +0,0 @@
package handlers
import (
"fmt"
"regexp"
"github.com/celestix/gotgproto/dispatcher"
"github.com/celestix/gotgproto/ext"
"github.com/charmbracelet/log"
"github.com/gotd/td/tg"
"github.com/krau/SaveAny-Bot/common/i18n"
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
"github.com/krau/SaveAny-Bot/common/utils/strutil"
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
"github.com/krau/SaveAny-Bot/config"
storconfig "github.com/krau/SaveAny-Bot/config/storage"
"github.com/krau/SaveAny-Bot/core"
"github.com/krau/SaveAny-Bot/core/tasks/batchimport"
"github.com/krau/SaveAny-Bot/pkg/storagetypes"
"github.com/krau/SaveAny-Bot/storage"
"github.com/rs/xid"
)
func handleImportCmd(ctx *ext.Context, update *ext.Update) error {
logger := log.FromContext(ctx)
args := strutil.ParseArgsRespectQuotes(update.EffectiveMessage.Text)
if len(args) < 3 {
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgImportUsage, nil)), nil)
return dispatcher.EndGroups
}
storageName := args[1]
dirPath := args[2]
userID := update.GetUserChat().GetID()
stor, err := storage.GetStorageByUserIDAndName(ctx, userID, storageName)
if err != nil {
logger.Errorf("Failed to get storage by user ID and name: %s", err)
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgImportErrorStorageNotFound, map[string]any{
"StorageName": storageName,
"Error": err,
})), nil)
return dispatcher.EndGroups
}
listable, ok := stor.(storage.StorageListable)
if !ok {
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgImportErrorStorageNotListable, map[string]any{
"StorageName": storageName,
})), nil)
return dispatcher.EndGroups
}
_, ok = stor.(storage.StorageReadable)
if !ok {
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgImportErrorStorageNotReadable, map[string]any{
"StorageName": storageName,
})), nil)
return dispatcher.EndGroups
}
telegramStorage, err := storage.GetTelegramStorageByUserID(ctx, userID)
if err != nil {
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgImportErrorNoTelegramStorage, map[string]any{
"Error": err,
})), nil)
return dispatcher.EndGroups
}
replied, err := ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgImportInfoFetchingFiles, nil)), nil)
if err != nil {
logger.Errorf("Failed to reply: %s", err)
return dispatcher.EndGroups
}
files, err := listable.ListFiles(ctx, dirPath)
if err != nil {
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
ID: replied.ID,
Message: i18n.T(i18nk.BotMsgImportErrorListFilesFailed, map[string]any{"Error": err}),
})
return dispatcher.EndGroups
}
var filter *regexp.Regexp
if len(args) >= 5 {
filter, err = regexp.Compile(args[4])
if err != nil {
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
ID: replied.ID,
Message: i18n.T(i18nk.BotMsgImportErrorInvalidRegex, map[string]any{"Error": err}),
})
return dispatcher.EndGroups
}
}
filteredFiles := make([]storagetypes.FileInfo, 0)
for _, file := range files {
if file.IsDir {
continue
}
if filter != nil && !filter.MatchString(file.Name) {
continue
}
filteredFiles = append(filteredFiles, file)
}
if len(filteredFiles) == 0 {
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
ID: replied.ID,
Message: i18n.T(i18nk.BotMsgImportErrorNoFilesToImport, nil),
})
return dispatcher.EndGroups
}
// Get default chat_id from Telegram storage config
targetChatID := int64(0)
if telegramCfg := config.C().GetStorageByName(telegramStorage.Name()); telegramCfg != nil {
if tgCfg, ok := telegramCfg.(*storconfig.TelegramStorageConfig); ok {
targetChatID = tgCfg.ChatID
}
}
if len(args) >= 4 {
parsedChatID, err := tgutil.ParseChatID(ctx, args[3])
if err != nil {
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
ID: replied.ID,
Message: i18n.T(i18nk.BotMsgImportErrorInvalidChatId, map[string]any{"Error": err}),
})
return dispatcher.EndGroups
}
targetChatID = parsedChatID
}
if targetChatID == 0 {
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
ID: replied.ID,
Message: i18n.T(i18nk.BotMsgImportErrorNoTargetChatId, nil),
})
return dispatcher.EndGroups
}
elems := make([]batchimport.TaskElement, 0, len(filteredFiles))
var totalSize int64
for _, file := range filteredFiles {
elem := batchimport.NewTaskElement(stor, file, telegramStorage, targetChatID)
elems = append(elems, *elem)
totalSize += file.Size
}
taskID := xid.New().String()
injectCtx := tgutil.ExtWithContext(ctx.Context, ctx)
task := batchimport.NewBatchImportTask(
taskID,
injectCtx,
elems,
batchimport.NewProgressTracker(replied.ID, userID),
true, // IgnoreErrors
)
if err := core.AddTask(injectCtx, task); err != nil {
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
ID: replied.ID,
Message: i18n.T(i18nk.BotMsgImportErrorAddTaskFailed, map[string]any{"Error": err}),
})
return dispatcher.EndGroups
}
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
ID: replied.ID,
Message: i18n.T(i18nk.BotMsgImportInfoTaskAdded, map[string]any{
"Count": len(elems),
"SizeMB": fmt.Sprintf("%.2f", float64(totalSize)/(1024*1024)),
"TaskID": taskID,
}),
})
return dispatcher.EndGroups
}

View File

@@ -31,7 +31,7 @@ var CommandHandlers = []DescCommandHandler{
{"dl", i18nk.BotMsgCmdDl, handleDlCmd}, {"dl", i18nk.BotMsgCmdDl, handleDlCmd},
{"aria2dl", i18nk.BotMsgCmdAria2dl, handleAria2DlCmd}, {"aria2dl", i18nk.BotMsgCmdAria2dl, handleAria2DlCmd},
{"ytdlp", i18nk.BotMsgCmdYtdlp, handleYtdlpCmd}, {"ytdlp", i18nk.BotMsgCmdYtdlp, handleYtdlpCmd},
{"import", i18nk.BotMsgCmdImport, handleImportCmd}, {"transfer", i18nk.BotMsgCmdTransfer, handleTransferCmd},
{"task", i18nk.BotMsgCmdTask, handleTaskCmd}, {"task", i18nk.BotMsgCmdTask, handleTaskCmd},
{"cancel", i18nk.BotMsgCmdCancel, handleCancelCmd}, {"cancel", i18nk.BotMsgCmdCancel, handleCancelCmd},
{"config", i18nk.BotMsgCmdConfig, handleConfigCmd}, {"config", i18nk.BotMsgCmdConfig, handleConfigCmd},

View File

@@ -0,0 +1,257 @@
package handlers
import (
"fmt"
"regexp"
"strings"
"github.com/celestix/gotgproto/dispatcher"
"github.com/celestix/gotgproto/ext"
"github.com/charmbracelet/log"
"github.com/gotd/td/tg"
"github.com/krau/SaveAny-Bot/client/bot/handlers/utils/msgelem"
"github.com/krau/SaveAny-Bot/common/i18n"
"github.com/krau/SaveAny-Bot/common/i18n/i18nk"
"github.com/krau/SaveAny-Bot/common/utils/strutil"
"github.com/krau/SaveAny-Bot/common/utils/tgutil"
"github.com/krau/SaveAny-Bot/core"
"github.com/krau/SaveAny-Bot/core/tasks/transfer"
"github.com/krau/SaveAny-Bot/pkg/enums/tasktype"
"github.com/krau/SaveAny-Bot/pkg/storagetypes"
"github.com/krau/SaveAny-Bot/pkg/tcbdata"
"github.com/krau/SaveAny-Bot/storage"
"github.com/rs/xid"
)
func handleTransferCmd(ctx *ext.Context, update *ext.Update) error {
logger := log.FromContext(ctx)
args := strutil.ParseArgsRespectQuotes(update.EffectiveMessage.Text)
if len(args) < 2 {
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgTransferUsage, nil)), nil)
return dispatcher.EndGroups
}
// Parse source: storage_name:/path
sourceParts := strings.SplitN(args[1], ":", 2)
if len(sourceParts) != 2 {
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgTransferErrorInvalidSource, nil)), nil)
return dispatcher.EndGroups
}
sourceStorageName := sourceParts[0]
sourcePath := sourceParts[1]
userID := update.GetUserChat().GetID()
// Get source storage
sourceStorage, err := storage.GetStorageByUserIDAndName(ctx, userID, sourceStorageName)
if err != nil {
logger.Errorf("Failed to get source storage by user ID and name: %s", err)
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgTransferErrorStorageNotFound, map[string]any{
"StorageName": sourceStorageName,
"Error": err,
})), nil)
return dispatcher.EndGroups
}
// Check if source storage supports listing
listable, ok := sourceStorage.(storage.StorageListable)
if !ok {
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgTransferErrorStorageNotListable, map[string]any{
"StorageName": sourceStorageName,
})), nil)
return dispatcher.EndGroups
}
// Check if source storage supports reading
_, ok = sourceStorage.(storage.StorageReadable)
if !ok {
ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgTransferErrorStorageNotReadable, map[string]any{
"StorageName": sourceStorageName,
})), nil)
return dispatcher.EndGroups
}
// Fetch file list
replied, err := ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgTransferInfoFetchingFiles, nil)), nil)
if err != nil {
logger.Errorf("Failed to reply: %s", err)
return dispatcher.EndGroups
}
files, err := listable.ListFiles(ctx, sourcePath)
if err != nil {
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
ID: replied.ID,
Message: i18n.T(i18nk.BotMsgTransferErrorListFilesFailed, map[string]any{"Error": err}),
})
return dispatcher.EndGroups
}
// Optional filter
var filter *regexp.Regexp
if len(args) >= 3 {
filter, err = regexp.Compile(args[2])
if err != nil {
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
ID: replied.ID,
Message: i18n.T(i18nk.BotMsgTransferErrorInvalidRegex, map[string]any{"Error": err}),
})
return dispatcher.EndGroups
}
}
// Filter files
filteredFiles := make([]storagetypes.FileInfo, 0)
for _, file := range files {
if file.IsDir {
continue
}
if filter != nil && !filter.MatchString(file.Name) {
continue
}
filteredFiles = append(filteredFiles, file)
}
if len(filteredFiles) == 0 {
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
ID: replied.ID,
Message: i18n.T(i18nk.BotMsgTransferErrorNoFilesToTransfer, nil),
})
return dispatcher.EndGroups
}
// Prepare file paths for callback data
filePaths := make([]string, 0, len(filteredFiles))
var totalSize int64
for _, file := range filteredFiles {
filePaths = append(filePaths, file.Path)
totalSize += file.Size
}
// Build storage selection keyboard
markup, err := msgelem.BuildAddSelectStorageKeyboard(storage.GetUserStorages(ctx, userID), tcbdata.Add{
TaskType: tasktype.TaskTypeTransfer,
TransferSourceStorName: sourceStorageName,
TransferSourcePath: sourcePath,
TransferFiles: filePaths,
})
if err != nil {
logger.Errorf("Failed to build storage selection keyboard: %s", err)
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
ID: replied.ID,
Message: i18n.T(i18nk.BotMsgTransferErrorBuildStorageSelectKeyboardFailed, map[string]any{"Error": err}),
})
return dispatcher.EndGroups
}
ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{
ID: replied.ID,
Message: i18n.T(i18nk.BotMsgTransferInfoFilesSelectStorage, map[string]any{
"Count": len(filteredFiles),
"SizeMB": fmt.Sprintf("%.2f", float64(totalSize)/(1024*1024)),
}),
ReplyMarkup: markup,
})
return dispatcher.EndGroups
}
func handleTransferCallback(ctx *ext.Context, userID int64, targetStorage storage.Storage, dirPath string, data tcbdata.Add, msgID int) error {
logger := log.FromContext(ctx)
// Get source storage
sourceStorage, err := storage.GetStorageByUserIDAndName(ctx, userID, data.TransferSourceStorName)
if err != nil {
logger.Errorf("Failed to get source storage: %s", err)
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
ID: msgID,
Message: i18n.T(i18nk.BotMsgTransferErrorStorageNotFound, map[string]any{"StorageName": data.TransferSourceStorName, "Error": err}),
})
return dispatcher.EndGroups
}
// Check if source storage supports listing
listable, ok := sourceStorage.(storage.StorageListable)
if !ok {
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
ID: msgID,
Message: i18n.T(i18nk.BotMsgTransferErrorStorageNotListable, map[string]any{"StorageName": data.TransferSourceStorName}),
})
return dispatcher.EndGroups
}
// Re-fetch files to get FileInfo (since we only stored paths)
// This is necessary to get size and other metadata
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
ID: msgID,
Message: i18n.T(i18nk.BotMsgTransferInfoFetchingFiles, nil),
})
allFiles, err := listable.ListFiles(ctx, data.TransferSourcePath)
if err != nil {
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
ID: msgID,
Message: i18n.T(i18nk.BotMsgTransferErrorListFilesFailed, map[string]any{"Error": err}),
})
return dispatcher.EndGroups
}
// Create a map for quick lookup
fileMap := make(map[string]storagetypes.FileInfo)
for _, file := range allFiles {
fileMap[file.Path] = file
}
// Build task elements for the selected files
elems := make([]transfer.TaskElement, 0, len(data.TransferFiles))
var totalSize int64
for _, filePath := range data.TransferFiles {
fileInfo, ok := fileMap[filePath]
if !ok {
logger.Warnf("File not found in source storage: %s", filePath)
continue
}
elem := transfer.NewTaskElement(sourceStorage, fileInfo, targetStorage, dirPath)
elems = append(elems, *elem)
totalSize += fileInfo.Size
}
if len(elems) == 0 {
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
ID: msgID,
Message: i18n.T(i18nk.BotMsgTransferErrorNoFilesToTransfer, nil),
})
return dispatcher.EndGroups
}
// Create and add task
taskID := xid.New().String()
injectCtx := tgutil.ExtWithContext(ctx.Context, ctx)
task := transfer.NewTransferTask(
taskID,
injectCtx,
elems,
transfer.NewProgressTracker(msgID, userID),
true, // IgnoreErrors
)
if err := core.AddTask(injectCtx, task); err != nil {
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
ID: msgID,
Message: i18n.T(i18nk.BotMsgTransferErrorAddTaskFailed, map[string]any{"Error": err}),
})
return dispatcher.EndGroups
}
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
ID: msgID,
Message: i18n.T(i18nk.BotMsgTransferInfoTaskAdded, map[string]any{
"Count": len(elems),
"SizeMB": fmt.Sprintf("%.2f", float64(totalSize)/(1024*1024)),
"TaskID": taskID,
}),
})
return dispatcher.EndGroups
}

View File

@@ -53,6 +53,10 @@ func BuildAddSelectStorageKeyboard(stors []storage.Storage, adddata tcbdata.Add)
Aria2URIs: adddata.Aria2URIs, Aria2URIs: adddata.Aria2URIs,
YtdlpURLs: adddata.YtdlpURLs, YtdlpURLs: adddata.YtdlpURLs,
YtdlpFlags: adddata.YtdlpFlags, YtdlpFlags: adddata.YtdlpFlags,
TransferSourceStorName: adddata.TransferSourceStorName,
TransferSourcePath: adddata.TransferSourcePath,
TransferFiles: adddata.TransferFiles,
} }
dataid := xid.New().String() dataid := xid.New().String()
err := cache.Set(dataid, data) err := cache.Set(dataid, data)

View File

@@ -46,7 +46,7 @@ func CreateAndAddAria2TaskWithEdit(ctx *ext.Context, stor storage.Storage, dirPa
logger.Infof("Aria2 download added with GID: %s", gid) logger.Infof("Aria2 download added with GID: %s", gid)
// Create task with the GID // Create task with the GID
task := aria2dl.NewTask(xid.New().String(), injectCtx, gid, uris, aria2Client, stor, stor.JoinStoragePath(dirPath), aria2dl.NewProgress(msgID, userID)) task := aria2dl.NewTask(xid.New().String(), injectCtx, gid, uris, aria2Client, stor, dirPath, aria2dl.NewProgress(msgID, userID))
if err := core.AddTask(injectCtx, task); err != nil { if err := core.AddTask(injectCtx, task); err != nil {
logger.Errorf("Failed to add task: %s", err) logger.Errorf("Failed to add task: %s", err)
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{ ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{

View File

@@ -16,7 +16,7 @@ import (
func CreateAndAddDirectTaskWithEdit(ctx *ext.Context, stor storage.Storage, dirPath string, links []string, msgID int, userID int64) error { func CreateAndAddDirectTaskWithEdit(ctx *ext.Context, stor storage.Storage, dirPath string, links []string, msgID int, userID int64) error {
injectCtx := tgutil.ExtWithContext(ctx.Context, ctx) injectCtx := tgutil.ExtWithContext(ctx.Context, ctx)
task := directlinks.NewTask(xid.New().String(), injectCtx, links, stor, stor.JoinStoragePath(dirPath), directlinks.NewProgress(msgID, userID)) task := directlinks.NewTask(xid.New().String(), injectCtx, links, stor, dirPath, directlinks.NewProgress(msgID, userID))
if err := core.AddTask(injectCtx, task); err != nil { if err := core.AddTask(injectCtx, task); err != nil {
log.FromContext(ctx).Errorf("Failed to add task: %s", err) log.FromContext(ctx).Errorf("Failed to add task: %s", err)
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{ ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{

View File

@@ -18,7 +18,7 @@ import (
func CreateAndAddParsedTaskWithEdit(ctx *ext.Context, stor storage.Storage, dirPath string, item *parser.Item, msgID int, userID int64) error { func CreateAndAddParsedTaskWithEdit(ctx *ext.Context, stor storage.Storage, dirPath string, item *parser.Item, msgID int, userID int64) error {
injectCtx := tgutil.ExtWithContext(ctx.Context, ctx) injectCtx := tgutil.ExtWithContext(ctx.Context, ctx)
task := parsed.NewTask(xid.New().String(), injectCtx, stor, stor.JoinStoragePath(dirPath), item, parsed.NewProgress(msgID, userID)) task := parsed.NewTask(xid.New().String(), injectCtx, stor, dirPath, item, parsed.NewProgress(msgID, userID))
if err := core.AddTask(injectCtx, task); err != nil { if err := core.AddTask(injectCtx, task); err != nil {
log.FromContext(ctx).Errorf("Failed to add task: %s", err) log.FromContext(ctx).Errorf("Failed to add task: %s", err)
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{ ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{

View File

@@ -59,7 +59,7 @@ func CreateAndAddTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor storage
} }
} }
startCreateTask: startCreateTask:
storagePath := stor.JoinStoragePath(path.Join(dirPath, file.Name())) storagePath := path.Join(dirPath, file.Name())
injectCtx := tgutil.ExtWithContext(ctx.Context, ctx) injectCtx := tgutil.ExtWithContext(ctx.Context, ctx)
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,
@@ -151,7 +151,7 @@ func CreateAndAddBatchTGFileTaskWithEdit(ctx *ext.Context, userID int64, stor st
} }
} }
if !dirPath.NeedNewForAlbum() { if !dirPath.NeedNewForAlbum() {
storPath := fileStor.JoinStoragePath(path.Join(dirPath.String(), file.Name())) storPath := path.Join(dirPath.String(), file.Name())
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)
@@ -188,7 +188,7 @@ 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 := af.storage.JoinStoragePath(path.Join(dirPath, albumDir, af.file.Name())) afstorPath := path.Join(dirPath, albumDir, af.file.Name())
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)

View File

@@ -32,7 +32,7 @@ func CreateAndAddtelegraphWithEdit(
tphpage.Path, tphpage.Path,
pics, pics,
stor, stor,
stor.JoinStoragePath(dirPath), dirPath,
tphutil.DefaultClient(), tphutil.DefaultClient(),
tphtask.NewProgress(trackMsgID, userID), tphtask.NewProgress(trackMsgID, userID),
) )

View File

@@ -38,7 +38,7 @@ func CreateAndAddYtdlpTaskWithEdit(ctx *ext.Context, stor storage.Storage, dirPa
urls, urls,
flags, flags,
stor, stor,
stor.JoinStoragePath(dirPath), dirPath,
ytdlp.NewProgress(msgID, userID), ytdlp.NewProgress(msgID, userID),
) )

View File

@@ -309,7 +309,7 @@ func listenMediaMessageEvent(ch chan userclient.MediaMessageEvent) {
} }
} }
startCreateTask: startCreateTask:
storagePath := stor.JoinStoragePath(path.Join(dirPath, file.Name())) storagePath := path.Join(dirPath, file.Name())
injectCtx := tgutil.ExtWithContext(ctx.Context, ctx) injectCtx := tgutil.ExtWithContext(ctx.Context, ctx)
taskid := xid.New().String() taskid := xid.New().String()
task, err := coretfile.NewTGFileTask(taskid, injectCtx, file, stor, storagePath, nil) task, err := coretfile.NewTGFileTask(taskid, injectCtx, file, stor, storagePath, nil)
@@ -403,7 +403,7 @@ func processWatchMediaGroup(ctx *ext.Context, user *database.User, stor storage.
logger.Infof("Creating album folder for group %d: %s with %d files", groupID, albumDir, len(afiles)) logger.Infof("Creating album folder for group %d: %s with %d files", groupID, albumDir, len(afiles))
for _, af := range afiles { for _, af := range afiles {
afstorPath := af.storage.JoinStoragePath(path.Join(dirPath, albumDir, af.file.Name())) afstorPath := path.Join(dirPath, albumDir, af.file.Name())
taskid := xid.New().String() taskid := xid.New().String()
task, err := coretfile.NewTGFileTask(taskid, injectCtx, af.file, albumStor, afstorPath, nil) task, err := coretfile.NewTGFileTask(taskid, injectCtx, af.file, albumStor, afstorPath, nil)
if err != nil { if err != nil {

View File

@@ -90,7 +90,7 @@ func Upload(cmd *cobra.Command, args []string) error {
fileName := fileInfo.Name() fileName := fileInfo.Name()
fileSize := fileInfo.Size() fileSize := fileInfo.Size()
uploadPath := stor.JoinStoragePath(path.Join(dirPath, fileName)) uploadPath := path.Join(dirPath, fileName)
ctx = context.WithValue(ctx, ctxkey.ContentLength, fileSize) ctx = context.WithValue(ctx, ctxkey.ContentLength, fileSize)
ctx = tgutil.ExtWithContext(ctx, bot.ExtContext()) ctx = tgutil.ExtWithContext(ctx, bot.ExtContext())

View File

@@ -31,6 +31,7 @@ const (
BotMsgCmdStorage Key = "bot.msg.cmd.storage" BotMsgCmdStorage Key = "bot.msg.cmd.storage"
BotMsgCmdSyncpeers Key = "bot.msg.cmd.syncpeers" BotMsgCmdSyncpeers Key = "bot.msg.cmd.syncpeers"
BotMsgCmdTask Key = "bot.msg.cmd.task" BotMsgCmdTask Key = "bot.msg.cmd.task"
BotMsgCmdTransfer Key = "bot.msg.cmd.transfer"
BotMsgCmdUnwatch Key = "bot.msg.cmd.unwatch" BotMsgCmdUnwatch Key = "bot.msg.cmd.unwatch"
BotMsgCmdUpdate Key = "bot.msg.cmd.update" BotMsgCmdUpdate Key = "bot.msg.cmd.update"
BotMsgCmdWatch Key = "bot.msg.cmd.watch" BotMsgCmdWatch Key = "bot.msg.cmd.watch"
@@ -106,20 +107,6 @@ const (
BotMsgDlInfoFilesSelectStorage Key = "bot.msg.dl.info_files_select_storage" BotMsgDlInfoFilesSelectStorage Key = "bot.msg.dl.info_files_select_storage"
BotMsgDlUsage Key = "bot.msg.dl.usage" BotMsgDlUsage Key = "bot.msg.dl.usage"
BotMsgHelpTextFmt Key = "bot.msg.help_text_fmt" BotMsgHelpTextFmt Key = "bot.msg.help_text_fmt"
BotMsgImportErrorAddTaskFailed Key = "bot.msg.import.error_add_task_failed"
BotMsgImportErrorInvalidChatId Key = "bot.msg.import.error_invalid_chat_id"
BotMsgImportErrorInvalidRegex Key = "bot.msg.import.error_invalid_regex"
BotMsgImportErrorListFilesFailed Key = "bot.msg.import.error_list_files_failed"
BotMsgImportErrorNoFilesToImport Key = "bot.msg.import.error_no_files_to_import"
BotMsgImportErrorNoTargetChatId Key = "bot.msg.import.error_no_target_chat_id"
BotMsgImportErrorNoTelegramStorage Key = "bot.msg.import.error_no_telegram_storage"
BotMsgImportErrorStorageNotFound Key = "bot.msg.import.error_storage_not_found"
BotMsgImportErrorStorageNotListable Key = "bot.msg.import.error_storage_not_listable"
BotMsgImportErrorStorageNotReadable Key = "bot.msg.import.error_storage_not_readable"
BotMsgImportInfoFetchingFiles Key = "bot.msg.import.info_fetching_files"
BotMsgImportInfoTaskAdded Key = "bot.msg.import.info_task_added"
BotMsgImportStartStats Key = "bot.msg.import.start_stats"
BotMsgImportUsage Key = "bot.msg.import.usage"
BotMsgMediaGroupErrorBuildStorageSelectKeyboardFailed Key = "bot.msg.media_group.error_build_storage_select_keyboard_failed" BotMsgMediaGroupErrorBuildStorageSelectKeyboardFailed Key = "bot.msg.media_group.error_build_storage_select_keyboard_failed"
BotMsgMediaGroupInfoGroupFoundFilesSelectStorage Key = "bot.msg.media_group.info_group_found_files_select_storage" BotMsgMediaGroupInfoGroupFoundFilesSelectStorage Key = "bot.msg.media_group.info_group_found_files_select_storage"
BotMsgMediaGroupInfoSavingFiles Key = "bot.msg.media_group.info_saving_files" BotMsgMediaGroupInfoSavingFiles Key = "bot.msg.media_group.info_saving_files"
@@ -164,20 +151,6 @@ const (
BotMsgProgressFileProcessingPrefix Key = "bot.msg.progress.file_processing_prefix" BotMsgProgressFileProcessingPrefix Key = "bot.msg.progress.file_processing_prefix"
BotMsgProgressFileSizePrefix Key = "bot.msg.progress.file_size_prefix" BotMsgProgressFileSizePrefix Key = "bot.msg.progress.file_size_prefix"
BotMsgProgressFileStartPrefix Key = "bot.msg.progress.file_start_prefix" BotMsgProgressFileStartPrefix Key = "bot.msg.progress.file_start_prefix"
BotMsgProgressImportAvgSpeedPrefix Key = "bot.msg.progress.import_avg_speed_prefix"
BotMsgProgressImportElapsedTimePrefix Key = "bot.msg.progress.import_elapsed_time_prefix"
BotMsgProgressImportFailedFilesPrefix Key = "bot.msg.progress.import_failed_files_prefix"
BotMsgProgressImportFailedPrefix Key = "bot.msg.progress.import_failed_prefix"
BotMsgProgressImportProcessingMore Key = "bot.msg.progress.import_processing_more"
BotMsgProgressImportProcessingPrefix Key = "bot.msg.progress.import_processing_prefix"
BotMsgProgressImportProgressPrefix Key = "bot.msg.progress.import_progress_prefix"
BotMsgProgressImportRemainingTimePrefix Key = "bot.msg.progress.import_remaining_time_prefix"
BotMsgProgressImportSpeedPrefix Key = "bot.msg.progress.import_speed_prefix"
BotMsgProgressImportStartPrefix Key = "bot.msg.progress.import_start_prefix"
BotMsgProgressImportSuccessPrefix Key = "bot.msg.progress.import_success_prefix"
BotMsgProgressImportTotalFilesPrefix Key = "bot.msg.progress.import_total_files_prefix"
BotMsgProgressImportTotalSizePrefix Key = "bot.msg.progress.import_total_size_prefix"
BotMsgProgressImportUploadedPrefix Key = "bot.msg.progress.import_uploaded_prefix"
BotMsgProgressParsedDonePrefix Key = "bot.msg.progress.parsed_done_prefix" BotMsgProgressParsedDonePrefix Key = "bot.msg.progress.parsed_done_prefix"
BotMsgProgressParsedStartPrefix Key = "bot.msg.progress.parsed_start_prefix" BotMsgProgressParsedStartPrefix Key = "bot.msg.progress.parsed_start_prefix"
BotMsgProgressProcessingListPrefix Key = "bot.msg.progress.processing_list_prefix" BotMsgProgressProcessingListPrefix Key = "bot.msg.progress.processing_list_prefix"
@@ -190,6 +163,20 @@ const (
BotMsgProgressTelegraphProgressPrefix Key = "bot.msg.progress.telegraph_progress_prefix" BotMsgProgressTelegraphProgressPrefix Key = "bot.msg.progress.telegraph_progress_prefix"
BotMsgProgressTelegraphStartPrefix Key = "bot.msg.progress.telegraph_start_prefix" BotMsgProgressTelegraphStartPrefix Key = "bot.msg.progress.telegraph_start_prefix"
BotMsgProgressTotalSizePrefix Key = "bot.msg.progress.total_size_prefix" BotMsgProgressTotalSizePrefix Key = "bot.msg.progress.total_size_prefix"
BotMsgProgressTransferAvgSpeedPrefix Key = "bot.msg.progress.transfer_avg_speed_prefix"
BotMsgProgressTransferElapsedTimePrefix Key = "bot.msg.progress.transfer_elapsed_time_prefix"
BotMsgProgressTransferFailedFilesPrefix Key = "bot.msg.progress.transfer_failed_files_prefix"
BotMsgProgressTransferFailedPrefix Key = "bot.msg.progress.transfer_failed_prefix"
BotMsgProgressTransferProcessingMore Key = "bot.msg.progress.transfer_processing_more"
BotMsgProgressTransferProcessingPrefix Key = "bot.msg.progress.transfer_processing_prefix"
BotMsgProgressTransferProgressPrefix Key = "bot.msg.progress.transfer_progress_prefix"
BotMsgProgressTransferRemainingTimePrefix Key = "bot.msg.progress.transfer_remaining_time_prefix"
BotMsgProgressTransferSpeedPrefix Key = "bot.msg.progress.transfer_speed_prefix"
BotMsgProgressTransferStartPrefix Key = "bot.msg.progress.transfer_start_prefix"
BotMsgProgressTransferSuccessPrefix Key = "bot.msg.progress.transfer_success_prefix"
BotMsgProgressTransferTotalFilesPrefix Key = "bot.msg.progress.transfer_total_files_prefix"
BotMsgProgressTransferTotalSizePrefix Key = "bot.msg.progress.transfer_total_size_prefix"
BotMsgProgressTransferUploadedPrefix Key = "bot.msg.progress.transfer_uploaded_prefix"
BotMsgProgressYtdlpDone Key = "bot.msg.progress.ytdlp_done" BotMsgProgressYtdlpDone Key = "bot.msg.progress.ytdlp_done"
BotMsgProgressYtdlpDownloading Key = "bot.msg.progress.ytdlp_downloading" BotMsgProgressYtdlpDownloading Key = "bot.msg.progress.ytdlp_downloading"
BotMsgProgressYtdlpStart Key = "bot.msg.progress.ytdlp_start" BotMsgProgressYtdlpStart Key = "bot.msg.progress.ytdlp_start"
@@ -245,6 +232,22 @@ const (
BotMsgTelegraphInfoPicCountPrefix Key = "bot.msg.telegraph.info_pic_count_prefix" BotMsgTelegraphInfoPicCountPrefix Key = "bot.msg.telegraph.info_pic_count_prefix"
BotMsgTelegraphInfoPromptSelectStorage Key = "bot.msg.telegraph.info_prompt_select_storage" BotMsgTelegraphInfoPromptSelectStorage Key = "bot.msg.telegraph.info_prompt_select_storage"
BotMsgTelegraphInfoTitlePrefix Key = "bot.msg.telegraph.info_title_prefix" BotMsgTelegraphInfoTitlePrefix Key = "bot.msg.telegraph.info_title_prefix"
BotMsgTransferErrorAddTaskFailed Key = "bot.msg.transfer.error_add_task_failed"
BotMsgTransferErrorBuildStorageSelectKeyboardFailed Key = "bot.msg.transfer.error_build_storage_select_keyboard_failed"
BotMsgTransferErrorInvalidRegex Key = "bot.msg.transfer.error_invalid_regex"
BotMsgTransferErrorInvalidSource Key = "bot.msg.transfer.error_invalid_source"
BotMsgTransferErrorInvalidTarget Key = "bot.msg.transfer.error_invalid_target"
BotMsgTransferErrorListFilesFailed Key = "bot.msg.transfer.error_list_files_failed"
BotMsgTransferErrorNoFilesToTransfer Key = "bot.msg.transfer.error_no_files_to_transfer"
BotMsgTransferErrorStorageNotFound Key = "bot.msg.transfer.error_storage_not_found"
BotMsgTransferErrorStorageNotListable Key = "bot.msg.transfer.error_storage_not_listable"
BotMsgTransferErrorStorageNotReadable Key = "bot.msg.transfer.error_storage_not_readable"
BotMsgTransferErrorTargetNotFound Key = "bot.msg.transfer.error_target_not_found"
BotMsgTransferInfoFetchingFiles Key = "bot.msg.transfer.info_fetching_files"
BotMsgTransferInfoFilesSelectStorage Key = "bot.msg.transfer.info_files_select_storage"
BotMsgTransferInfoTaskAdded Key = "bot.msg.transfer.info_task_added"
BotMsgTransferStartStats Key = "bot.msg.transfer.start_stats"
BotMsgTransferUsage Key = "bot.msg.transfer.usage"
BotMsgUpdateButtonUpgrade Key = "bot.msg.update.button_upgrade" BotMsgUpdateButtonUpgrade Key = "bot.msg.update.button_upgrade"
BotMsgUpdateErrorCheckLatestFailed Key = "bot.msg.update.error_check_latest_failed" BotMsgUpdateErrorCheckLatestFailed Key = "bot.msg.update.error_check_latest_failed"
BotMsgUpdateErrorNoReleaseFound Key = "bot.msg.update.error_no_release_found" BotMsgUpdateErrorNoReleaseFound Key = "bot.msg.update.error_no_release_found"

View File

@@ -54,6 +54,7 @@ bot:
aria2dl: "Download files using Aria2" aria2dl: "Download files using Aria2"
ytdlp: "Download video/audio using yt-dlp" ytdlp: "Download video/audio using yt-dlp"
import: "Import files from storage to Telegram" import: "Import files from storage to Telegram"
transfer: "Transfer files between storages"
task: "Manage task queue" task: "Manage task queue"
cancel: "Cancel task" cancel: "Cancel task"
watch: "Watch chats (UserBot)" watch: "Watch chats (UserBot)"
@@ -296,20 +297,28 @@ bot:
info_urls_select_storage: "Found {{.Count}} links, please select storage" info_urls_select_storage: "Found {{.Count}} links, please select storage"
info_downloading: "Downloading via yt-dlp..." info_downloading: "Downloading via yt-dlp..."
error_download_failed: "yt-dlp download failed: {{.Error}}" error_download_failed: "yt-dlp download failed: {{.Error}}"
import: transfer:
usage: "Usage: /import <storage_name> <dir_path> [target_chat_id] [filter]\n\nExamples:\n/import local1 /downloads\n/import MyAlist /media/photos -1001234567890\n/import MyLocal /backup \".*[.]mp4$\"" usage: |
Usage: /transfer <source_storage>:/<source_path> [filter]
Examples:
/transfer local1:/downloads
/transfer alist1:/media/photos
/transfer webdav1:/files ".*\.mp4$"
error_invalid_source: "Invalid source path format, should be: storage_name:/path"
error_invalid_target: "Invalid target path format, should be: storage_name:/path"
error_storage_not_found: "Storage '{{.StorageName}}' not found or access denied: {{.Error}}" error_storage_not_found: "Storage '{{.StorageName}}' not found or access denied: {{.Error}}"
error_storage_not_listable: "Storage '{{.StorageName}}' does not support listing files" error_storage_not_listable: "Storage '{{.StorageName}}' does not support listing files"
error_storage_not_readable: "Storage '{{.StorageName}}' does not support reading files" error_storage_not_readable: "Storage '{{.StorageName}}' does not support reading files"
error_no_telegram_storage: "No Telegram storage found: {{.Error}}" error_target_not_found: "Target storage '{{.StorageName}}' not found or access denied: {{.Error}}"
info_fetching_files: "Fetching file list..." info_fetching_files: "Fetching file list..."
error_list_files_failed: "Failed to list files: {{.Error}}" error_list_files_failed: "Failed to list files: {{.Error}}"
error_invalid_regex: "Invalid regular expression: {{.Error}}" error_invalid_regex: "Invalid regular expression: {{.Error}}"
error_no_files_to_import: "No files to import in directory" error_no_files_to_transfer: "No files to transfer in directory"
error_invalid_chat_id: "Invalid Chat ID: {{.Error}}"
error_no_target_chat_id: "No target channel ID specified and Telegram storage has no default chat_id configured"
error_add_task_failed: "Failed to add task: {{.Error}}" error_add_task_failed: "Failed to add task: {{.Error}}"
info_task_added: "Added {{.Count}} files to import queue\nTotal size: {{.SizeMB}} MB\nTask ID: {{.TaskID}}" info_task_added: "Added {{.Count}} files to transfer queue\nTotal size: {{.SizeMB}} MB\nTask ID: {{.TaskID}}"
start_stats: "Total files: {{.Count}}\nTotal size: {{.SizeMB}} MB"
info_files_select_storage: "Total {{.Count}} files ({{.SizeMB}} MB), please select target storage"
error_build_storage_select_keyboard_failed: "Failed to build storage selection keyboard: {{.Error}}"
cancel: cancel:
usage: "Usage: /cancel <task_id>" usage: "Usage: /cancel <task_id>"
error_cancel_failed: "Failed to cancel task: {{.Error}}" error_cancel_failed: "Failed to cancel task: {{.Error}}"
@@ -358,20 +367,20 @@ bot:
ytdlp_done: "yt-dlp download completed and transferred ({{.Count}} files)\n" ytdlp_done: "yt-dlp download completed and transferred ({{.Count}} files)\n"
downloaded_prefix: "\nDownloaded: " downloaded_prefix: "\nDownloaded: "
current_speed_prefix: "\nCurrent speed: " current_speed_prefix: "\nCurrent speed: "
import_start_prefix: "Importing: " transfer_start_prefix: "Transfering: "
import_progress_prefix: "Import progress: " transfer_progress_prefix: "Transfer progress: "
import_uploaded_prefix: "\nUploaded: " transfer_uploaded_prefix: "\nUploaded: "
import_speed_prefix: "\nSpeed: " transfer_speed_prefix: "\nSpeed: "
import_remaining_time_prefix: "\nRemaining time: " transfer_remaining_time_prefix: "\nRemaining time: "
import_processing_prefix: "\nProcessing:\n" transfer_processing_prefix: "\nProcessing:\n"
import_processing_more: "...and {{.Count}} more files\n" transfer_processing_more: "...and {{.Count}} more files\n"
import_failed_prefix: "Import failed\n" transfer_failed_prefix: "Transfer failed\n"
import_success_prefix: "Import completed\n" transfer_success_prefix: "Transfer completed\n"
import_total_files_prefix: "\nTotal files: " transfer_total_files_prefix: "\nTotal files: "
import_total_size_prefix: "\nTotal size: " transfer_total_size_prefix: "\nTotal size: "
import_elapsed_time_prefix: "\nElapsed time: " transfer_elapsed_time_prefix: "\nElapsed time: "
import_avg_speed_prefix: "\nAverage speed: " transfer_avg_speed_prefix: "\nAverage speed: "
import_failed_files_prefix: "\nFailed files: " transfer_failed_files_prefix: "\nFailed files: "
syncpeers: syncpeers:
start: "Starting to sync peers..." start: "Starting to sync peers..."
done: "Peer sync completed, total {{.Count}} chats synced" done: "Peer sync completed, total {{.Count}} chats synced"

View File

@@ -55,6 +55,7 @@ bot:
aria2dl: "使用 Aria2 下载给定链接的文件" aria2dl: "使用 Aria2 下载给定链接的文件"
ytdlp: "使用 yt-dlp 下载视频/音频" ytdlp: "使用 yt-dlp 下载视频/音频"
import: "从存储端导入文件到 Telegram" import: "从存储端导入文件到 Telegram"
transfer: "在存储端之间传输文件"
task: "管理任务队列" task: "管理任务队列"
cancel: "取消任务" cancel: "取消任务"
watch: "监听聊天(UserBot)" watch: "监听聊天(UserBot)"
@@ -297,26 +298,28 @@ bot:
info_urls_select_storage: "共 {{.Count}} 个链接, 请选择存储位置" info_urls_select_storage: "共 {{.Count}} 个链接, 请选择存储位置"
info_downloading: "正在通过 yt-dlp 下载..." info_downloading: "正在通过 yt-dlp 下载..."
error_download_failed: "yt-dlp 下载失败: {{.Error}}" error_download_failed: "yt-dlp 下载失败: {{.Error}}"
import: transfer:
usage: | usage: |
用法: /import <storage_name> <dir_path> [target_chat_id] [filter] 用法: /transfer <source_storage>:/<source_path> [filter]
示例: 示例:
/import 本机1 /downloads /transfer local1:/downloads
/import MyAlist /media/photos -1001234567890 /transfer alist1:/media/photos
/import MyLocal /backup ".*\.mp4$" /transfer webdav1:/files ".*\.mp4$"
error_invalid_source: "源路径格式无效,应为: storage_name:/path"
error_invalid_target: "目标路径格式无效,应为: storage_name:/path"
error_storage_not_found: "存储端 '{{.StorageName}}' 不存在或您无权访问: {{.Error}}" error_storage_not_found: "存储端 '{{.StorageName}}' 不存在或您无权访问: {{.Error}}"
error_storage_not_listable: "存储端 '{{.StorageName}}' 不支持列举文件功能" error_storage_not_listable: "存储端 '{{.StorageName}}' 不支持列举文件功能"
error_storage_not_readable: "存储端 '{{.StorageName}}' 不支持读取文件功能" error_storage_not_readable: "存储端 '{{.StorageName}}' 不支持读取文件功能"
error_no_telegram_storage: "未找到可用的 Telegram 存储: {{.Error}}" error_target_not_found: "目标存储端 '{{.StorageName}}' 不存在或您无权访问: {{.Error}}"
info_fetching_files: "正在获取文件列表..." info_fetching_files: "正在获取文件列表..."
error_list_files_failed: "获取文件列表失败: {{.Error}}" error_list_files_failed: "获取文件列表失败: {{.Error}}"
error_invalid_regex: "正则表达式无效: {{.Error}}" error_invalid_regex: "正则表达式无效: {{.Error}}"
error_no_files_to_import: "目录中没有可导入的文件" error_no_files_to_transfer: "目录中没有可传输的文件"
error_invalid_chat_id: "无效的 Chat ID: {{.Error}}"
error_no_target_chat_id: "未指定目标频道 ID且 Telegram 存储未配置默认 chat_id"
error_add_task_failed: "添加任务失败: {{.Error}}" error_add_task_failed: "添加任务失败: {{.Error}}"
info_task_added: "已添加 {{.Count}} 个文件到导入队列\n总大小: {{.SizeMB}} MB\n任务 ID: {{.TaskID}}" info_task_added: "已添加 {{.Count}} 个文件到传输队列\n总大小: {{.SizeMB}} MB\n任务 ID: {{.TaskID}}"
start_stats: "总文件数: {{.Count}}\n总大小: {{.SizeMB}} MB" start_stats: "总文件数: {{.Count}}\n总大小: {{.SizeMB}} MB"
info_files_select_storage: "共 {{.Count}} 个文件 (总大小: {{.SizeMB}} MB),请选择目标存储位置"
error_build_storage_select_keyboard_failed: "构建存储选择键盘失败: {{.Error}}"
cancel: cancel:
usage: "用法: /cancel <task_id>" usage: "用法: /cancel <task_id>"
error_cancel_failed: "取消任务失败: {{.Error}}" error_cancel_failed: "取消任务失败: {{.Error}}"
@@ -365,20 +368,20 @@ bot:
ytdlp_done: "yt-dlp 下载完成并已转存 ({{.Count}} 个文件)\n" ytdlp_done: "yt-dlp 下载完成并已转存 ({{.Count}} 个文件)\n"
downloaded_prefix: "\n已下载: " downloaded_prefix: "\n已下载: "
current_speed_prefix: "\n当前速度: " current_speed_prefix: "\n当前速度: "
import_start_prefix: "正在导入: " transfer_start_prefix: "正在转存: "
import_progress_prefix: "导入进度: " transfer_progress_prefix: "转存进度: "
import_uploaded_prefix: "\n已上传: " transfer_uploaded_prefix: "\n已上传: "
import_speed_prefix: "\n速度: " transfer_speed_prefix: "\n速度: "
import_remaining_time_prefix: "\n剩余时间: " transfer_remaining_time_prefix: "\n剩余时间: "
import_processing_prefix: "\n正在处理:\n" transfer_processing_prefix: "\n正在处理:\n"
import_processing_more: "...和其他 {{.Count}} 个文件\n" transfer_processing_more: "...和其他 {{.Count}} 个文件\n"
import_failed_prefix: "导入失败\n" transfer_failed_prefix: "转存失败\n"
import_success_prefix: "导入完成\n" transfer_success_prefix: "转存完成\n"
import_total_files_prefix: "\n总文件数: " transfer_total_files_prefix: "\n总文件数: "
import_total_size_prefix: "\n总大小: " transfer_total_size_prefix: "\n总大小: "
import_elapsed_time_prefix: "\n耗时: " transfer_elapsed_time_prefix: "\n耗时: "
import_avg_speed_prefix: "\n平均速度: " transfer_avg_speed_prefix: "\n平均速度: "
import_failed_files_prefix: "\n失败文件数: " transfer_failed_files_prefix: "\n失败文件数: "
syncpeers: syncpeers:
start: "正在同步对话列表..." start: "正在同步对话列表..."
success: "对话列表同步完成, 共同步 {{.Count}} 个对话" success: "对话列表同步完成, 共同步 {{.Count}} 个对话"

View File

@@ -1,10 +1,11 @@
package batchimport package transfer
import ( import (
"context" "context"
"fmt" "fmt"
"io" "io"
"os" "os"
"path"
"path/filepath" "path/filepath"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
@@ -16,8 +17,8 @@ import (
// Execute implements core.Executable. // Execute implements core.Executable.
func (t *Task) Execute(ctx context.Context) error { func (t *Task) Execute(ctx context.Context) error {
logger := log.FromContext(ctx).WithPrefix(fmt.Sprintf("batch_import[%s]", t.ID)) logger := log.FromContext(ctx).WithPrefix(fmt.Sprintf("transfer[%s]", t.ID))
logger.Info("Starting batch import task") logger.Info("Starting transfer task")
t.Progress.OnStart(ctx, t) t.Progress.OnStart(ctx, t)
workers := config.C().Workers workers := config.C().Workers
@@ -59,9 +60,9 @@ func (t *Task) Execute(ctx context.Context) error {
err := eg.Wait() err := eg.Wait()
if err != nil { if err != nil {
logger.Errorf("Error during batch import processing: %v", err) logger.Errorf("Error during transfer processing: %v", err)
} else { } else {
logger.Info("Batch import task completed successfully") logger.Info("Transfer task completed successfully")
} }
t.Progress.OnDone(ctx, t, err) t.Progress.OnDone(ctx, t, err)
@@ -84,15 +85,15 @@ func (t *Task) processElement(ctx context.Context, elem TaskElement) error {
} }
defer reader.Close() defer reader.Close()
// Build Telegram storage path: /<chat_id>/<filename> // Build target storage path: /target_path/filename
storagePath := fmt.Sprintf("/%d/%s", elem.TargetChatID, elem.FileInfo.Name) storagePath := path.Join(elem.TargetPath, elem.FileInfo.Name)
// 注入文件大小到 context // Inject file size into context
ctx = context.WithValue(ctx, ctxkey.ContentLength, size) ctx = context.WithValue(ctx, ctxkey.ContentLength, size)
if config.C().Stream { if config.C().Stream {
if err := elem.TargetStorage.Save(ctx, reader, storagePath); err != nil { if err := elem.TargetStorage.Save(ctx, reader, storagePath); err != nil {
return fmt.Errorf("failed to upload file to telegram: %w", err) return fmt.Errorf("failed to upload file to storage: %w", err)
} }
} else { } else {
logger.Info("Downloading to temporary file for ReadSeeker support") logger.Info("Downloading to temporary file for ReadSeeker support")
@@ -107,9 +108,9 @@ func (t *Task) processElement(ctx context.Context, elem TaskElement) error {
return fmt.Errorf("failed to seek temp file: %w", err) return fmt.Errorf("failed to seek temp file: %w", err)
} }
logger.Infof("Uploading file to Telegram storage (size: %d bytes)", size) logger.Infof("Uploading file to storage (size: %d bytes)", size)
if err := elem.TargetStorage.Save(ctx, tempFile, storagePath); err != nil { if err := elem.TargetStorage.Save(ctx, tempFile, storagePath); err != nil {
return fmt.Errorf("failed to upload file to telegram: %w", err) return fmt.Errorf("failed to upload file to storage: %w", err)
} }
} }

View File

@@ -1,4 +1,4 @@
package batchimport package transfer
import ( import (
"context" "context"
@@ -40,17 +40,17 @@ func NewProgressTracker(messageID int, chatID int64) ProgressTracker {
func (p *Progress) OnStart(ctx context.Context, info TaskInfo) { func (p *Progress) OnStart(ctx context.Context, info TaskInfo) {
p.start = time.Now() p.start = time.Now()
p.lastUpdatePercent.Store(0) p.lastUpdatePercent.Store(0)
log.FromContext(ctx).Debugf("Batch import task progress tracking started for message %d in chat %d", p.MessageID, p.ChatID) log.FromContext(ctx).Debugf("Transfer task progress tracking started for message %d in chat %d", p.MessageID, p.ChatID)
sizeMB := float64(info.TotalSize()) / (1024 * 1024) sizeMB := float64(info.TotalSize()) / (1024 * 1024)
statsText := i18n.T(i18nk.BotMsgImportStartStats, map[string]any{ statsText := i18n.T(i18nk.BotMsgTransferStartStats, map[string]any{
"SizeMB": fmt.Sprintf("%.2f", sizeMB), "SizeMB": fmt.Sprintf("%.2f", sizeMB),
"Count": info.Count(), "Count": info.Count(),
}) })
entityBuilder := entity.Builder{} entityBuilder := entity.Builder{}
if err := styling.Perform(&entityBuilder, if err := styling.Perform(&entityBuilder,
styling.Plain(i18n.T(i18nk.BotMsgProgressImportStartPrefix, nil)), styling.Plain(i18n.T(i18nk.BotMsgProgressTransferStartPrefix, nil)),
styling.Code(statsText), styling.Code(statsText),
); err != nil { ); err != nil {
log.FromContext(ctx).Errorf("Failed to build entities: %s", err) log.FromContext(ctx).Errorf("Failed to build entities: %s", err)
@@ -75,7 +75,10 @@ func (p *Progress) OnStart(ctx context.Context, info TaskInfo) {
ext := tgutil.ExtFromContext(ctx) ext := tgutil.ExtFromContext(ctx)
if ext != nil { if ext != nil {
ext.EditMessage(p.ChatID, req) _, err := ext.EditMessage(p.ChatID, req)
if err != nil {
log.FromContext(ctx).Errorf("Failed to send progress start message: %s", err)
}
} }
} }
@@ -94,32 +97,32 @@ func (p *Progress) OnProgress(ctx context.Context, info TaskInfo) {
entityBuilder := entity.Builder{} entityBuilder := entity.Builder{}
var progressText strings.Builder var progressText strings.Builder
progressText.WriteString(i18n.T(i18nk.BotMsgProgressImportProgressPrefix, nil)) progressText.WriteString(i18n.T(i18nk.BotMsgProgressTransferProgressPrefix, nil))
progressText.WriteString(fmt.Sprintf("%d%%", percent)) fmt.Fprintf(&progressText, "%d%%", percent)
progressText.WriteString(i18n.T(i18nk.BotMsgProgressImportUploadedPrefix, nil)) progressText.WriteString(i18n.T(i18nk.BotMsgProgressTransferUploadedPrefix, nil))
progressText.WriteString(fmt.Sprintf("%.2f MB / %.2f MB", fmt.Fprintf(&progressText, "%.2f MB / %.2f MB",
float64(info.Uploaded())/(1024*1024), float64(info.Uploaded())/(1024*1024),
float64(info.TotalSize())/(1024*1024))) float64(info.TotalSize())/(1024*1024))
if p.start.Unix() > 0 { if p.start.Unix() > 0 {
elapsed := time.Since(p.start) elapsed := time.Since(p.start)
speed := float64(info.Uploaded()) / elapsed.Seconds() speed := float64(info.Uploaded()) / elapsed.Seconds()
progressText.WriteString(i18n.T(i18nk.BotMsgProgressImportSpeedPrefix, nil)) progressText.WriteString(i18n.T(i18nk.BotMsgProgressTransferSpeedPrefix, nil))
progressText.WriteString(dlutil.FormatSize(int64(speed)) + "/s") progressText.WriteString(dlutil.FormatSize(int64(speed)) + "/s")
if info.Uploaded() > 0 { if info.Uploaded() > 0 {
remaining := time.Duration(float64(info.TotalSize()-info.Uploaded()) / speed * float64(time.Second)) remaining := time.Duration(float64(info.TotalSize()-info.Uploaded()) / speed * float64(time.Second))
progressText.WriteString(i18n.T(i18nk.BotMsgProgressImportRemainingTimePrefix, nil)) progressText.WriteString(i18n.T(i18nk.BotMsgProgressTransferRemainingTimePrefix, nil))
progressText.WriteString(formatDuration(remaining)) progressText.WriteString(formatDuration(remaining))
} }
} }
processing := info.Processing() processing := info.Processing()
if len(processing) > 0 { if len(processing) > 0 {
progressText.WriteString(i18n.T(i18nk.BotMsgProgressImportProcessingPrefix, nil)) progressText.WriteString(i18n.T(i18nk.BotMsgProgressTransferProcessingPrefix, nil))
for i, elem := range processing { for i, elem := range processing {
if i >= 3 { if i >= 3 {
progressText.WriteString(i18n.T(i18nk.BotMsgProgressImportProcessingMore, map[string]any{"Count": len(processing) - 3})) progressText.WriteString(i18n.T(i18nk.BotMsgProgressTransferProcessingMore, map[string]any{"Count": len(processing) - 3}))
break break
} }
fmt.Fprintf(&progressText, "- %s\n", elem.FileName()) fmt.Fprintf(&progressText, "- %s\n", elem.FileName())
@@ -156,42 +159,42 @@ func (p *Progress) OnProgress(ctx context.Context, info TaskInfo) {
} }
func (p *Progress) OnDone(ctx context.Context, info TaskInfo, err error) { func (p *Progress) OnDone(ctx context.Context, info TaskInfo, err error) {
log.FromContext(ctx).Debugf("Batch import task progress tracking done for message %d in chat %d", p.MessageID, p.ChatID) log.FromContext(ctx).Debugf("Transfer task progress tracking done for message %d in chat %d", p.MessageID, p.ChatID)
entityBuilder := entity.Builder{} entityBuilder := entity.Builder{}
var resultText strings.Builder var resultText strings.Builder
if err != nil { if err != nil {
resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportFailedPrefix, nil)) resultText.WriteString(i18n.T(i18nk.BotMsgProgressTransferFailedPrefix, nil))
resultText.WriteString(i18n.T(i18nk.BotMsgProgressErrorPrefix, nil)) resultText.WriteString(i18n.T(i18nk.BotMsgProgressErrorPrefix, nil))
fmt.Fprintf(&resultText, "%v\n", err) fmt.Fprintf(&resultText, "%v\n", err)
} else { } else {
resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportSuccessPrefix, nil)) resultText.WriteString(i18n.T(i18nk.BotMsgProgressTransferSuccessPrefix, nil))
} }
elapsed := time.Since(p.start) elapsed := time.Since(p.start)
resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportTotalFilesPrefix, nil)) resultText.WriteString(i18n.T(i18nk.BotMsgProgressTransferTotalFilesPrefix, nil))
fmt.Fprintf(&resultText, "%d\n", info.Count()) fmt.Fprintf(&resultText, "%d\n", info.Count())
resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportTotalSizePrefix, nil)) resultText.WriteString(i18n.T(i18nk.BotMsgProgressTransferTotalSizePrefix, nil))
fmt.Fprintf(&resultText, "%.2f MB\n", float64(info.TotalSize())/(1024*1024)) fmt.Fprintf(&resultText, "%.2f MB\n", float64(info.TotalSize())/(1024*1024))
resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportUploadedPrefix, nil)) resultText.WriteString(i18n.T(i18nk.BotMsgProgressTransferUploadedPrefix, nil))
fmt.Fprintf(&resultText, "%.2f MB\n", float64(info.Uploaded())/(1024*1024)) fmt.Fprintf(&resultText, "%.2f MB\n", float64(info.Uploaded())/(1024*1024))
resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportElapsedTimePrefix, nil)) resultText.WriteString(i18n.T(i18nk.BotMsgProgressTransferElapsedTimePrefix, nil))
fmt.Fprintf(&resultText, "%s\n", formatDuration(elapsed)) fmt.Fprintf(&resultText, "%s\n", formatDuration(elapsed))
if elapsed.Seconds() > 0 { if elapsed.Seconds() > 0 {
avgSpeed := float64(info.Uploaded()) / elapsed.Seconds() avgSpeed := float64(info.Uploaded()) / elapsed.Seconds()
resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportAvgSpeedPrefix, nil)) resultText.WriteString(i18n.T(i18nk.BotMsgProgressTransferAvgSpeedPrefix, nil))
fmt.Fprintf(&resultText, "%s/s\n", dlutil.FormatSize(int64(avgSpeed))) fmt.Fprintf(&resultText, "%s/s\n", dlutil.FormatSize(int64(avgSpeed)))
} }
failedFiles := info.FailedFiles() failedFiles := info.FailedFiles()
if len(failedFiles) > 0 { if len(failedFiles) > 0 {
resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportFailedFilesPrefix, nil)) resultText.WriteString(i18n.T(i18nk.BotMsgProgressTransferFailedFilesPrefix, nil))
fmt.Fprintf(&resultText, "%d\n", len(failedFiles)) fmt.Fprintf(&resultText, "%d\n", len(failedFiles))
for i, name := range failedFiles { for i, name := range failedFiles {
if i >= 5 { if i >= 5 {
resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportProcessingMore, map[string]any{"Count": len(failedFiles) - 5})) resultText.WriteString(i18n.T(i18nk.BotMsgProgressTransferProcessingMore, map[string]any{"Count": len(failedFiles) - 5}))
break break
} }
fmt.Fprintf(&resultText, "- %s\n", name) fmt.Fprintf(&resultText, "- %s\n", name)

View File

@@ -1,4 +1,4 @@
package batchimport package transfer
import ( import (
"context" "context"
@@ -21,7 +21,7 @@ type TaskElement struct {
SourcePath string SourcePath string
FileInfo storagetypes.FileInfo FileInfo storagetypes.FileInfo
TargetStorage storage.Storage TargetStorage storage.Storage
TargetChatID int64 TargetPath string
} }
type Task struct { type Task struct {
@@ -44,7 +44,7 @@ func (t *Task) Title() string {
// Type implements core.Executable. // Type implements core.Executable.
func (t *Task) Type() tasktype.TaskType { func (t *Task) Type() tasktype.TaskType {
return tasktype.TaskTypeBatchimport return tasktype.TaskTypeTransfer
} }
// TaskID implements core.Executable. // TaskID implements core.Executable.
@@ -56,7 +56,7 @@ func NewTaskElement(
sourceStorage storage.Storage, sourceStorage storage.Storage,
fileInfo storagetypes.FileInfo, fileInfo storagetypes.FileInfo,
targetStorage storage.Storage, targetStorage storage.Storage,
targetChatID int64, targetPath string,
) *TaskElement { ) *TaskElement {
id := xid.New().String() id := xid.New().String()
return &TaskElement{ return &TaskElement{
@@ -65,11 +65,11 @@ func NewTaskElement(
SourcePath: fileInfo.Path, SourcePath: fileInfo.Path,
FileInfo: fileInfo, FileInfo: fileInfo,
TargetStorage: targetStorage, TargetStorage: targetStorage,
TargetChatID: targetChatID, TargetPath: targetPath,
} }
} }
func NewBatchImportTask( func NewTransferTask(
id string, id string,
ctx context.Context, ctx context.Context,
elems []TaskElement, elems []TaskElement,

View File

@@ -1,4 +1,4 @@
package batchimport package transfer
type TaskElementInfo interface { type TaskElementInfo interface {
FileName() string FileName() string

View File

@@ -1,5 +1,5 @@
package tasktype package tasktype
//go:generate go-enum --values --names --flag --nocase //go:generate go-enum --values --names --flag --nocase
// ENUM(tgfiles,tphpics,parseditem,directlinks,aria2,ytdlp,batchimport) // ENUM(tgfiles,tphpics,parseditem,directlinks,aria2,ytdlp,transfer)
type TaskType string type TaskType string

View File

@@ -24,8 +24,8 @@ const (
TaskTypeAria2 TaskType = "aria2" TaskTypeAria2 TaskType = "aria2"
// TaskTypeYtdlp is a TaskType of type ytdlp. // TaskTypeYtdlp is a TaskType of type ytdlp.
TaskTypeYtdlp TaskType = "ytdlp" TaskTypeYtdlp TaskType = "ytdlp"
// TaskTypeBatchimport is a TaskType of type batchimport. // TaskTypeTransfer is a TaskType of type transfer.
TaskTypeBatchimport TaskType = "batchimport" TaskTypeTransfer TaskType = "transfer"
) )
var ErrInvalidTaskType = fmt.Errorf("not a valid TaskType, try [%s]", strings.Join(_TaskTypeNames, ", ")) var ErrInvalidTaskType = fmt.Errorf("not a valid TaskType, try [%s]", strings.Join(_TaskTypeNames, ", "))
@@ -37,7 +37,7 @@ var _TaskTypeNames = []string{
string(TaskTypeDirectlinks), string(TaskTypeDirectlinks),
string(TaskTypeAria2), string(TaskTypeAria2),
string(TaskTypeYtdlp), string(TaskTypeYtdlp),
string(TaskTypeBatchimport), string(TaskTypeTransfer),
} }
// TaskTypeNames returns a list of possible string values of TaskType. // TaskTypeNames returns a list of possible string values of TaskType.
@@ -56,7 +56,7 @@ func TaskTypeValues() []TaskType {
TaskTypeDirectlinks, TaskTypeDirectlinks,
TaskTypeAria2, TaskTypeAria2,
TaskTypeYtdlp, TaskTypeYtdlp,
TaskTypeBatchimport, TaskTypeTransfer,
} }
} }
@@ -79,7 +79,7 @@ var _TaskTypeValue = map[string]TaskType{
"directlinks": TaskTypeDirectlinks, "directlinks": TaskTypeDirectlinks,
"aria2": TaskTypeAria2, "aria2": TaskTypeAria2,
"ytdlp": TaskTypeYtdlp, "ytdlp": TaskTypeYtdlp,
"batchimport": TaskTypeBatchimport, "transfer": TaskTypeTransfer,
} }
// ParseTaskType attempts to convert a string to a TaskType. // ParseTaskType attempts to convert a string to a TaskType.

View File

@@ -50,6 +50,10 @@ type Add struct {
// ytdlp // ytdlp
YtdlpURLs []string YtdlpURLs []string
YtdlpFlags []string YtdlpFlags []string
// transfer
TransferSourceStorName string
TransferSourcePath string
TransferFiles []string // file paths relative to source storage
} }
type SetDefaultStorage struct { type SetDefaultStorage struct {

View File

@@ -51,6 +51,7 @@ func (l *Local) JoinStoragePath(path string) string {
func (l *Local) Save(ctx context.Context, r io.Reader, storagePath string) error { func (l *Local) Save(ctx context.Context, r io.Reader, storagePath string) error {
l.logger.Infof("Saving file to %s", storagePath) l.logger.Infof("Saving file to %s", storagePath)
storagePath = l.JoinStoragePath(storagePath)
ext := filepath.Ext(storagePath) ext := filepath.Ext(storagePath)
base := strings.TrimSuffix(storagePath, ext) base := strings.TrimSuffix(storagePath, ext)

View File

@@ -77,13 +77,13 @@ func (m *Minio) JoinStoragePath(p string) string {
func (m *Minio) Save(ctx context.Context, r io.Reader, storagePath string) error { func (m *Minio) Save(ctx context.Context, r io.Reader, storagePath string) error {
m.logger.Infof("Saving file from reader to %s", storagePath) m.logger.Infof("Saving file from reader to %s", storagePath)
storagePath = m.JoinStoragePath(storagePath)
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++ { for i := 1; m.Exists(ctx, candidate); i++ {
candidate = fmt.Sprintf("%s_%d%s", base, i, ext) candidate = fmt.Sprintf("%s_%d%s", base, i, ext)
if i > 100 { if i > 10 {
m.logger.Errorf("Too many attempts to find a unique filename for %s", storagePath) m.logger.Errorf("Too many attempts to find a unique filename for %s", storagePath)
candidate = fmt.Sprintf("%s_%s%s", base, xid.New().String(), ext) candidate = fmt.Sprintf("%s_%s%s", base, xid.New().String(), ext)
break break

View File

@@ -65,7 +65,7 @@ func (m *S3) JoinStoragePath(p string) string {
func (m *S3) Save(ctx context.Context, r io.Reader, storagePath string) error { func (m *S3) Save(ctx context.Context, r io.Reader, storagePath string) error {
m.logger.Infof("Saving file from reader to %s", storagePath) m.logger.Infof("Saving file from reader to %s", storagePath)
storagePath = m.JoinStoragePath(storagePath)
ext := path.Ext(storagePath) ext := path.Ext(storagePath)
base := strings.TrimSuffix(storagePath, ext) base := strings.TrimSuffix(storagePath, ext)
candidate := storagePath candidate := storagePath
@@ -73,7 +73,7 @@ func (m *S3) Save(ctx context.Context, r io.Reader, storagePath string) error {
// Unique filename // Unique filename
for i := 1; m.Exists(ctx, candidate); i++ { for i := 1; m.Exists(ctx, candidate); i++ {
candidate = fmt.Sprintf("%s_%d%s", base, i, ext) candidate = fmt.Sprintf("%s_%d%s", base, i, ext)
if i > 100 { if i > 10 {
m.logger.Errorf("Too many attempts for unique filename: %s", storagePath) m.logger.Errorf("Too many attempts for unique filename: %s", storagePath)
candidate = fmt.Sprintf("%s_%s%s", base, xid.New().String(), ext) candidate = fmt.Sprintf("%s_%s%s", base, xid.New().String(), ext)
break break

View File

@@ -21,7 +21,6 @@ type Storage interface {
Init(ctx context.Context, cfg storcfg.StorageConfig) error Init(ctx context.Context, cfg storcfg.StorageConfig) error
Type() storenum.StorageType Type() storenum.StorageType
Name() string Name() string
JoinStoragePath(p string) string
Save(ctx context.Context, reader io.Reader, storagePath string) error Save(ctx context.Context, reader io.Reader, storagePath string) error
Exists(ctx context.Context, storagePath string) bool Exists(ctx context.Context, storagePath string) bool
} }

View File

@@ -66,15 +66,12 @@ func (t *Telegram) Name() string {
return t.config.Name return t.config.Name
} }
func (t *Telegram) JoinStoragePath(p string) string {
return path.Clean(p)
}
func (t *Telegram) Exists(ctx context.Context, storagePath string) bool { func (t *Telegram) Exists(ctx context.Context, storagePath string) bool {
return false return false
} }
func (t *Telegram) Save(ctx context.Context, r io.Reader, storagePath string) error { func (t *Telegram) Save(ctx context.Context, r io.Reader, storagePath string) error {
storagePath = path.Clean(storagePath)
tctx := tgutil.ExtFromContext(ctx) tctx := tgutil.ExtFromContext(ctx)
if tctx == nil { if tctx == nil {
return fmt.Errorf("failed to get telegram context") return fmt.Errorf("failed to get telegram context")

View File

@@ -53,7 +53,7 @@ func (w *Webdav) JoinStoragePath(p string) string {
func (w *Webdav) Save(ctx context.Context, r io.Reader, storagePath string) error { func (w *Webdav) Save(ctx context.Context, r io.Reader, storagePath string) error {
w.logger.Infof("Saving file to %s", storagePath) w.logger.Infof("Saving file to %s", storagePath)
storagePath = w.JoinStoragePath(storagePath)
ext := path.Ext(storagePath) ext := path.Ext(storagePath)
base := strings.TrimSuffix(storagePath, ext) base := strings.TrimSuffix(storagePath, ext)
candidate := storagePath candidate := storagePath