/** * Zero-dependency atomic file replacement. `writeFileAtomic` writes a * random-suffix sibling with exclusive create and the caller's permission * bits, then renames it over the target, so readers observe either the old or * the new complete content and a replaced file ends up with exactly the * stated mode. * @module @deepseek-ai/dsh-atomic-write */ import { randomBytes } from 'node:crypto' import { mkdir, rename, rm, writeFile } from 'node:fs/promises' import { dirname } from 'node:path' /** * Filesystem options for {@link writeFileAtomic}; `mode` is required so the * permission decision stays visible at every call site. */ export interface WriteFileAtomicOptions { /** * Permission bits stamped on the fresh temp inode and carried through the * rename (subject to the process umask, like every fresh inode). */ mode: number } /** * Replace `filename` with `content` in one atomic step, creating parent * directories. The content is first written to a random-suffix sibling opened * with exclusive create (`wx`): the open refuses to follow a symlink planted * at the temp path, and the fresh inode carries `options.mode` through the * rename, so replacing a wider-permission file narrows it without a chmod * race. The rename also replaces a symlinked target itself instead of writing * through to its referent, and the same-directory sibling keeps the rename on * one filesystem. On any failure the temp file is removed and the failure * rethrown. Crash durability (fsync) is out of scope. * @param filename - final path receiving the content. * @param content - complete next file content. * @param options - permission bits for the replacement inode. */ export async function writeFileAtomic(filename: string, content: string, options: WriteFileAtomicOptions): Promise { await mkdir(dirname(filename), { recursive: true }) const temp = `${filename}.${randomBytes(6).toString('hex')}.tmp` try { await writeFile(temp, content, { mode: options.mode, flag: 'wx' }) await rename(temp, filename) } catch (error) { await rm(temp, { force: true }) throw error } }