feat(fs): W4-E4 fsproxy hang isolation, partial sweep, replace-with-verify, TIMEOUT retry
- adapters/fs: FsProxy + fsworker run each file op in a killable subprocess;
progress-stall watchdog (FS_HANG_TIMEOUT_MS) for copy/hash, total timeout for
quick ops; SIGTERM -> SIGKILL -> FsError('TIMEOUT'); stat/checkRoot timeouts
never read as missing
- copyWithVerify: onExisting conflict/skip/replace (hardlink backup + atomic
rename, old file to .umt-trash or deleted), sweep stale partials before write,
document why permissions/owner are not copied
- filesMatch/filesMatchSha256 throw FsError on read failure instead of
match=false; crossDeviceMove skips redundant re-hash after sha256 copy
- FsErrorCode TIMEOUT + fsErrorClass; organize executeRecord routes fs ops
through fsproxy, retries TIMEOUT/IO up to FS_TRANSFER_MAX_ATTEMPTS then opens
a decision without deleting the record; reconcile errors no longer reported
as EEXIST
- domain: subtitle .default marker (ORGANIZE_DEFAULT_SUBTITLE_LANG) and
findOtherVersions (same season/episode/part, video only) as upgrade
disposal candidates
- scripts/bench/fs-bench.ts and benchmark record in porting doc
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
36dc048e89
commit
2e2d56184c
21 files changed
+1970
-179
No files matched your search
@@ -79,7 +79,7 @@ export interface CrossMoveResult {
|
||||
steps: CrossMoveStepLog[];
|
||||
}
|
||||
|
||||
export interface CrossMoveOptions extends Omit<CopyOptions, "hash"> {
|
||||
export interface CrossMoveOptions extends Omit<CopyOptions, "hash" | "onExisting" | "replaceOld"> {
|
||||
hash?: VerifyMethod;
|
||||
/** 上次运行的步骤日志,用于中断续跑(只作参考,实际仍以文件系统观察为准) */
|
||||
previous?: CrossMoveStepLog[];
|
||||
@@ -103,6 +103,10 @@ async function exists(p: string): Promise<boolean> {
|
||||
*/
|
||||
export async function crossDeviceMove(src: string, dst: string, opts: CrossMoveOptions = {}): Promise<CrossMoveResult> {
|
||||
const method = opts.hash ?? "size";
|
||||
const io = {
|
||||
...(opts.signal ? { signal: opts.signal } : {}),
|
||||
...(opts.onVerifyProgress ? { onProgress: opts.onVerifyProgress } : {}),
|
||||
};
|
||||
const steps: CrossMoveStepLog[] = [];
|
||||
const log = (step: CrossMoveStep, status: CrossMoveStepLog["status"], detail?: string, err?: unknown) => {
|
||||
const e = err === undefined ? undefined : toFsError(err);
|
||||
@@ -136,11 +140,14 @@ export async function crossDeviceMove(src: string, dst: string, opts: CrossMoveO
|
||||
return { status: dstExists ? "attention" : "failed", steps };
|
||||
}
|
||||
|
||||
let fullyVerified = false;
|
||||
if (dstExists) {
|
||||
try {
|
||||
// 目标不是本次写出的:只比大小/抽样会把同尺寸的不同文件当成一致并删源,这里一律用 sha256
|
||||
if (await filesMatch(src, dst, "sha256")) log("copy", "skipped", "target already matches source");
|
||||
else {
|
||||
if (await filesMatch(src, dst, "sha256", io)) {
|
||||
log("copy", "skipped", "target already matches source");
|
||||
fullyVerified = true;
|
||||
} else {
|
||||
log("copy", "failed", "target exists and differs", new FsError("EEXIST", `target exists: ${dst}`));
|
||||
return { status: "failed", steps };
|
||||
}
|
||||
@@ -153,6 +160,8 @@ export async function crossDeviceMove(src: string, dst: string, opts: CrossMoveO
|
||||
const { previous: _p, ...copyOpts } = opts;
|
||||
const r = await copyWithVerify(src, dst, { ...copyOpts, hash: method });
|
||||
log("copy", "done", `${r.bytes} bytes`);
|
||||
// copy 已用流式哈希 + 回读临时文件全量比对,并确认源 size/mtime 未变:verify 不再重复全量哈希两边
|
||||
if (r.sha256) fullyVerified = true;
|
||||
} catch (e) {
|
||||
log("copy", "failed", undefined, e);
|
||||
return { status: "failed", steps };
|
||||
@@ -160,11 +169,11 @@ export async function crossDeviceMove(src: string, dst: string, opts: CrossMoveO
|
||||
}
|
||||
|
||||
try {
|
||||
if (!(await filesMatch(src, dst, method))) {
|
||||
if (!(await filesMatch(src, dst, fullyVerified ? "size" : method, io))) {
|
||||
log("verify", "failed", undefined, new FsError("VERIFY_FAILED", "target differs from source"));
|
||||
return { status: "failed", steps };
|
||||
}
|
||||
log("verify", "done", method);
|
||||
log("verify", "done", fullyVerified ? `${method} (sha256 verified in copy step)` : method);
|
||||
} catch (e) {
|
||||
log("verify", "failed", undefined, e);
|
||||
return { status: "failed", steps };
|
||||
|
||||
Reference in new issue
Block a user