fix(drivers): 修复 remove/move/copy 路径重复拼接与分享复制链接为空 - #90
Conversation
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>
This comment has been minimized.
This comment has been minimized.
评审指出 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>
|
@pikachuren 感谢细致的评审,三项都已处理: 1. 🔴 确认属实:它是 2. 同意「语义反转是潜伏陷阱」的判断。21 个驱动的 52 处 3. 测试缺口 —— 已补 6 个驱动 13 例(
关于 关于分享迁移的 release note 建议:采纳,会在合并说明里注明「恰好等于 Go 默认文案的自定义 当前状态: |
pikachuren
left a comment
There was a problem hiding this comment.
🙏 感谢 @Astroptis 提交!
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 DeepSeek-V4.1-Flash 模型进行分析。
🔄 增量评审: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 例,且做了红/绿对照:
webdav3/3、sftp2/2、yandex2/2、ftp2/2、dropbox2/2、onedrive2/2、local4/4,回退到修复前实现后全部变红,测试真实锁定修复、无空转。 - ✅ 迁移会在 release note 提示的 P2 建议 → PR 描述已注明,采纳。
- ⏳
chunk/wps的 pre-existing 守则问题 → 同意单独跟踪,不在本 PR 范围。
本轮额外做的漏改排查(上轮曾靠此找出 local.ts):
- 全
drivers/检索消费names的位置 → 仅alias/alist_v3/baidu_netdisk/kodbox/openlist/url_tree在用,均为「父目录 + names」或names[0]作 newname 的正确用法;chunk/crypt为包装驱动,转发的是项路径。 - 检索
joinPath/posixJoin/join(physicalPath|srcPhys|dstPhys, …)与${physicalPath}/${…}→ 仅命中chunk/driver.ts:136、local.ts:36,均在list()内拼子项路径,正确。 - 另查「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] 兜底增强尾部斜杠健壮性),不阻塞合并。
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 后递归删除也查不到,于是静默返回成功。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。4. 响应评审的两项建议
names.length > 1防御分支(commit3f9329c):该分支在真实调用链里恒不可达(op/storage.ts恒传单元素names),且会把参数语义从「项路径」反转为「目录」,属潜伏陷阱。已在全部 21 个驱动中统一删除,一律按「参数即目标项自身路径」直接处理;names保留于签名(接口要求)。行为不变(分支本就不可达)。9039f64):见 Testing 节,新增 6 个驱动共 13 例。各 API 的语义差异按「physical 即目标项」重新推导,例如:
teraboxfilemanager的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等。其他
/ 本 PR 包含破坏性变更。
/ 本 PR 修改了公开 API、配置、存储格式或迁移行为。
新增
share_summary_content设置项迁移(空值与旧文案 → 新默认模板)。/ 本 PR 需要相关仓库同步修改。
OpenList(Go) 与 OpenList-Docs 无对应 PR:Go 端分享文案行为保持不变,本 PR 只改 Worker 侧默认值。
Related repository PRs / 相关仓库 PR:
Testing / 测试
go test ./...(本仓库为 TypeScript,未涉及 Go 代码)src/backend/drivers/s3/s3_remove.test.ts(内存 S3 mock,先复现失败,修复后通过)。src/backend/drivers/local_remove.test.ts(临时目录真实 fs,覆盖 remove/move/copy,修复前 4 例全红)。remove/move回归测试(13 例,全部通过、无真实网络连接):dropbox/yandex/webdav/onedrive:mockglobalThis.fetch,断言请求里的路径参数为目标项自身路径;sftp:注入 fakesftpClient(利用getSFTP()缓存点),断言unlink/rename收到项路径;ftp:替换私有 client(dropbox/driver.test.ts先例),断言removeRecursive/rename参数。ftp/sftp无法用mock.module(Node 未开--experimental-test-module-mocks),故采用注入方式。https://<host>/@s/<id>纯 URL;s3 删除后对象确实消失。Checklist / 检查清单
/ 我已阅读 CONTRIBUTING。
/ 我确认此贡献符合仓库许可证、贡献政策和行为准则。
gofmt,go fmt, orprettierwhere applicable./ 我已对改动代码使用适用的格式化工具。
本次新增/修改的代码已通过 prettier 检查;少数上游文件(如
db.ts、public.ts)本就存在历史格式问题,为避免无关 diff 未做全量重排。/ 我已按需请求相关维护者或代码所有者审查。
AI Disclosure / AI 使用披露
/ 本 PR 包含 AI 辅助内容。
Tools used / 使用过的工具:
opencode/mimo-v2.6-flash-free)Usage scope / 使用范围:
分工说明:AI 负责根因定位、代码与测试编写、以及 20 个驱动的同类问题审查;关键决策(复制链接改为纯 URL、批量修复范围、是否保留防御性分支)与线上部署、真机验证由仓库所有者完成。
/ 我已审查并验证本 PR 中的全部 AI 辅助内容。
Co-Authored-Byattribution./ 我已确保所有 AI 辅助提交均包含
Co-Authored-By署名。/ 我可以在没有任何 AI 工具的情况下复现本 PR 中的全部 AI 辅助内容。