Files
UHD_Media_Tool/packages/adapters/src/fs/errors.ts
T
rootandClaude Opus 5.5 521c86d730 fix(storage): 本地写入口拒绝 Alist 虚拟路径,补字幕在 Alist 库根上明确失败
虚拟前缀 /@alist/… 读到本地是 ENOENT,但 mkdir -p、复制 / 移动目标、写文件、回收站落点会在本机真建出
/@alist 并写入文件。统一守卫:任何本地写类操作参数是虚拟路径即抛 REMOTE_PATH_ON_LOCAL_FS
(remote_path_on_local_fs),不落盘。

- adapters:fs/remote-guard.ts,接在 runFsOp、FsProxy.run(父进程拒绝不起子进程)、copyWithVerify、
  crossDeviceMove、moveSafe、commitNoClobber、sweepStalePartials、writeRootSentinel
- server:storage/local-fs.ts 带守卫的写函数,整理 / 替换 / 刮削 / 回收站 / 上传 / 字幕 / vfs 本地分支改用它
- 补字幕目标在 Alist 媒体库上返回失败原因,撤销同样拒绝

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-07 00:35:41 +08:00

78 lines
2.3 KiB
TypeScript

export type FsErrorCode =
| "EXDEV" // 跨设备,不会自动降级
| "EEXIST" // 目标已存在(no-clobber)
| "ENOENT"
| "EACCES"
| "ENOSPC"
| "VERIFY_FAILED"
| "SOURCE_CHANGED"
| "ROOT_OFFLINE"
| "ABORTED"
/** 文件操作挂死(进度停滞/快操作超时),执行子进程已被回收;与 IO 同属可退避重试,不代表文件缺失 */
| "TIMEOUT"
/** 本地文件操作收到 Alist 虚拟路径(/@alist/…):调用方没按存储分派,拒绝而不是写错位置 */
| "REMOTE_PATH_ON_LOCAL_FS"
| "IO";
/**
* 错误码 → 处置类别(对照 MP:所有 IO 故障归 OSError 复用重试;我们更细):
* - retry:可退避重试(IO / TIMEOUT / ROOT_OFFLINE / SOURCE_CHANGED),超限后进待处理
* - attention:直接进待处理、不重试(EACCES / ENOSPC / EXDEV)
* - manual:需人工判断(EEXIST / VERIFY_FAILED / ENOENT)
* - aborted:被取消
*/
export type FsErrorClass = "retry" | "attention" | "manual" | "aborted";
export function fsErrorClass(code: string): FsErrorClass {
switch (code) {
case "IO":
case "TIMEOUT":
case "ROOT_OFFLINE":
case "SOURCE_CHANGED":
return "retry";
case "EACCES":
case "ENOSPC":
case "EXDEV":
return "attention";
case "ABORTED":
return "aborted";
default:
return "manual";
}
}
export class FsError extends Error {
override readonly name = "FsError";
constructor(
readonly code: FsErrorCode,
message: string,
readonly detail: { path?: string; syscall?: string; cause?: unknown } = {},
) {
super(message);
}
}
export function errnoOf(e: unknown): string | undefined {
return (e as NodeJS.ErrnoException | undefined)?.code;
}
export function toFsError(e: unknown, path?: string): FsError {
if (e instanceof FsError) return e;
const code = errnoOf(e);
const mapped: FsErrorCode =
code === "EXDEV" || code === "EEXIST" || code === "ENOENT" || code === "ENOSPC"
? code
: code === "EACCES" || code === "EPERM"
? "EACCES"
: code === "ETIMEDOUT"
? "TIMEOUT"
: (e as Error)?.name === "AbortError"
? "ABORTED"
: "IO";
return new FsError(mapped, (e as Error)?.message ?? String(e), {
...(path !== undefined ? { path } : {}),
...((e as NodeJS.ErrnoException)?.syscall ? { syscall: (e as NodeJS.ErrnoException).syscall! } : {}),
cause: e,
});
}