// fsproxy 的操作表:主进程(in-process 回退)与执行子进程(fsworker.ts)共用同一份实现。 // 迁移来源:MoviePilot app/adapters/system/fsworker.py 的请求/响应模型(e6820a8),只借语义不借代码。 import { unlink } from "node:fs/promises"; import { type CopyOptions, copyWithVerify, filesMatchSha256, sha256Full, sweepStalePartials } from "./copy.ts"; import { FsError, toFsError } from "./errors.ts"; import { type CrossMoveOptions, crossDeviceMove, moveSafe } from "./move.ts"; import { checkRoot, statSafe } from "./stat.ts"; /** 可序列化的复制选项(去掉信号与回调) */ export type WireCopyOptions = Omit; export type WireCrossMoveOptions = Omit; /** 测试开关:执行子进程在累计进度达到 hangAfterBytes 后阻塞 JS 线程(模拟卡死的系统调用);ignoreTerm 屏蔽 SIGTERM */ export interface FsTestHook { hangAfterBytes?: number; ignoreTerm?: boolean; } export type FsOpRequest = ( | { op: "copy"; src: string; dst: string; opts?: WireCopyOptions } | { op: "crossMove"; src: string; dst: string; opts?: WireCrossMoveOptions } | { op: "move"; src: string; dst: string } | { op: "match"; a: string; b: string } | { op: "sha256"; path: string } | { op: "stat"; path: string; root?: string; sentinel?: string } | { op: "checkRoot"; root: string; sentinel?: string } | { op: "sweep"; dir: string; olderThanMs: number; suffix?: string } | { op: "unlink"; path: string } ) & { _test?: FsTestHook }; export type FsOpName = FsOpRequest["op"]; /** 按"进度停滞"判挂死的数据类操作;其余是快操作,按总时长判超时 */ export const DATA_OPS: ReadonlySet = new Set(["copy", "crossMove", "match", "sha256"]); export interface FsProgress { phase: string; bytes: number; total: number; } export interface FsOpContext { signal: AbortSignal; progress: (p: FsProgress) => void; } export async function runFsOp(req: FsOpRequest, ctx: FsOpContext): Promise { const { signal, progress } = ctx; const at = (phase: string) => (bytes: number, total: number) => progress({ phase, bytes, total }); switch (req.op) { case "copy": return copyWithVerify(req.src, req.dst, { ...req.opts, signal, onProgress: at("copy"), onVerifyProgress: at("verify"), }); case "crossMove": return crossDeviceMove(req.src, req.dst, { ...req.opts, signal, onProgress: at("copy"), onVerifyProgress: at("verify"), }); case "move": return moveSafe(req.src, req.dst); case "match": return filesMatchSha256(req.a, req.b, { signal, onProgress: at("hash") }); case "sha256": return sha256Full(req.path, signal, at("hash")); case "stat": return statSafe(req.path, { ...(req.root ? { root: req.root } : {}), ...(req.sentinel ? { sentinel: req.sentinel } : {}), }); case "checkRoot": return checkRoot(req.root, req.sentinel); case "sweep": return sweepStalePartials(req.dir, req.olderThanMs, req.suffix); case "unlink": try { await unlink(req.path); return null; } catch (e) { throw toFsError(e, req.path); } default: throw new FsError("IO", `unknown fs op: ${(req as { op: string }).op}`); } }