Skip to content

fix(drivers): 修复 remove/move/copy 路径重复拼接与分享复制链接为空 - #90

Merged
PIKACHUIM merged 7 commits into
OpenListTeam:mainfrom
Astroptis:main
Sep 29, 2026
Merged

PIKACHUIM merged 7 commits into
OpenListTeam:mainfrom
Astroptis:main

Conversation

@Astroptis

@Astroptis Astroptis commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary / 概要

修复实际部署中报告的两个问题,并对同一根因做全仓库批量修复。

1. 管理后台分享「复制链接」复制出空内容

  • 根因:share_summary_content 的种子值为空字符串,writeText("") 会清空剪贴板。
  • 修复:默认模板改为 {{base_url}}/@s/{{id}}(只复制纯 URL,与上游 OpenList Go 的整段分享文案不同,这是本 PR 明确的行为差异,请 maintainer 确认是否接受);db.ts 与 /api/public/settings 两处默认值同步;新增设置迁移,把历史空值与旧文案一次性迁移到新模板。
  • 兼容性:仅改默认值与迁移,不改接口结构;已有非空且非旧文案的自定义模板不会被动。

2. 对象存储删除显示 Success 但文件仍在(s3 实测)

  • 根因:op/storage.ts 的 removeItems/moveItems/copyItems 逐项调用驱动,传入的 physicalPath 已经是目标项自身的物理路径(含文件名)。s3 却把它当目录再拼一次 name,得到 <file>/<name>,HEAD 404 后递归删除也查不到,于是静默返回成功。
  • 修复:s3 的 remove/move/copy 直接使用该项路径(仅 names.length > 1 时才按目录展开,属防御分支)。
  • 回归测试:新增 src/backend/drivers/s3/s3_remove.test.ts,用内存 S3 mock 断言 DELETE/PUT 命中真实 key。

3. 同一根因的批量修复(21 个驱动,含评审补漏的 local)

azure_blob、uss、chunk、terabox、mopan、onedrive、onedrive_app、pikpak、mediatrack、google_drive、mega、dropbox、wps、yandex、seafile、ftp、sftp、webdav、alias、local、139。

评审跟进:local 由 评审评论 指出漏改——它是 drivers/local.ts 单文件而非 drivers/local/driver.ts,首轮按目录扫描时未命中。它的 remove 对 <item>/<name> 执行 fs.rm(..., { force: true }),ENOENT 被 force 吞掉造成静默假成功但什么都没删,是该 bug 形态里症状最重的一个;move/copy 则直接 ENOENT。已按同一约定修复,并新增 local_remove.test.ts 用临时目录真实 fs 断言(修复前 4 例全红)。

4. 响应评审的两项建议

  • 删除 names.length > 1 防御分支(commit 3f9329c):该分支在真实调用链里恒不可达(op/storage.ts 恒传单元素 names),且会把参数语义从「项路径」反转为「目录」,属潜伏陷阱。已在全部 21 个驱动中统一删除,一律按「参数即目标项自身路径」直接处理;names 保留于签名(接口要求)。行为不变(分支本就不可达)。
  • 补齐回归测试(commit 9039f64):见 Testing 节,新增 6 个驱动共 13 例。

各 API 的语义差异按「physical 即目标项」重新推导,例如:

  • terabox filemanager 的 dest 是目标目录取 dstPhys 父级、newname 取末段;
  • pikpak / mediatrack 的 resolveParentId 实际返回路径自身 id,故先去掉末段再解析;
  • chunk 分片回退路径的 dst 端改为目标项自己的分片目录;
  • alias 先取父目录再做别名映射,最后把 name 交给下层 removeItems。

审查后确认本就正确的实现未改动:kodbox、halalcloud_open、url_tree、netease_music、189、189_tv、smb、google_drive/util 等。

