File size: 1,642 Bytes
2b64d42 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | /**
* Atomic JSON file writer.
*
* The naive `writeFileSync(path, JSON.stringify(...))` truncates the
* target file and then writes — if the process gets killed (SIGTERM
* during docker stop, SIGKILL on OOM, panic-style crash) between the
* truncate and the final write, the file is left empty or partial.
* Next start, JSON.parse fails, the load() handler logs a warning and
* silently falls back to defaults — user loses every persisted
* setting, model-access list, proxy config, etc.
*
* Pattern: write the new contents to a sibling `${target}.tmp` first,
* then `rename(2)` it onto the target. rename is atomic on POSIX and
* replaces an existing target on Windows (per Node's documented
* fs.renameSync behavior). A crash between writeFileSync(tmp) and
* renameSync leaves the target intact; a crash after renameSync
* leaves the new contents in place. Either way, no truncated JSON.
*
* Tmp file gets unlinked on write failure so repeated failures don't
* leak garbage in DATA_DIR.
*
* Used by every dashboard config persister: model-access.json,
* proxy.json, stats.json, runtime-config.json. accounts.json already
* uses the same pattern hand-rolled in src/auth.js (kept inline there
* because it has its own coalescing/_saveInFlight machinery).
*/
import { writeFileSync, renameSync, unlinkSync } from 'node:fs';
export function writeJsonAtomic(targetPath, value, { spaces = 2 } = {}) {
const tmp = `${targetPath}.tmp`;
try {
writeFileSync(tmp, JSON.stringify(value, null, spaces));
renameSync(tmp, targetPath);
} catch (err) {
try { unlinkSync(tmp); } catch {}
throw err;
}
}
|