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:
1 parent
c532060c83
commit
4b49be7228
2 files changed
+58
-7
No files matched your search
@@ -584,7 +584,10 @@ export async function restoreTrash(
|
||||
return { restored, failed, recordId };
|
||||
}
|
||||
|
||||
/** 清除:先删行(复核仍存在)再删文件;ids 缺省 = 只清过期项 */
|
||||
/**
|
||||
* 清除:先删文件再删行(删文件失败时行留着,下次再清,不留没有登记的孤儿文件);ids 缺省 = 只清过期项,
|
||||
* 并顺带清掉各库根 `.umt-trash/replaced-*` 里过期、没有回收站登记的旧文件(替换旧文件登记回收站失败时留下的)。
|
||||
*/
|
||||
export async function purgeTrash(
|
||||
d: FmDeps,
|
||||
principal: Principal | undefined,
|
||||
@@ -598,18 +601,51 @@ export async function purgeTrash(
|
||||
for (const row of rows) {
|
||||
try {
|
||||
if (!trashPathSafe(d, row)) throw new FilesError("path_escape", "回收站路径异常,拒绝清除");
|
||||
const gone = await d.ctx.db.deleteFrom("trash_item").where("id", "=", row.id).returning("id").executeTakeFirst();
|
||||
if (!gone) continue;
|
||||
await rm(row.trash_path, { recursive: true, force: true });
|
||||
// 顺带清掉空的 <uuid>/<yyyymmdd> 目录
|
||||
const uuidDir = row.trash_path.slice(0, row.trash_path.length - row.original_path.length - 1);
|
||||
await rm(uuidDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
purged.push(row.id);
|
||||
const gone = await d.ctx.db.deleteFrom("trash_item").where("id", "=", row.id).returning("id").executeTakeFirst();
|
||||
if (gone) purged.push(row.id);
|
||||
} catch (e) {
|
||||
failed.push({ id: row.id, code: e instanceof FilesError ? e.reason : "io_error", message: errMessage(e) });
|
||||
}
|
||||
}
|
||||
if (purged.length || failed.length)
|
||||
await audit(d, principal, "trash_purge", { purged, failed, expiredOnly: !body.ids });
|
||||
const orphans = body.ids ? [] : await purgeReplacedOrphans(d);
|
||||
if (purged.length || failed.length || orphans.length)
|
||||
await audit(d, principal, "trash_purge", { purged, failed, orphans, expiredOnly: !body.ids });
|
||||
return { purged, failed };
|
||||
}
|
||||
|
||||
/** 各启用库根 `.umt-trash/replaced-*`:修改时间早于保留期、里面没有任何回收站登记的,整个删掉;返回删掉的目录 */
|
||||
async function purgeReplacedOrphans(d: FmDeps): Promise<string[]> {
|
||||
const out: string[] = [];
|
||||
const cutoff = d.ao.ports.now().getTime() - d.options.trashRetentionDays * 86_400_000;
|
||||
const roots = await d.ctx.db
|
||||
.selectFrom("library_root")
|
||||
.select(["local_path"])
|
||||
.where("enabled", "=", true)
|
||||
.where("management_mode", "=", "managed")
|
||||
.execute();
|
||||
for (const r of roots) {
|
||||
const rootReal = await realpath(r.local_path).catch(() => null);
|
||||
if (!rootReal) continue;
|
||||
const bin = join(rootReal, TRASH_DIR);
|
||||
for (const name of await readdir(bin).catch(() => [] as string[])) {
|
||||
if (!name.startsWith("replaced-")) continue;
|
||||
const dir = join(bin, name);
|
||||
const st = await lstat(dir).catch(() => null);
|
||||
if (!st?.isDirectory() || st.mtimeMs > cutoff) continue;
|
||||
const held = await d.ctx.db
|
||||
.selectFrom("trash_item")
|
||||
.select("id")
|
||||
.where("trash_path", "like", likePrefix(dir))
|
||||
.executeTakeFirst();
|
||||
if (held) continue;
|
||||
await rm(dir, { recursive: true, force: true })
|
||||
.then(() => out.push(dir))
|
||||
.catch(() => undefined);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in new issue
Block a user