W
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;
  }
}