// 本项目新增(REQ §8.2 挂载根哨兵);MP 无对应实现。 import { lstat, stat, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { errnoOf } from "./errors.ts"; export const DEFAULT_ROOT_SENTINEL = ".uhd-root"; export interface FileFacts { size: number; mtimeMs: number; ino: number; dev: number; nlink: number; isFile: boolean; isDirectory: boolean; isSymbolicLink: boolean; } export type StatResult = | { kind: "ok"; facts: FileFacts } | { kind: "missing" } | { kind: "permission_denied" } /** 挂载根哨兵不存在/不可读:不能据此判定文件缺失 */ | { kind: "root_offline"; root: string; reason: string } | { kind: "error"; code: string; message: string }; export type RootHealth = | { kind: "online"; dev: number } | { kind: "offline"; reason: "sentinel_missing" | "root_missing" | "permission_denied" | "error"; code?: string }; /** 检查库根/缓存根是否在线:根目录存在且哨兵文件存在。 */ export async function checkRoot(root: string, sentinel: string = DEFAULT_ROOT_SENTINEL): Promise { let dev: number; try { const st = await stat(root); dev = st.dev; } catch (e) { const code = errnoOf(e); if (code === "ENOENT") return { kind: "offline", reason: "root_missing" }; if (code === "EACCES" || code === "EPERM") return { kind: "offline", reason: "permission_denied", code }; return { kind: "offline", reason: "error", code: code ?? "UNKNOWN" }; } try { await stat(join(root, sentinel)); return { kind: "online", dev }; } catch (e) { const code = errnoOf(e); if (code === "ENOENT") return { kind: "offline", reason: "sentinel_missing" }; if (code === "EACCES" || code === "EPERM") return { kind: "offline", reason: "permission_denied", code }; return { kind: "offline", reason: "error", code: code ?? "UNKNOWN" }; } } /** 在根目录写入哨兵文件(首次登记库根时调用)。 */ export async function writeRootSentinel(root: string, sentinel: string = DEFAULT_ROOT_SENTINEL): Promise { await writeFile(join(root, sentinel), `uhd-media-tool root sentinel\ncreated=${new Date().toISOString()}\n`, { flag: "wx", }).catch((e) => { if (errnoOf(e) !== "EEXIST") throw e; }); } function facts(st: Awaited>): FileFacts { return { size: Number(st.size), mtimeMs: Number(st.mtimeMs), ino: Number(st.ino), dev: Number(st.dev), nlink: Number(st.nlink), isFile: st.isFile(), isDirectory: st.isDirectory(), isSymbolicLink: st.isSymbolicLink(), }; } /** * 区分 ENOENT / EACCES / 挂载根离线的 stat。给出 root 时先检查哨兵, * 根离线返回 root_offline 而不是 missing(REQ §8.2)。不抛异常。 */ export async function statSafe(path: string, opts: { root?: string; sentinel?: string } = {}): Promise { if (opts.root) { const h = await checkRoot(opts.root, opts.sentinel); if (h.kind === "offline") return { kind: "root_offline", root: opts.root, reason: h.reason }; } try { return { kind: "ok", facts: facts(await lstat(path)) }; } catch (e) { const code = errnoOf(e); if (code === "ENOENT" || code === "ENOTDIR") return { kind: "missing" }; if (code === "EACCES" || code === "EPERM") return { kind: "permission_denied" }; return { kind: "error", code: code ?? "UNKNOWN", message: (e as Error)?.message ?? String(e) }; } }