From c33034a28941c8f2d205a2b57d4d7f193ff373c0 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 27 Sep 2026 19:01:46 +0800 Subject: [PATCH] fix(adapters): crossDeviceMove verifies pre-existing target with sha256 before deleting source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the default hash="size", a target that already existed with the same size but different content was treated as "already copied" and the source was deleted (data loss; REQ §2.1 cleanup needs verified import). The pre-existing-target comparison now always uses sha256; test covers the same-size/different-content case with default options. Co-Authored-By: Claude Fable 5.1 --- packages/adapters/src/fs/fs.test.ts | 4 ++++ packages/adapters/src/fs/move.ts | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/adapters/src/fs/fs.test.ts b/packages/adapters/src/fs/fs.test.ts index 4e799206..9020e1e8 100644 --- a/packages/adapters/src/fs/fs.test.ts +++ b/packages/adapters/src/fs/fs.test.ts @@ -121,6 +121,10 @@ describe("moveSafe / crossDeviceMove", () => { expect(await readFile(t.path("src/b.mkv"), "utf8")).toBe("bbbb"); const r4 = await crossDeviceMove(t.path("src/none.mkv"), t.path("dst/b.mkv")); expect(r4.status).toBe("attention"); + // 默认 hash=size 时,已存在的同尺寸异内容目标也必须判为不一致,不得删源 + const r5 = await crossDeviceMove(t.path("src/b.mkv"), t.path("dst/b.mkv")); + expect(r5.status).toBe("failed"); + expect(await readFile(t.path("src/b.mkv"), "utf8")).toBe("bbbb"); }); it.skipIf(isRoot)("删源失败 → attention(以 root 运行时跳过:root 无视目录权限)", async () => { diff --git a/packages/adapters/src/fs/move.ts b/packages/adapters/src/fs/move.ts index c779aa1e..6ae54d59 100644 --- a/packages/adapters/src/fs/move.ts +++ b/packages/adapters/src/fs/move.ts @@ -97,7 +97,7 @@ async function exists(p: string): Promise { /** * 跨设备搬运:copy → verify → delete_source,每步返回日志,可重入: - * - 目标已存在且与源一致 → copy 记为 skipped(上次已完成);不一致 → failed,不覆盖 + * - 目标已存在且与源一致(强制 sha256)→ copy 记为 skipped(上次已完成);不一致 → failed,不覆盖 * - 源已不存在但目标在,且上次日志 verify=done → 视为删源已完成 * - 删源失败 → attention(不能报成功) */ @@ -138,7 +138,8 @@ export async function crossDeviceMove(src: string, dst: string, opts: CrossMoveO if (dstExists) { try { - if (await filesMatch(src, dst, method)) log("copy", "skipped", "target already matches source"); + // 目标不是本次写出的:只比大小/抽样会把同尺寸的不同文件当成一致并删源,这里一律用 sha256 + if (await filesMatch(src, dst, "sha256")) log("copy", "skipped", "target already matches source"); else { log("copy", "failed", "target exists and differs", new FsError("EEXIST", `target exists: ${dst}`)); return { status: "failed", steps };