Skip to content

新增功能: 重名文件时选择保存策略 - 重命名,覆盖,跳过#204

Merged
krau merged 3 commits into
krau:mainfrom
Rain-kl:feat/save-strategy
May 20, 2026
Merged

新增功能: 重名文件时选择保存策略 - 重命名,覆盖,跳过#204
krau merged 3 commits into
krau:mainfrom
Rain-kl:feat/save-strategy

Conversation

@Rain-kl
Copy link
Copy Markdown
Contributor

@Rain-kl Rain-kl commented May 4, 2026

bot 下载文件前先检查文件名是否存在重复, 如果重复则要求用户选择保存策略

重命名: 现有策略-重命名文件末尾加_1
覆盖: 覆盖文件
跳过: 不处理

image

feat: implement conflict resolution strategy for file uploads
Copilot AI review requested due to automatic review settings May 4, 2026 04:31
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR adds duplicate-name handling for Telegram file downloads by prompting the user to choose a save strategy when a target filename already exists.

Changes:

  • Adds conflict-strategy selection (rename, overwrite, skip) to the add-task flow and persists the choice through callback data.
  • Propagates overwrite behavior into storage backends and adds skipped-file reporting to batch task messages/progress.
  • Adds new i18n strings and context keys needed for the conflict prompt and result summaries.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