其他

  • This PR has breaking changes.
    / 本 PR 包含破坏性变更。
  • This PR changes public API, config, storage format, or migration behavior.
    / 本 PR 修改了公开 API、配置、存储格式或迁移行为。
    新增 share_summary_content 设置项迁移(空值与旧文案 → 新默认模板)。
  • This PR requires corresponding changes in related repositories.
    / 本 PR 需要相关仓库同步修改。
    OpenList(Go) 与 OpenList-Docs 无对应 PR:Go 端分享文案行为保持不变,本 PR 只改 Worker 侧默认值。

Related repository PRs / 相关仓库 PR:

  • OpenList: —
  • OpenList-Docs: —

Testing / 测试

  • go test ./...(本仓库为 TypeScript,未涉及 Go 代码)
  • Manual test / 手动测试:
pnpm run lint            # tsc --noEmit, 0 errors
pnpm run test:all        # 425 tests pass (drivers=130 含 189=20, server=173,
                         # model=39, store=12, regress=71)
node node_modules\prettier\bin\prettier.cjs --end-of-line auto --check <changed files>
  • 新增回归测试 src/backend/drivers/s3/s3_remove.test.ts(内存 S3 mock,先复现失败,修复后通过)。
  • 新增回归测试 src/backend/drivers/local_remove.test.ts(临时目录真实 fs,覆盖 remove/move/copy,修复前 4 例全红)。
  • 响应评审补齐 6 个驱动的 remove/move 回归测试(13 例,全部通过、无真实网络连接):
    • dropbox / yandex / webdav / onedrive:mock globalThis.fetch,断言请求里的路径参数为目标项自身路径;
    • sftp:注入 fake sftpClient(利用 getSFTP() 缓存点),断言 unlink/rename 收到项路径;
    • ftp:替换私有 client(dropbox/driver.test.ts 先例),断言 removeRecursive/rename 参数。
    • ftp/sftp 无法用 mock.module(Node 未开 --experimental-test-module-mocks),故采用注入方式。
  • 已部署到 Cloudflare Workers 手动验证:复制链接粘贴得到 https://<host>/@s/<id> 纯 URL;s3 删除后对象确实消失。

Checklist / 检查清单

  • I have read CONTRIBUTING.
    / 我已阅读 CONTRIBUTING。
  • I confirm this contribution follows the repository license, contribution policy, and code of conduct.
    / 我确认此贡献符合仓库许可证、贡献政策和行为准则。
  • I have formatted the changed code with gofmt, go fmt, or prettier where applicable.
    / 我已对改动代码使用适用的格式化工具。
    本次新增/修改的代码已通过 prettier 检查;少数上游文件(如 db.ts、public.ts)本就存在历史格式问题,为避免无关 diff 未做全量重排。
  • I have requested review from relevant maintainers or code owners where applicable.
    / 我已按需请求相关维护者或代码所有者审查。

AI Disclosure / AI 使用披露

  • This PR includes AI-assisted content.
    / 本 PR 包含 AI 辅助内容。

Tools used / 使用过的工具:

  • ChatGPT
  • Codex
  • GitHub Copilot
  • Claude
  • Gemini
  • Other (please specify) / 其他(请注明): OpenCode (model: opencode/mimo-v2.6-flash-free)

Usage scope / 使用范围:

  • Code generation / 代码生成
  • Refactoring / 重构
  • Documentation / 文档
  • Tests / 测试
  • Translation / 翻译
  • Review assistance / 审查辅助

分工说明:AI 负责根因定位、代码与测试编写、以及 20 个驱动的同类问题审查;关键决策(复制链接改为纯 URL、批量修复范围、是否保留防御性分支)与线上部署、真机验证由仓库所有者完成。

  • I have reviewed and validated all AI-assisted content included in this PR.
    / 我已审查并验证本 PR 中的全部 AI 辅助内容。
  • I have ensured that all AI-assisted commits include Co-Authored-By attribution.
    / 我已确保所有 AI 辅助提交均包含 Co-Authored-By 署名。
  • I can reproduce all AI-assisted content included in this PR without any AI tools.
    / 我可以在没有任何 AI 工具的情况下复现本 PR 中的全部 AI 辅助内容。

