import { randomUUID } from "node:crypto"; import { link, open, rename, rm, stat } from "node:fs/promises"; import { basename, dirname, join } from "node:path"; export interface AtomicWriteOptions { /** 目标存在时是否原子替换;为 false 时以 EEXIST 拒绝覆盖。 */ overwrite?: boolean; /** 新文件权限;目标已存在时优先继承原权限。 */ mode?: number; } /** 判断未知错误是否具有指定 Node.js 文件系统错误码。 */ function hasErrorCode(error: unknown, code: string): boolean { return error instanceof Error && (error as NodeJS.ErrnoException).code === code; } /** 读取现有目标权限;目标不存在时使用调用方提供的默认权限。 */ async function resolveMode(path: string, fallback: number): Promise { try { return (await stat(path)).mode & 0o777; } catch (error) { if (hasErrorCode(error, "ENOENT")) { return fallback; } throw error; } } /** * 在目标同目录写入并同步临时文件,再通过 rename/link 原子提交。 * overwrite=false 时不会覆盖提交期间由其他进程创建的目标。 */ export async function writeTextFileAtomic( path: string, content: string, options: AtomicWriteOptions = {}, ): Promise { const overwrite = options.overwrite ?? true; const mode = await resolveMode(path, options.mode ?? 0o600); const tempPath = join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`); let handle; try { handle = await open(tempPath, "wx", mode); await handle.writeFile(content, "utf8"); await handle.sync(); await handle.close(); handle = undefined; if (overwrite) { await rename(tempPath, path); } else { await link(tempPath, path); await rm(tempPath, { force: true }); } } catch (error) { await handle?.close().catch(() => undefined); await rm(tempPath, { force: true }).catch(() => undefined); throw error; } }