From 3d20fbd0fe88989b38a60011d1b9b84d345d7f4b Mon Sep 17 00:00:00 2001 From: krau <71133316+krau@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:01:50 +0800 Subject: [PATCH] feat: implement transfer command for file transfers between storages --- client/bot/handlers/register.go | 2 +- .../bot/handlers/{import.go => transfer.go} | 111 +++++++++--------- common/i18n/i18nk/keys.go | 57 ++++----- common/i18n/locale/en.yaml | 49 ++++---- common/i18n/locale/zh-Hans.yaml | 49 ++++---- core/tasks/batchimport/execute.go | 13 +- core/tasks/batchimport/progress.go | 38 +++--- core/tasks/batchimport/task.go | 6 +- 8 files changed, 166 insertions(+), 159 deletions(-) rename client/bot/handlers/{import.go => transfer.go} (52%) diff --git a/client/bot/handlers/register.go b/client/bot/handlers/register.go index 7375702..8257fa4 100644 --- a/client/bot/handlers/register.go +++ b/client/bot/handlers/register.go @@ -31,7 +31,7 @@ var CommandHandlers = []DescCommandHandler{ {"dl", i18nk.BotMsgCmdDl, handleDlCmd}, {"aria2dl", i18nk.BotMsgCmdAria2dl, handleAria2DlCmd}, {"ytdlp", i18nk.BotMsgCmdYtdlp, handleYtdlpCmd}, - {"import", i18nk.BotMsgCmdImport, handleImportCmd}, + {"transfer", i18nk.BotMsgCmdTransfer, handleTransferCmd}, {"task", i18nk.BotMsgCmdTask, handleTaskCmd}, {"cancel", i18nk.BotMsgCmdCancel, handleCancelCmd}, {"config", i18nk.BotMsgCmdConfig, handleConfigCmd}, diff --git a/client/bot/handlers/import.go b/client/bot/handlers/transfer.go similarity index 52% rename from client/bot/handlers/import.go rename to client/bot/handlers/transfer.go index 4271e89..7d66f9b 100644 --- a/client/bot/handlers/import.go +++ b/client/bot/handlers/transfer.go @@ -3,6 +3,7 @@ package handlers import ( "fmt" "regexp" + "strings" "github.com/celestix/gotgproto/dispatcher" "github.com/celestix/gotgproto/ext" @@ -12,8 +13,6 @@ import ( "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" @@ -21,81 +20,105 @@ import ( "github.com/rs/xid" ) -func handleImportCmd(ctx *ext.Context, update *ext.Update) error { +func handleTransferCmd(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) + ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgTransferUsage, nil)), nil) return dispatcher.EndGroups } - storageName := args[1] - dirPath := args[2] + // 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] + + // Parse target: storage_name:/path + targetParts := strings.SplitN(args[2], ":", 2) + if len(targetParts) != 2 { + ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgTransferErrorInvalidTarget, nil)), nil) + return dispatcher.EndGroups + } + targetStorageName := targetParts[0] + targetPath := targetParts[1] userID := update.GetUserChat().GetID() - stor, err := storage.GetStorageByUserIDAndName(ctx, userID, storageName) + // Get source storage + sourceStorage, err := storage.GetStorageByUserIDAndName(ctx, userID, sourceStorageName) 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, + 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 } - listable, ok := stor.(storage.StorageListable) + // Check if source storage supports listing + listable, ok := sourceStorage.(storage.StorageListable) if !ok { - ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgImportErrorStorageNotListable, map[string]any{ - "StorageName": storageName, + ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgTransferErrorStorageNotListable, map[string]any{ + "StorageName": sourceStorageName, })), nil) return dispatcher.EndGroups } - _, ok = stor.(storage.StorageReadable) + // Check if source storage supports reading + _, ok = sourceStorage.(storage.StorageReadable) if !ok { - ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgImportErrorStorageNotReadable, map[string]any{ - "StorageName": storageName, + ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgTransferErrorStorageNotReadable, map[string]any{ + "StorageName": sourceStorageName, })), nil) return dispatcher.EndGroups } - telegramStorage, err := storage.GetTelegramStorageByUserID(ctx, userID) + // Get target storage + targetStorage, err := storage.GetStorageByUserIDAndName(ctx, userID, targetStorageName) if err != nil { - ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgImportErrorNoTelegramStorage, map[string]any{ - "Error": err, + logger.Errorf("Failed to get target storage by user ID and name: %s", err) + ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgTransferErrorTargetNotFound, map[string]any{ + "StorageName": targetStorageName, + "Error": err, })), nil) return dispatcher.EndGroups } - replied, err := ctx.Reply(update, ext.ReplyTextString(i18n.T(i18nk.BotMsgImportInfoFetchingFiles, nil)), nil) + // 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, dirPath) + files, err := listable.ListFiles(ctx, sourcePath) if err != nil { ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{ ID: replied.ID, - Message: i18n.T(i18nk.BotMsgImportErrorListFilesFailed, map[string]any{"Error": err}), + Message: i18n.T(i18nk.BotMsgTransferErrorListFilesFailed, map[string]any{"Error": err}), }) return dispatcher.EndGroups } + // Optional filter var filter *regexp.Regexp - if len(args) >= 5 { - filter, err = regexp.Compile(args[4]) + if len(args) >= 4 { + filter, err = regexp.Compile(args[3]) if err != nil { ctx.EditMessage(update.EffectiveChat().GetID(), &tg.MessagesEditMessageRequest{ ID: replied.ID, - Message: i18n.T(i18nk.BotMsgImportErrorInvalidRegex, map[string]any{"Error": err}), + 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 { @@ -110,47 +133,21 @@ func handleImportCmd(ctx *ext.Context, update *ext.Update) error { 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), + Message: i18n.T(i18nk.BotMsgTransferErrorNoFilesToTransfer, nil), }) return dispatcher.EndGroups } + // Create task elements elems := make([]batchimport.TaskElement, 0, len(filteredFiles)) var totalSize int64 for _, file := range filteredFiles { - elem := batchimport.NewTaskElement(stor, file, telegramStorage, targetChatID) + elem := batchimport.NewTaskElement(sourceStorage, file, targetStorage, targetPath) elems = append(elems, *elem) totalSize += file.Size } + // Create and add task taskID := xid.New().String() injectCtx := tgutil.ExtWithContext(ctx.Context, ctx) task := batchimport.NewBatchImportTask( @@ -164,14 +161,14 @@ func handleImportCmd(ctx *ext.Context, update *ext.Update) error { 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}), + Message: i18n.T(i18nk.BotMsgTransferErrorAddTaskFailed, 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{ + Message: i18n.T(i18nk.BotMsgTransferInfoTaskAdded, map[string]any{ "Count": len(elems), "SizeMB": fmt.Sprintf("%.2f", float64(totalSize)/(1024*1024)), "TaskID": taskID, diff --git a/common/i18n/i18nk/keys.go b/common/i18n/i18nk/keys.go index 29af390..0dc77bd 100644 --- a/common/i18n/i18nk/keys.go +++ b/common/i18n/i18nk/keys.go @@ -31,6 +31,7 @@ const ( BotMsgCmdStorage Key = "bot.msg.cmd.storage" BotMsgCmdSyncpeers Key = "bot.msg.cmd.syncpeers" BotMsgCmdTask Key = "bot.msg.cmd.task" + BotMsgCmdTransfer Key = "bot.msg.cmd.transfer" BotMsgCmdUnwatch Key = "bot.msg.cmd.unwatch" BotMsgCmdUpdate Key = "bot.msg.cmd.update" BotMsgCmdWatch Key = "bot.msg.cmd.watch" @@ -106,20 +107,6 @@ const ( BotMsgDlInfoFilesSelectStorage Key = "bot.msg.dl.info_files_select_storage" BotMsgDlUsage Key = "bot.msg.dl.usage" 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" BotMsgMediaGroupInfoGroupFoundFilesSelectStorage Key = "bot.msg.media_group.info_group_found_files_select_storage" BotMsgMediaGroupInfoSavingFiles Key = "bot.msg.media_group.info_saving_files" @@ -164,20 +151,6 @@ const ( BotMsgProgressFileProcessingPrefix Key = "bot.msg.progress.file_processing_prefix" BotMsgProgressFileSizePrefix Key = "bot.msg.progress.file_size_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" BotMsgProgressParsedStartPrefix Key = "bot.msg.progress.parsed_start_prefix" BotMsgProgressProcessingListPrefix Key = "bot.msg.progress.processing_list_prefix" @@ -190,6 +163,20 @@ const ( BotMsgProgressTelegraphProgressPrefix Key = "bot.msg.progress.telegraph_progress_prefix" BotMsgProgressTelegraphStartPrefix Key = "bot.msg.progress.telegraph_start_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" BotMsgProgressYtdlpDownloading Key = "bot.msg.progress.ytdlp_downloading" BotMsgProgressYtdlpStart Key = "bot.msg.progress.ytdlp_start" @@ -245,6 +232,20 @@ const ( BotMsgTelegraphInfoPicCountPrefix Key = "bot.msg.telegraph.info_pic_count_prefix" BotMsgTelegraphInfoPromptSelectStorage Key = "bot.msg.telegraph.info_prompt_select_storage" BotMsgTelegraphInfoTitlePrefix Key = "bot.msg.telegraph.info_title_prefix" + BotMsgTransferErrorAddTaskFailed Key = "bot.msg.transfer.error_add_task_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" + 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" BotMsgUpdateErrorCheckLatestFailed Key = "bot.msg.update.error_check_latest_failed" BotMsgUpdateErrorNoReleaseFound Key = "bot.msg.update.error_no_release_found" diff --git a/common/i18n/locale/en.yaml b/common/i18n/locale/en.yaml index 2cb38eb..b7fe555 100644 --- a/common/i18n/locale/en.yaml +++ b/common/i18n/locale/en.yaml @@ -54,6 +54,7 @@ bot: aria2dl: "Download files using Aria2" ytdlp: "Download video/audio using yt-dlp" import: "Import files from storage to Telegram" + transfer: "Transfer files between storages" task: "Manage task queue" cancel: "Cancel task" watch: "Watch chats (UserBot)" @@ -296,20 +297,26 @@ bot: info_urls_select_storage: "Found {{.Count}} links, please select storage" info_downloading: "Downloading via yt-dlp..." error_download_failed: "yt-dlp download failed: {{.Error}}" - import: - usage: "Usage: /import [target_chat_id] [filter]\n\nExamples:\n/import local1 /downloads\n/import MyAlist /media/photos -1001234567890\n/import MyLocal /backup \".*[.]mp4$\"" + transfer: + usage: | + Usage: /transfer :/ :/ [filter] + Examples: + /transfer local1:/downloads tg1:/backup + /transfer alist1:/media/photos local1:/photos + /transfer webdav1:/files tg1:/archive ".*\.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_listable: "Storage '{{.StorageName}}' does not support listing 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..." error_list_files_failed: "Failed to list files: {{.Error}}" error_invalid_regex: "Invalid regular expression: {{.Error}}" - error_no_files_to_import: "No files to import 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_no_files_to_transfer: "No files to transfer in directory" 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" cancel: usage: "Usage: /cancel " error_cancel_failed: "Failed to cancel task: {{.Error}}" @@ -358,20 +365,20 @@ bot: ytdlp_done: "yt-dlp download completed and transferred ({{.Count}} files)\n" downloaded_prefix: "\nDownloaded: " current_speed_prefix: "\nCurrent speed: " - import_start_prefix: "Importing: " - import_progress_prefix: "Import progress: " - import_uploaded_prefix: "\nUploaded: " - import_speed_prefix: "\nSpeed: " - import_remaining_time_prefix: "\nRemaining time: " - import_processing_prefix: "\nProcessing:\n" - import_processing_more: "...and {{.Count}} more files\n" - import_failed_prefix: "Import failed\n" - import_success_prefix: "Import completed\n" - import_total_files_prefix: "\nTotal files: " - import_total_size_prefix: "\nTotal size: " - import_elapsed_time_prefix: "\nElapsed time: " - import_avg_speed_prefix: "\nAverage speed: " - import_failed_files_prefix: "\nFailed files: " + transfer_start_prefix: "Importing: " + transfer_progress_prefix: "Import progress: " + transfer_uploaded_prefix: "\nUploaded: " + transfer_speed_prefix: "\nSpeed: " + transfer_remaining_time_prefix: "\nRemaining time: " + transfer_processing_prefix: "\nProcessing:\n" + transfer_processing_more: "...and {{.Count}} more files\n" + transfer_failed_prefix: "Import failed\n" + transfer_success_prefix: "Import completed\n" + transfer_total_files_prefix: "\nTotal files: " + transfer_total_size_prefix: "\nTotal size: " + transfer_elapsed_time_prefix: "\nElapsed time: " + transfer_avg_speed_prefix: "\nAverage speed: " + transfer_failed_files_prefix: "\nFailed files: " syncpeers: start: "Starting to sync peers..." done: "Peer sync completed, total {{.Count}} chats synced" diff --git a/common/i18n/locale/zh-Hans.yaml b/common/i18n/locale/zh-Hans.yaml index df073b3..7a144aa 100644 --- a/common/i18n/locale/zh-Hans.yaml +++ b/common/i18n/locale/zh-Hans.yaml @@ -55,6 +55,7 @@ bot: aria2dl: "使用 Aria2 下载给定链接的文件" ytdlp: "使用 yt-dlp 下载视频/音频" import: "从存储端导入文件到 Telegram" + transfer: "在存储端之间传输文件" task: "管理任务队列" cancel: "取消任务" watch: "监听聊天(UserBot)" @@ -297,25 +298,25 @@ bot: info_urls_select_storage: "共 {{.Count}} 个链接, 请选择存储位置" info_downloading: "正在通过 yt-dlp 下载..." error_download_failed: "yt-dlp 下载失败: {{.Error}}" - import: + transfer: usage: | - 用法: /import [target_chat_id] [filter] + 用法: /transfer :/ :/ [filter] 示例: - /import 本机1 /downloads - /import MyAlist /media/photos -1001234567890 - /import MyLocal /backup ".*\.mp4$" + /transfer local1:/downloads tg1:/backup + /transfer alist1:/media/photos local1:/photos + /transfer webdav1:/files tg1:/archive ".*\.mp4$" + error_invalid_source: "源路径格式无效,应为: storage_name:/path" + error_invalid_target: "目标路径格式无效,应为: storage_name:/path" error_storage_not_found: "存储端 '{{.StorageName}}' 不存在或您无权访问: {{.Error}}" error_storage_not_listable: "存储端 '{{.StorageName}}' 不支持列举文件功能" error_storage_not_readable: "存储端 '{{.StorageName}}' 不支持读取文件功能" - error_no_telegram_storage: "未找到可用的 Telegram 存储: {{.Error}}" + error_target_not_found: "目标存储端 '{{.StorageName}}' 不存在或您无权访问: {{.Error}}" info_fetching_files: "正在获取文件列表..." error_list_files_failed: "获取文件列表失败: {{.Error}}" error_invalid_regex: "正则表达式无效: {{.Error}}" - error_no_files_to_import: "目录中没有可导入的文件" - error_invalid_chat_id: "无效的 Chat ID: {{.Error}}" - error_no_target_chat_id: "未指定目标频道 ID,且 Telegram 存储未配置默认 chat_id" + error_no_files_to_transfer: "目录中没有可传输的文件" 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" cancel: usage: "用法: /cancel " @@ -365,20 +366,20 @@ bot: ytdlp_done: "yt-dlp 下载完成并已转存 ({{.Count}} 个文件)\n" downloaded_prefix: "\n已下载: " current_speed_prefix: "\n当前速度: " - import_start_prefix: "正在导入: " - import_progress_prefix: "导入进度: " - import_uploaded_prefix: "\n已上传: " - import_speed_prefix: "\n速度: " - import_remaining_time_prefix: "\n剩余时间: " - import_processing_prefix: "\n正在处理:\n" - import_processing_more: "...和其他 {{.Count}} 个文件\n" - import_failed_prefix: "导入失败\n" - import_success_prefix: "导入完成\n" - import_total_files_prefix: "\n总文件数: " - import_total_size_prefix: "\n总大小: " - import_elapsed_time_prefix: "\n耗时: " - import_avg_speed_prefix: "\n平均速度: " - import_failed_files_prefix: "\n失败文件数: " + transfer_start_prefix: "正在导入: " + transfer_progress_prefix: "导入进度: " + transfer_uploaded_prefix: "\n已上传: " + transfer_speed_prefix: "\n速度: " + transfer_remaining_time_prefix: "\n剩余时间: " + transfer_processing_prefix: "\n正在处理:\n" + transfer_processing_more: "...和其他 {{.Count}} 个文件\n" + transfer_failed_prefix: "导入失败\n" + transfer_success_prefix: "导入完成\n" + transfer_total_files_prefix: "\n总文件数: " + transfer_total_size_prefix: "\n总大小: " + transfer_elapsed_time_prefix: "\n耗时: " + transfer_avg_speed_prefix: "\n平均速度: " + transfer_failed_files_prefix: "\n失败文件数: " syncpeers: start: "正在同步对话列表..." success: "对话列表同步完成, 共同步 {{.Count}} 个对话" diff --git a/core/tasks/batchimport/execute.go b/core/tasks/batchimport/execute.go index b76d93f..9687cd3 100644 --- a/core/tasks/batchimport/execute.go +++ b/core/tasks/batchimport/execute.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "path" "path/filepath" "github.com/charmbracelet/log" @@ -84,15 +85,15 @@ func (t *Task) processElement(ctx context.Context, elem TaskElement) error { } defer reader.Close() - // Build Telegram storage path: // - storagePath := fmt.Sprintf("/%d/%s", elem.TargetChatID, elem.FileInfo.Name) + // Build target storage path: /target_path/filename + storagePath := path.Join(elem.TargetPath, elem.FileInfo.Name) - // 注入文件大小到 context + // Inject file size into context ctx = context.WithValue(ctx, ctxkey.ContentLength, size) if config.C().Stream { 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 { 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) } - 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 { - return fmt.Errorf("failed to upload file to telegram: %w", err) + return fmt.Errorf("failed to upload file to storage: %w", err) } } diff --git a/core/tasks/batchimport/progress.go b/core/tasks/batchimport/progress.go index fe89b02..c8fc74d 100644 --- a/core/tasks/batchimport/progress.go +++ b/core/tasks/batchimport/progress.go @@ -43,14 +43,14 @@ func (p *Progress) OnStart(ctx context.Context, info TaskInfo) { log.FromContext(ctx).Debugf("Batch import task progress tracking started for message %d in chat %d", p.MessageID, p.ChatID) sizeMB := float64(info.TotalSize()) / (1024 * 1024) - statsText := i18n.T(i18nk.BotMsgImportStartStats, map[string]any{ - "SizeMB": fmt.Sprintf("%.2f", sizeMB), - "Count": info.Count(), + statsText := i18n.T(i18nk.BotMsgTransferStartStats, map[string]any{ + "SizeMB": fmt.Sprintf("%.2f", sizeMB), + "Count": info.Count(), }) entityBuilder := entity.Builder{} if err := styling.Perform(&entityBuilder, - styling.Plain(i18n.T(i18nk.BotMsgProgressImportStartPrefix, nil)), + styling.Plain(i18n.T(i18nk.BotMsgProgressTransferStartPrefix, nil)), styling.Code(statsText), ); err != nil { log.FromContext(ctx).Errorf("Failed to build entities: %s", err) @@ -94,9 +94,9 @@ func (p *Progress) OnProgress(ctx context.Context, info TaskInfo) { entityBuilder := entity.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)) - progressText.WriteString(i18n.T(i18nk.BotMsgProgressImportUploadedPrefix, nil)) + progressText.WriteString(i18n.T(i18nk.BotMsgProgressTransferUploadedPrefix, nil)) progressText.WriteString(fmt.Sprintf("%.2f MB / %.2f MB", float64(info.Uploaded())/(1024*1024), float64(info.TotalSize())/(1024*1024))) @@ -104,22 +104,22 @@ func (p *Progress) OnProgress(ctx context.Context, info TaskInfo) { if p.start.Unix() > 0 { elapsed := time.Since(p.start) 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") if info.Uploaded() > 0 { 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)) } } processing := info.Processing() if len(processing) > 0 { - progressText.WriteString(i18n.T(i18nk.BotMsgProgressImportProcessingPrefix, nil)) + progressText.WriteString(i18n.T(i18nk.BotMsgProgressTransferProcessingPrefix, nil)) for i, elem := range processing { 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 } fmt.Fprintf(&progressText, "- %s\n", elem.FileName()) @@ -162,36 +162,36 @@ func (p *Progress) OnDone(ctx context.Context, info TaskInfo, err error) { var resultText strings.Builder if err != nil { - resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportFailedPrefix, nil)) + resultText.WriteString(i18n.T(i18nk.BotMsgProgressTransferFailedPrefix, nil)) resultText.WriteString(i18n.T(i18nk.BotMsgProgressErrorPrefix, nil)) fmt.Fprintf(&resultText, "%v\n", err) } else { - resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportSuccessPrefix, nil)) + resultText.WriteString(i18n.T(i18nk.BotMsgProgressTransferSuccessPrefix, nil)) } 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()) - 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)) - 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)) - resultText.WriteString(i18n.T(i18nk.BotMsgProgressImportElapsedTimePrefix, nil)) + resultText.WriteString(i18n.T(i18nk.BotMsgProgressTransferElapsedTimePrefix, nil)) fmt.Fprintf(&resultText, "%s\n", formatDuration(elapsed)) if elapsed.Seconds() > 0 { 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))) } failedFiles := info.FailedFiles() 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)) for i, name := range failedFiles { 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 } fmt.Fprintf(&resultText, "- %s\n", name) diff --git a/core/tasks/batchimport/task.go b/core/tasks/batchimport/task.go index 0536745..2f70288 100644 --- a/core/tasks/batchimport/task.go +++ b/core/tasks/batchimport/task.go @@ -21,7 +21,7 @@ type TaskElement struct { SourcePath string FileInfo storagetypes.FileInfo TargetStorage storage.Storage - TargetChatID int64 + TargetPath string } type Task struct { @@ -56,7 +56,7 @@ func NewTaskElement( sourceStorage storage.Storage, fileInfo storagetypes.FileInfo, targetStorage storage.Storage, - targetChatID int64, + targetPath string, ) *TaskElement { id := xid.New().String() return &TaskElement{ @@ -65,7 +65,7 @@ func NewTaskElement( SourcePath: fileInfo.Path, FileInfo: fileInfo, TargetStorage: targetStorage, - TargetChatID: targetChatID, + TargetPath: targetPath, } }