Astroptis and others added 4 commits September 27, 2026 00:45
S3 的 remove/move/copy 按「目录 + name」拼接路径,但 op/storage.ts 传入的
physicalPath 已经是目标项自身的物理路径,结果指向 <file>/<name>:HEAD 404
后退化成按目录递归删除,listObjects 同样查不到对象,最后静默返回成功——
接口报 Success,对象仍在(move/copy 同根因)。新增 s3_remove.test.ts 用内存
S3 锁定删除/移动实际命中的 key。

分享摘要模板 share_summary_content 被 seed 成空串,前端复制链接 =
matchTemplate(getSetting("share_summary_content"), data),模板为空时
writeText("") 会清空剪贴板,却照样弹「已复制」。默认值改为与 Go setting.go
一致的模板,并加迁移把已部署实例 KV 里的空串换成默认值;/api/public/settings
同步补默认值兜底。

Co-Authored-By: opencode <noreply@opencode.ai>
管理后台「复制链接」渲染 share_summary_content 模板。此前该模板被 seed
成空串,复制出的是空白(writeText("") 直接清空剪贴板却照样弹「已复制」);
上一版修空值时改用了上游 Go 的整段社交文案,按钮叫 Copy link 却复制出一堆
非链接内容——而文件工具栏「分享」对话框里的同名按钮 copy(link()) 复制的
一直是纯 URL。

默认模板改为 {{base_url}}/@s/{{id}},与按钮文案及另一处「复制链接」行为
保持一致;空串与旧文案两种已落盘的值都由 LEGACY_SETTING_MIGRATIONS 迁移
成纯链接,/api/public/settings 的默认值同步更新。

Co-Authored-By: opencode <noreply@opencode.ai>
op/storage.ts 的 removeItems/moveItems/copyItems 逐项调用 driver,传入的
physicalPath / srcPhys / dstPhys 是目标项自身的物理路径(resolvePath 已经把
name 解析进去)。多数 driver 却把它当作目录再拼一次 name,得到 `<item>/<name>`
这种不存在的路径:

- remove:删除不存在的路径。接口静默返回成功,对象仍在——用户报的
  「对象存储删除显示 Success 但文件还在」就是这个根因(s3 已单独修复);
- move/copy:源/目标路径错位,操作静默失败或作用到错误对象。

按同一约定修复 20 个 driver:azure_blob, uss, chunk, terabox, mopan, onedrive,
onedrive_app, pikpak, mediatrack, google_drive, mega, dropbox, wps, yandex,
seafile, ftp, sftp, webdav, alias, 139。

各 API 的语义差异按「physical 即目标项」重新推导:如 terabox filemanager 的
dest 是目标目录取 dstPhys 父级、名字取末段;pikpak/mediatrack 的
resolveParentId 实际返回路径自身 id,故先去掉末段再解析。names.length > 1
时保留「目录 + 多个 name」的展开分支(调用方恒传 1 个,属防御性代码)。

审查后确认本就正确的实现未改动:kodbox, halalcloud_open, url_tree,
netease_music, 189, 189_tv, smb, google_drive/util 等。

Co-Authored-By: opencode <noreply@opencode.ai>
mediatrack/pikpak 的三元表达式换行与 public_settings 测试断言
折行不符合仓库 prettier 配置,重排为单行/标准折行。

Co-Authored-By: opencode <noreply@opencode.ai>
@pikachuren

This comment has been minimized.

Astroptis and others added 3 commits September 27, 2026 22:38
评审指出 drivers/local.ts 是单文件(非 drivers/local/driver.ts),此前批量
修复时被漏检。remove 把项路径当目录再拼一次 name,对 <item>/<name> 执行
fs.rm({force:true}) 时不存在的路径被 force 忽略,静默成功但文件仍在;
move/copy 则直接 ENOENT。

与其余 21 个驱动按同一约定修复:physicalPath/srcPhys/dstPhys 即目标项
自身,仅 names.length > 1 时按目录展开。新增 local_remove.test.ts,
用临时目录真实 fs 断言 remove/move/copy 命中目标项。

