Files
ECC/scripts/lib/atomic-write.js
haelyra dbe8bfbba9 fix(install): pin Claude settings parent during atomic replacement
Reject directory replacement after temporary file creation or staging, preserve unrelated files during cleanup, and retry settings edits observed before the final rename. Add three regression tests for the review findings.
2026-09-07 16:31:33 -04:00

53 lines
1.6 KiB
JavaScript

'use strict';
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
function writeFileAtomic(filePath, content, options = {}) {
const resolvedPath = path.resolve(filePath);
const parentDir = path.dirname(resolvedPath);
const tempPath = path.join(
parentDir,
`.${path.basename(resolvedPath)}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`
);
const mode = options.mode || 0o600;
if (options.validateParent) options.validateParent();
fs.mkdirSync(parentDir, { recursive: true });
let descriptor;
try {
if (options.validateParent) options.validateParent();
descriptor = fs.openSync(tempPath, 'wx', mode);
if (options.validateParent) options.validateParent();
fs.writeFileSync(descriptor, content, { encoding: options.encoding || 'utf8' });
fs.fsyncSync(descriptor);
fs.closeSync(descriptor);
descriptor = undefined;
if (options.validateParent) options.validateParent();
if (options.beforeRename) options.beforeRename();
fs.renameSync(tempPath, resolvedPath);
} catch (error) {
if (descriptor !== undefined) {
fs.closeSync(descriptor);
}
// If the parent was replaced, this pathname may now name somebody else's
// file. Leave the private staging file in its original directory.
let parentUnchanged = true;
try {
if (options.validateParent) options.validateParent();
} catch (_error) {
parentUnchanged = false;
}
if (parentUnchanged) fs.rmSync(tempPath, { force: true });
throw error;
}
return resolvedPath;
}
module.exports = {
writeFileAtomic,
};