Files
UHD_Media_Tool/packages/adapters/src/fs/stat.ts
T
rootandClaude Fable 5.1 304feea141 chore(deno): migrate toolchain from Node 24 + pnpm to Deno 2.9.7 + TypeScript 7.0.2 (W1.5)
- root deno.json workspace (nodeModulesDir auto), npm versions registered once in imports, deno.lock;
  remove pnpm-lock/pnpm-workspace/root package.json; members keep minimal package.json (name/type/exports)
- tasks: dev, typecheck (tsc 7.0.2 per member + deno check), lint/fmt (biome, useImportExtensions),
  test (vitest via deno), check:tokens, build (web + deno compile), ci, db:migrate/reset, gen:types
- tsconfig base moved to config/ (no root tsconfig.json); deno types -> types/deno.d.ts; web drops baseUrl
- server: Deno.serve entry with graceful shutdown, --role migrate|preflight|probe, WEB_ROOT static SPA hosting;
  drop @hono/node-server, tsx, esbuild build.mjs
- logger: redact child() bindings and serialize err by message/stack; sync stdout destination; tests
- relative imports get explicit extensions (sites/mp-sync paths left for follow-up)
- crash-recovery tests spawn `deno run -A`; tasks tests use unique per-file schemas
- single-container Dockerfile + entrypoint (migrate -> preflight -> api+worker watchdog), docker-compose.yml
- docs: README, runbook, ADR-001 superseded, plan §1 validation row

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-27 18:53:06 +08:00

94 lines
3.4 KiB
TypeScript

// 本项目新增(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<RootHealth> {
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<void> {
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<ReturnType<typeof lstat>>): 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<StatResult> {
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) };
}
}