Co-Authored-By: opencode <noreply@opencode.ai>
评审指出:各驱动修复时加的 expand 分支在真实调用链里恒不可达
(op/storage.ts 的 removeItems/moveItems/copyItems 恒传单元素 names),
且它把参数语义从「项路径」反转成「目录」,属潜伏陷阱——将来若有
调用方传多 name,行为会与单 name 时不一致。

统一删除:所有 remove/move/copy 一律按「physicalPath/srcPhys/dstPhys
即目标项自身路径」直接处理,names 保留于签名(接口要求)。
行为不变(该分支本就不可达),342 项测试全过。

Co-Authored-By: opencode <noreply@opencode.ai>
…e 回归测试

评审指出 22 个改动驱动里只有 s3/local 有回归保护。按可 mock 性补齐 6 个:
- dropbox/yandex/webdav/onedrive:mock globalThis.fetch,断言 DELETE/POST/MOVE
  请求的路径参数为目标项自身路径(无 <item>/<name> 二次拼接)
- sftp:注入 fake sftpClient(getSFTP 缓存点),断言 unlink/rename 收到项路径
- ftp:替换私有 client(dropbox 先例),断言 removeRecursive/rename 参数

ftp/sftp 无法 mock.module(Node 无实验标志),故用注入方式;6 文件共 13 例,
均验证通过且不发起真实网络连接。

Co-Authored-By: opencode <noreply@opencode.ai>
@Astroptis

Copy link
Copy Markdown
Contributor Author

@pikachuren 感谢细致的评审,三项都已处理:

1. 🔴 local.ts 漏改 —— 已修复(7b303b7)

确认属实:它是 drivers/local.ts 单文件而非 drivers/local/driver.ts,首轮按目录扫描时未命中。已按建议改:remove 直接 fs.rm(physicalPath, { recursive: true, force: true })(去掉 for + join),move/copy 直接用 srcPhys/dstPhys。新增 local_remove.test.ts(临时目录真实 fs,覆盖 remove 文件/目录、move、copy),修复前 4 例全红、修复后全绿。

2. names.length > 1 防御分支 —— 已删除(3f9329c)

同意「语义反转是潜伏陷阱」的判断。21 个驱动的 52 处 expand 分支已全部删除,一律按「physicalPath/srcPhys/dstPhys 即目标项自身路径」直接处理,names 保留于签名(接口要求)。行为无变化(该分支本就不可达),342 项测试全过。

3. 测试缺口 —— 已补 6 个驱动 13 例(9039f64)

  • dropbox / yandex / webdav / onedrive:mock globalThis.fetch,断言请求里的路径参数为目标项自身路径;
  • sftp:注入 fake sftpClient(getSFTP() 的缓存点),断言 unlink/rename 收到项路径;
  • ftp:替换私有 client(dropbox/driver.test.ts 有先例),断言 removeRecursive/rename 参数。

ftp/sftp 无法用 mock.module(本仓库测试命令未加 --experimental-test-module-mocks,实测 mock.module 为 undefined),故采用注入方式。全部通过且不发起真实网络连接。

关于 chunk/wps 的 pre-existing 守则问题:同意单独跟踪,不在本 PR 范围。

关于分享迁移的 release note 建议:采纳,会在合并说明里注明「恰好等于 Go 默认文案的自定义 share_summary_content 也会被迁移到纯链接模板」。

当前状态:tsc --noEmit 0 错误;test:all 425 项全过(drivers=130 含 189=20、server=173、model=39、store=12、regress=71)。

@pikachuren pikachuren left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🙏 感谢 @Astroptis 提交!
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 DeepSeek-V4.1-Flash 模型进行分析。
⚠️ AI 分析结果仅供参考,可能存在误判或遗漏。如您发现任何问题或有不同意见,欢迎随时提出讨论和纠正。
⚠️ 重要提醒:即使 AI 评审认为代码质量良好且建议合并,最终是否合并仍需由项目维护者进行人工判定。项目维护者会综合考虑代码质量、项目规划、技术方向、团队资源等多方面因素做出是否合并的决策。