storage/webdav/webdav.go Updates WebDAV save logic to honor overwrite mode and refactors existence checks.
storage/s3/s3.go Updates S3 save logic to skip renaming when overwrite is selected.
storage/rclone/rclone.go Gates the existing rename loop behind the overwrite flag for rclone storage.
storage/minio/client.go Adds overwrite-aware save behavior and a helper for object existence checks.
storage/local/local.go Adds overwrite-aware local save behavior and refactors path existence checks.
storage/context.go Adds context helpers for setting and reading overwrite mode.
storage/alist/alist.go Adds overwrite-aware save behavior and refactors existence checks for Alist.
pkg/tcbdata/data.go Extends callback payload data with conflict-strategy and selected directory path.
pkg/enums/ctxkey/context_key_enum.go Registers the new overwrite-existing context key.
pkg/enums/ctxkey/context_key.go Updates enum source definition for the new context key.
core/tasks/batchtfile/progress.go Adds skipped-file reporting to batch progress completion output.
common/i18n/locale/zh-Hans.yaml Adds Chinese strings for conflict prompts/buttons and skipped-file notices.
common/i18n/locale/en.yaml Adds English strings for conflict prompts/buttons and skipped-file notices.
common/i18n/i18nk/keys.go Adds i18n key constants for the new conflict-related messages.
client/bot/handlers/utils/shortcut/tftask.go Adds conflict prompting, skip/overwrite handling, and skipped-file summaries in task creation helpers.
client/bot/handlers/utils/msgelem/storage.go Adds inline keyboard generation for conflict-strategy selection.
client/bot/handlers/add_task.go Adds conflict pre-checking in the add-task callback flow before creating TG file tasks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread storage/webdav/webdav.go
Comment on lines +61 to +63
if overwrite, _ := ctx.Value(ctxkey.OverwriteExisting).(bool); !overwrite {
for i := 1; w.existsPath(ctx, candidate); i++ {
candidate = fmt.Sprintf("%s_%d%s", base, i, ext)
Comment thread storage/webdav/webdav.go
Comment on lines +85 to 89
return w.existsPath(ctx, w.JoinStoragePath(storagePath))
}

func (w *Webdav) existsPath(ctx context.Context, storagePath string) bool {
exists, err := w.client.Exists(ctx, storagePath)
Comment thread storage/s3/s3.go
Comment on lines +73 to +76
if overwrite, _ := ctx.Value(ctxkey.OverwriteExisting).(bool); !overwrite {
// Unique filename
for i := 1; m.existsKey(ctx, candidate); i++ {
candidate = fmt.Sprintf("%s_%d%s", base, i, ext)
Comment thread storage/s3/s3.go
Comment on lines +104 to +108
return m.existsKey(ctx, m.JoinStoragePath(storagePath))
}

func (m *S3) existsKey(ctx context.Context, key string) bool {
return m.client.Exists(ctx, key)
Comment thread storage/minio/client.go
Comment on lines +84 to +86
if overwrite, _ := ctx.Value(ctxkey.OverwriteExisting).(bool); !overwrite {
for i := 1; m.existsObject(ctx, candidate); i++ {
candidate = fmt.Sprintf("%s_%d%s", base, i, ext)
Comment thread client/bot/handlers/add_task.go Outdated
Comment on lines +84 to +102
conflicts, err := findTGFileConflicts(ctx, userID, selectedStorage, dirPath, data.Files)
if err != nil {
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, err.Error()))
return dispatcher.EndGroups
}
if len(conflicts) > 0 && data.ConflictStrategy == "" {
markup, err := msgelem.BuildConflictStrategyMarkup(data)
if err != nil {
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, i18n.T(i18nk.BotMsgCommonErrorBuildStorageSelectKeyboardFailed, map[string]any{
"Error": err.Error(),
})))
return dispatcher.EndGroups
}
ctx.EditMessage(userID, &tg.MessagesEditMessageRequest{
ID: update.CallbackQuery.GetMsgID(),
Message: i18n.T(i18nk.BotMsgCommonPromptSelectConflictStrategy, map[string]any{"Files": formatConflictFiles(conflicts)}),
ReplyMarkup: markup,
})
return dispatcher.EndGroups
Comment on lines +105 to +107
return shortcut.CreateAndAddBatchTGFileTaskWithEdit(ctx, userID, selectedStorage, dirPath, data.Files, msgID, data.ConflictStrategy)
}
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, userID, selectedStorage, dirPath, data.Files[0], msgID)
return shortcut.CreateAndAddTGFileTaskWithEdit(ctx, userID, selectedStorage, dirPath, data.Files[0], msgID, data.ConflictStrategy)
Comment thread client/bot/handlers/add_task.go Outdated
if len(conflicts) > 0 && data.ConflictStrategy == "" {
markup, err := msgelem.BuildConflictStrategyMarkup(data)
if err != nil {
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, i18n.T(i18nk.BotMsgCommonErrorBuildStorageSelectKeyboardFailed, map[string]any{
Comment on lines +329 to +332
return i18n.T(i18nk.BotMsgCommonInfoBatchTasksAddedWithSkipped, map[string]any{
"Count": count,
"Skipped": strings.Join(skipped, "\n"),
})
Comment on lines +160 to +162
return styling.Plain("\n\n" + i18n.T(i18nk.BotMsgCommonInfoConflictFilesSkipped, map[string]any{
"Skipped": strings.Join(p.skippedFiles, "\n"),
}))
@krau
Copy link
Copy Markdown
Owner

krau commented May 5, 2026

hi, 感谢贡献. 该功能在本地已经测试过了吗? 有没有增加把某个策略设为默认的选项呢? (比如某些频道的几乎所有文件的文件名完全一样, 保存它们的时候如果要一一选择会不太方便)

@Rain-kl
Copy link
Copy Markdown
Contributor Author

Rain-kl commented May 6, 2026

你说得对, 我补充提交了默认策略选项, 可以通过/config 命令进行配置,
默认策略为"始终重命名"防止升级上去出现问题
另外我已经在本地测试了,没有发现问题

image iShot_2026-05-06_15 11 01

Comment thread client/bot/handlers/add_task.go Outdated
return nil, err
}
for _, file := range afiles {
storagePath := path.Join(dirPath, albumDir, file.Name())
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里用 dirPath 而不是 fileDirPath 的话逻辑正确吗

Comment thread client/bot/handlers/config.go Outdated
return dispatcher.EndGroups
}

func effectiveUserConflictStrategy(user *database.User) string {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个函数和 client/bot/handlers/utils/shortcut/tftask.go 中的 userConflictStrategy 完全一样, 建议还是复用一下

Comment on lines +184 to +189
if fileStor.Exists(ctx, storPath) {
if strategy == tcbdata.ConflictStrategyAsk {
conflicts = append(conflicts, fmt.Sprintf("[%s]:%s", fileStor.Name(), storPath))
}
}
if strategy == tcbdata.ConflictStrategySkip && fileStor.Exists(ctx, storPath) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里的俩 fileStor.Exists 建议合并为一个, 因为对于远端存储来说 Exists 检查有网络开销

Comment thread storage/context.go Outdated
return context.WithValue(ctx, ctxkey.OverwriteExisting, true)
}

func ShouldOverwrite(ctx context.Context) bool {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ShouldOverwrite 放在这里的话, 由于循环依赖, 也无法被内部的 storage 实现调用, 鉴于所有 storage 内部都自己做了 if overwrite, _ := ctx.Value(ctxkey.OverwriteExisting).(bool); !overwrite... 的处理, 这里的函数删了即可

Comment thread client/bot/handlers/add_task.go Outdated
Comment on lines +89 to +94
conflicts, err := findTGFileConflicts(ctx, userID, selectedStorage, dirPath, data.Files)
if err != nil {
ctx.AnswerCallback(msgelem.AlertCallbackAnswer(queryID, err.Error()))
return dispatcher.EndGroups
}
if len(conflicts) > 0 && strategy == tcbdata.ConflictStrategyAsk {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里是不是应该先判断策略是否是 ask, 再做 findTGFileConflicts , 避免不必要的检查

@krau
Copy link
Copy Markdown
Owner

krau commented May 18, 2026

@Rain-kl 不好意思前段时间忙, 有空的话可以看一下 review 吗

@Rain-kl
Copy link
Copy Markdown
Contributor Author

Rain-kl commented May 20, 2026

@krau 感谢建议, 本次提交对代码进行了一定的质量优化

add_task.go 去掉了提前冲突扫描逻辑, 只把用户选择的冲突策略传给后续任务构建流程, 避免在策略不是“每次询问”时做不必要的远端 Exists 检查
把重复的冲突策略逻辑抽到了 client/bot/handlers/utils/conflictutil, config.go 和 shortcut/tftask.go 现在复用同一套实现,减少重复代码
合并 tftask.go 中重复的 fileStor.Exists 调用。现在最多检查一次,再根据策略决定加入冲突列表或跳过列表,减少远端存储网络开销

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants