From e8acda5d9dc62d3aaaa061320932f0666b03a678 Mon Sep 17 00:00:00 2001 From: Philippe Faist Date: Fri, 14 Feb 2025 01:12:55 +0100 Subject: [PATCH] attempt atomic file write for cache --- src/citationmanager/_manager.js | 8 ++++- src/util/atomicfilewrite.js | 58 +++++++++++++++++++++++++++++++++ src/zooflm/citationcompiler.js | 8 ++++- 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 src/util/atomicfilewrite.js diff --git a/src/citationmanager/_manager.js b/src/citationmanager/_manager.js index a77febd..87ca8df 100644 --- a/src/citationmanager/_manager.js +++ b/src/citationmanager/_manager.js @@ -6,6 +6,7 @@ const debug = debug_module('zoodb.citationmanager'); import sha256 from 'hash.js/lib/hash/sha/256.js'; import { promisifyMethods } from '../util/prify.js'; +import { writeFileAtomic } from '../util/atomicfilewrite.js'; import { Cache, one_day } from './_cache.js'; @@ -111,7 +112,12 @@ export class CitationDatabaseManager const fsp = this.cache_fsp; // debug(`Saving database to cache file ‘${this.cache_file}’`); // to this.cache_file - await fsp.writeFile(this.cache_file, this.cache.exportJson()); + await writeFileAtomic({ + fsp, + fileName: this.cache_file, + data: this.cache.exportJson(), + processPid: (process != null) ? process.pid : 'XX', + }); } /** diff --git a/src/util/atomicfilewrite.js b/src/util/atomicfilewrite.js new file mode 100644 index 0000000..5e6e13c --- /dev/null +++ b/src/util/atomicfilewrite.js @@ -0,0 +1,58 @@ +import path from 'path'; + +/** + * Write `data` to the file with the given `fileName`, overwriting it silently if it + * exists. The write first happens to a temporary hidden file, and finally after the + * write succeeds, the temporary file is renamed to the target file. This procedure + * prevents the output file from being corrupted if the process is interrupted while + * data is being written to the file. + * + * Arguments: + * + * - `fsp` a promisified filesystem object compatible with Node.js' promisified `fs` + * object. + * + * - `fileName` the name of the final file to write to. + * + * - `data` the data to write to the file. Can be string, typed array, or whatever + * fsp's `writeFile()` can handle. + * + * - `processPid` - specify `process.pid` here (or whatever you choose to use as relevant + * PID for platforms/browser that might not have process PIDs). + */ +export async function writeFileAtomic({ fsp, fileName, data, useTempDir, processPid }) +{ + let atomicWriteTempFileName = null; + let counter = 0; + let dataWritten = false; + + const fDir = useTempDir ?? path.dirname(fileName) ?? ''; + const fBaseTemplate = `.atomicwrite.${path.basename(fileName)}.${processPid}` + + while (!dataWritten && counter <= 99) { + atomicWriteTempFileName = path.join( + fDir, + `${fBaseTemplate}.${counter}.tmp` + ); + // do not accidentally overwrite a file; use 'wx' as open flags so that open fails + // if the file exists. + try { + await fsp.writeFile(atomicWriteTempFileName, data, { + flag: 'wx', + }); + dataWritten = true; + } catch (err) { + // if file already exists, try another one + if (err && err.code === 'EEXIST') { + ++ counter; + } else { + // it's another error, send it up the chain + throw err; + } + } + } + if (!dataWritten) { + throw new Error(`writeFileAtomic: Failed to find temporary file to write to!`); + } + await fsp.rename(atomicWriteTempFileName, fileName); +} diff --git a/src/zooflm/citationcompiler.js b/src/zooflm/citationcompiler.js index 9a51690..3e2edf9 100644 --- a/src/zooflm/citationcompiler.js +++ b/src/zooflm/citationcompiler.js @@ -8,6 +8,7 @@ const { import { split_prefix_label } from '../util/index.js'; import { promisifyMethods } from '../util/prify.js'; +import { writeFileAtomic } from '../util/atomicfilewrite.js'; import { Cache, one_day } from '../citationmanager/_cache.js'; @@ -399,7 +400,12 @@ export class CitationCompiler { const fsp = this.cache_fsp; debug(`Saving compiled citations to cache file ‘${this.cache_file}’`); - await fsp.writeFile(this.cache_file, this.cache.exportJson()); + await writeFileAtomic({ + fsp, + fileName: this.cache_file, + data: this.cache.exportJson(), + processPid: (process != null) ? process.pid : 'XX', + }); }