🔄 增量评审:v1(0e34eba) → v2(9039f64)

本轮新增 3 个 commits、27 个文件(+1178/−883):7b303b7 修 local.ts、3f9329c 删除不可达防御分支、9039f64 补 6 驱动回归测试。

新增改动的问题:

  • 💡 [P2] local.ts:99、139/driver.ts:206 等约 20 个驱动的 remove/move/copy 中,names 在删掉防御分支后已成未使用参数,但仍保留原名;仓库既有写法(crypt/driver.ts:142、baidu_netdisk/driver.ts:203)用 _names 前缀表示「有意不使用」。建议统一为 _names 以贴合既有约定(tsconfig 未开 noUnusedParameters,不影响编译,仅约定一致性)。
  • 💡 [P2] 部分驱动改为从路径末段反推目标名(如 139/driver.ts:210、pikpak/driver.ts 的 split("/").pop() || "")。当前调用链下末段恒等于 name,无实际差异;但若物理路径带尾部斜杠,末段会解析为 "",查找落空后静默 no-op,与本次要消除的「静默失败」属同类症状。建议以调用方权威值兜底,同时消掉未使用参数:
    const name = names[0] || clean.substring(clean.lastIndexOf("/") + 1)
    const dirPath = clean.substring(0, clean.lastIndexOf("/")) || "/"

旧问题解决情况(含本轮本地实跑验证):

  • ✅ local.ts 漏改(P0)→ 已改 fs.rm(physicalPath, {recursive:true, force:true}),move/copy 直接用 srcPhys/dstPhys,与同文件 list/get/mkdir/rename/put 语义一致;新增 4 例测试回退后 4/4 全红,修复到位。
  • ✅ names.length > 1 防御分支(P1)→ 52 处已删除。已独立核对调用链:server/fs.ts:735/768/801(批量入口 cleanNames 可多元素)、fs.ts:1234/1270、server/s3.ts:169、server/webdav.ts:180/195/210 及 alias/chunk/crypt 的转发,全部经 op/storage.ts 逐项循环以 [name] 单元素下发驱动(storage.ts:1581-1589/1607-1627/1645-1665),分支确不可达,行为不变。
  • ✅ 测试缺口(P1)→ 7 文件 17 例,且做了红/绿对照:webdav 3/3、sftp 2/2、yandex 2/2、ftp 2/2、dropbox 2/2、onedrive 2/2、local 4/4,回退到修复前实现后全部变红,测试真实锁定修复、无空转。
  • ✅ 迁移会在 release note 提示的 P2 建议 → PR 描述已注明,采纳。
  • ⏳ chunk/wps 的 pre-existing 守则问题 → 同意单独跟踪,不在本 PR 范围。

本轮额外做的漏改排查(上轮曾靠此找出 local.ts):

  1. 全 drivers/ 检索消费 names 的位置 → 仅 alias/alist_v3/baidu_netdisk/kodbox/openlist/url_tree 在用,均为「父目录 + names」或 names[0] 作 newname 的正确用法;chunk/crypt 为包装驱动,转发的是项路径。
  2. 检索 joinPath/posixJoin/join(physicalPath|srcPhys|dstPhys, …) 与 ${physicalPath}/${…} → 仅命中 chunk/driver.ts:136、local.ts:36,均在 list() 内拼子项路径,正确。
  3. 另查「dst 端被误当目录」形态(dest/dst_dir/parent_id 直取 dstPhys)→ 相关驱动均取目标目录或父级,正确。

全量回归本地实跑:tsc -p tsconfig.json --noEmit 0 错误;test:189 20/20、test:drivers 130/130、test:server 173/173、test:store 12/12、test:model 39/39、test:regress 71/71(去重合计 425/425),与你的声明一致。

🎯 结论:✅ Approve — 上轮 1 个 P0 + 2 个 P1 全部按建议修复且经验证有效,本轮未引入阻塞问题;剩余 2 个 P2 为可选优化(命名约定统一、以 names[0] 兜底增强尾部斜杠健壮性),不阻塞合并。

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