31 lines
1.2 KiB
JavaScript
31 lines
1.2 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
|
|
async function writeJsonAtomic(file, value, io = fs) {
|
|
const folder = path.dirname(file), name = path.basename(file);
|
|
const temp = path.join(folder, `.${name}.${crypto.randomUUID()}.tmp`);
|
|
await io.writeFile(temp, JSON.stringify(value, null, 2) + '\n');
|
|
try {
|
|
try { await io.rename(temp, file); return; }
|
|
catch (error) {
|
|
if (!['EPERM', 'EACCES', 'EBUSY', 'EEXIST'].includes(error.code)) throw error;
|
|
}
|
|
// Windows may refuse rename-over-existing. Keep the old JSON recoverable
|
|
// until the replacement is safely in place; never delete it first.
|
|
const backup = path.join(folder, `.old-${name}-${crypto.randomUUID()}`);
|
|
await io.rename(file, backup);
|
|
try { await io.rename(temp, file); }
|
|
catch (error) {
|
|
try { await io.rename(backup, file); }
|
|
catch (restoreError) { error.message += `; previous JSON remains at ${backup}: ${restoreError.message}`; }
|
|
throw error;
|
|
}
|
|
await io.rm(backup, { force: true });
|
|
} finally { await io.rm(temp, { force: true }); }
|
|
}
|
|
|
|
module.exports = { writeJsonAtomic };
|