Skip to content

fix: 本地上传的视频被拉黑后无法恢复 - #89

Merged
nianzhibai merged 2 commits into
nianzhibai:mainfrom
thazjswe42700:fix/restore-blacklisted-local-uploads
Aug 14, 2026
Merged

fix: 本地上传的视频被拉黑后无法恢复#89
nianzhibai merged 2 commits into
nianzhibai:mainfrom
thazjswe42700:fix/restore-blacklisted-local-uploads

Conversation

@thazjswe42700

@thazjswe42700 thazjswe42700 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

问题

前台点「不再展示」会删掉视频记录并写一条黑名单墓碑(OnHideVideodeleteVideo(ctx, id, false))。对本地上传的视频,这个操作不可逆

  • deletedVideoRestorePolicydrive_id == "local-upload" 判为 none,后台黑名单页面因此连「取消拉黑」按钮都不渲染,只显示「不可自动恢复」
  • 但拉黑时 deleteSource=false源文件其实还在 uploads 目录里
  • 之所以判为不可恢复,是因为 localupload.Driver.List() 直接返回 drives.ErrNotSupportedinternal/drives/localupload/driver.go:32),这个盘无法枚举,没有任何扫盘或爬取能重新发现它;maintainLocalUploadFileNames 也只遍历已入库的视频,不扫描目录

结果是:文件占着磁盘,视频永远回不来,用户在后台没有任何补救手段,只能重新上传一遍。

改动

新增第四种恢复策略 direct,适用于「源文件由本应用保留、但无法枚举」的来源(目前只有本地上传)。取消拉黑时当场重建记录,而不是去等一场永远不会到来的扫盘。

恢复是无损的:DeleteVideoWithTombstoneOptions 本来就把完整的 Video JSON 存进了 restore_payloadcatalog.go:1128),所以标题、作者、标签、简介、时长都能还原。派生资源(封面、预览、转码产物)在拉黑时已被物理删除,因此重置为 pending 交给生成 worker 重建 —— 保留原路径会让记录指向不存在的文件。

因为 direct 是唯一无条件写回记录的路径,恢复前会先让调用方确认保留下来的源文件仍然存在(RemoveDeletedVideoWithSourceCheck)。其他策略靠「扫不到就不入库」天然安全,不需要这层校验,也不会触发它。

实现上参考了爬虫已有的 RestoreRequestedVideos——「保留文件 + 墓碑 → 重新入库」这套流程本来就跑通了,本地上传只是缺了对应的那一半。

兼容性

策略判定的顺序做了调整,把「源文件已删」和「重复文件」提到来源判断之前:

if v == nil || v.SourceDeleted || v.Reason == duplicate {
    return none          // 与来源无关,最先命中
}
if v.DriveID == "local-upload" { return direct }
if driveKind == "scriptcrawler" { return crawler }
return scan

所以行为发生变化的只有「本地上传 + 源文件还在 + 非重复」这一种组合,也就是这个 bug 本身。源文件已删的本地上传、被去重删掉的本地上传、普通网盘、爬虫来源,行为全部不变,这三条都有测试锁定。

restorePolicy 全项目只有两个消费点(ListDeletedVideos 展示、RemoveDeletedVideo 分支判断);扫盘拦截、去重、备份恢复、源文件删除队列都是直接查 SQL,不读这个字段。

RemoveDeletedVideo(ctx, id) 的签名保持不变,11 个既有调用点一个都没改。

测试

后端新增/更新 6 个用例(internal/catalog + internal/api):

  • 本地上传 → direct
  • 源文件已删的本地上传 → 仍为 none(优先级回归)
  • 被去重删掉的本地上传 → 仍为 none(优先级回归)
  • 无损恢复:元数据保留、派生资源重置、墓碑清除、计数正确
  • 老墓碑 restore_payload 为空时的兜底(标题从文件名还原)
  • API 层:恢复成功并调用源文件校验 / 源文件缺失返回 409 且不恢复 / 扫盘来源不触发校验且仍是等下次扫盘

前端新增 1 个用例(tests/adminResponsive.test.ts),481 个前端测试全绿,tsc --noEmit 干净。

纯上游代码基(8cda4a5)上验证过:go build ./... 通过,internal/catalog 与黑名单相关测试全部通过。另外跑了全量回归对比(上游基线 vs 本改动),失败集合零新增。

🤖 Generated with Claude Code

Follow-up 修正

Review 后补充了系统级完整性修复(8d09e75):

  • direct 恢复改为在同一事务中写回视频、标签关系并删除 tombstone,同时兼容旧版恢复 payload,避免留下部分恢复状态。
  • 恢复前重新读取 provider 元数据,拒绝缺失、空文件或拉黑后已被替换的源文件;成功后主动进入封面、预览和转码生成队列。
  • 取消拉黑与后台源文件删除按视频 ID 串行化;删除任务会重新校验 tombstone 快照,已恢复或发生变化的条目按 skipped 处理,避免误删恢复后的源文件。
  • 管理端统一使用简短确认文案,确认按钮改为“确认”,移除重复的“本地上传”徽标,并展示删除任务的 skipped 计数。
  • 补充 catalog、API、server 与前端回归测试。

thazjswe42700 and others added 2 commits August 14, 2026 22:40
Hiding a video from the front end deletes its row and writes a blacklist
tombstone. Local uploads were then classified as unrestorable, so the admin
UI offered no way back and the video was gone for good even though its file
was still on disk: the local-upload drive returns ErrNotSupported from List,
so no scan or crawl can ever rediscover it.

Add a fourth restore policy, "direct", for sources whose file this
application retains but cannot enumerate. Removing such a tombstone rebuilds
the catalog row on the spot from the restore payload the tombstone already
stores, so title, author, tags and description survive. Derived assets were
deleted with the original row, so thumbnail, preview and transcode state are
reset to pending for the generation workers to rebuild.

Because this is the only restore path that writes a row back
unconditionally, it first asks the caller to confirm the retained source file
still exists; the other policies rely on a rescan that simply finds nothing
when the file is gone.

The policy checks are reordered so a deleted source and a deduplicated row
still resolve to "none" regardless of the drive, leaving scan and crawler
sources untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Restore retained local-upload sources directly, simplify the cancellation confirmation, and remove the redundant local-upload badge.
@nianzhibai
nianzhibai merged commit c6a9af9 into nianzhibai:main Aug 14, 2026
1 check passed
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.

2 participants