- 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]>
2.9 KiB
fs
本地文件操作:copyWithVerify(同目录隐藏临时文件 → fsync → size/sha256/抽样校验 → 提交 → fsync 目录,保留 mtime,不复制权限/属主,默认不建父目录;目标已存在按 onExisting:conflict 抛 EEXIST / skip 不写 / replace 旧文件硬链接留底 → rename 原子替换 → 留底进回收目录或删除,不裸覆盖;写前顺带清本目录 >24h 的 .uhd-partial)、moveSafe(同设备 no-clobber 移动,EXDEV 返回 exdev 不降级)、crossDeviceMove(copy → verify → delete_source,返回步骤日志,可重入,删源失败为 attention;copy 已做 sha256 时 verify 不再重复全量哈希)、statSafe/checkRoot(先查库根哨兵 .uhd-root,区分 missing / permission_denied / root_offline)、scanIncremental、ffprobe、diskUsage、sweepStalePartials、filesMatchSha256(读失败/超时抛 FsError,不返回 match=false 冒充"内容不同")。
挂死隔离:FsProxy(对标 MP fsproxy)
new FsProxy({ hangTimeoutMs, quickTimeoutMs, progressIntervalMs, killGraceMs, mode }) 把单个文件操作(copy / crossMove / move / match / sha256 / stat / checkRoot / sweep / unlink)放进一次性执行子进程 fsworker.ts(deno run --no-config -A,约 50 ms 启动),stdin 一条 JSON 请求,stdout 逐行心跳 + 结果。
- 数据类操作(复制、跨卷搬运、哈希)按进度停滞判挂死:
hangTimeoutMs(缺省 120 s)内字节数与阶段都没前进 → SIGTERM(worker 中止并清掉自己的 partial)→killGraceMs后 SIGKILL →FsError('TIMEOUT')。大文件复制几小时不会误杀。 - 快操作按总时长
quickTimeoutMs(缺省 30 s)。statSafe超时返回{kind:"error",code:"TIMEOUT"},checkRoot超时返回offline(timeout)——挂死不等于文件缺失。 - 卡在 D 状态的子进程被
unref,不拖住本进程;被 SIGKILL 的 partial 由下次写前清理收走。 - 子进程里 sha256 走流式(带心跳),不走
sha256sum(看不到进度);吞吐见docs/porting/moviepilot-rust-transfer.md基准记录。 mode: "auto":Deno.execPath()是 deno CLI 才用子进程;deno compile出的二进制回退 in-process(同样的看门狗,超时立即报 TIMEOUT 并中止,但卡死系统调用占住的线程无法回收)。要在编译二进制里用子进程,入口加--role fsworker调fsWorkerMain(),并以workerCommand: { cmd: execPath, args: ["--role","fsworker"] }构造 FsProxy(待办)。- 测试开关
_test: { hangAfterBytes, ignoreTerm }:worker 在累计进度达到阈值后阻塞 JS 线程,模拟卡死的系统调用。
错误码处置类别见 fsErrorClass:IO/TIMEOUT/ROOT_OFFLINE/SOURCE_CHANGED 可退避重试;EACCES/ENOSPC/EXDEV 进待处理;EEXIST/VERIFY_FAILED/ENOENT 需人工。
在本机临时目录与 /dev/shm(跨设备)上做了集成测试(含 FIFO 真实阻塞);权限拒绝类用例在 root 下跳过。未在真实 NAS/FUSE/网络挂载上验证。