fix(files): 回收站清除先删文件再删行,顺带清过期的无登记 replaced-* 目录

- purge 先删文件、成功后再删行,删文件失败时行留着下次再清,不再留下没有登记的孤儿文件;
- 清过期项时扫各可写库根 .umt-trash/replaced-*,过期且没有任何回收站登记的整个删掉(替换旧文件登记回收站失败时留下的)。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-03 23:37:18 +08:00
1 parent c532060c83
commit 4b49be7228
2 files changed
+58 -7

No files matched your search

+16 -1
View File
@@ -1,4 +1,4 @@
import { access, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
import { access, mkdir, mkdtemp, readFile, rm, symlink, utimes, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { findCrossDeviceBase } from "@umt/testkit";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
@@ -171,6 +171,21 @@ describe.skipIf(skip)("files 删除与回收站", () => {
expect(left.map((x) => x.id)).not.toContain(oldId);
});
it("purge 过期项时,过期且没有登记的 .umt-trash/replaced-* 一并清掉", async () => {
const oldBin = join(h.lib, ".umt-trash", "replaced-2020-01-01T00-00-00-000Z-aaaaaa");
const newBin = join(h.lib, ".umt-trash", "replaced-2099-01-01T00-00-00-000Z-bbbbbb");
await mkdir(oldBin, { recursive: true });
await mkdir(newBin, { recursive: true });
await writeFile(join(oldBin, "old.mkv"), "o");
await writeFile(join(newBin, "new.mkv"), "n");
const past = new Date(Date.now() - 30 * 86_400_000);
await utimes(oldBin, past, past);
const r = await h.api("POST", "/files/trash/purge", { idempotencyKey: key() }, SESSION);
expect(r.status).toBe(200);
expect(await exists(oldBin)).toBe(false);
expect(await exists(join(newBin, "new.mkv"))).toBe(true);
});
it("永久删除默认关闭;API Key 删除需一次性审批", async () => {
await writeFile(join(h.lib, "perm.txt"), "p");
const p = await del({ items: [{ root: h.libRoot, path: "perm.txt" }], mode: "permanent", dryRun: false });