Files
immich-folio/lib/atomicWrite.ts
Ralksta a3a6319c29 release: v0.16.0
See CHANGELOG.md for the full v0.16.0 entry.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ralf <rohlenbo79@gmail.com>
Co-authored-by: Paul Thies <110185070+ImScheinox@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
2026-09-22 14:12:13 +02:00

57 lines
2.2 KiB
TypeScript

import fs from 'fs/promises';
/**
* Monotonic within the process, so two writes to the same path started in the
* same millisecond cannot land on the same temp file. Several of the copies
* this replaces used `Date.now()`, which can.
*/
let tmpCounter = 0;
/**
* Write a file atomically: unique temp file, then rename.
*
* The rename is what makes this atomic; the temp filename is what did not.
* With a constant `${filePath}.tmp`, two saves of the same file in flight at
* once shared it — the second writeFile could interleave with the first's
* rename, leaving one save's bytes published under the other's, or a truncated
* mix. A double-clicked Save in the page builder is enough.
*
* Readers matter as much as writers: `GET /api/favicon` reads its file on every
* request, so a plain writeFile would let a concurrent read see a half-written
* icon.
*
* No encoding is passed. `fs.writeFile` defaults to utf8 for a string and
* writes a Buffer verbatim, which is what every caller wants — the copies this
* replaces variously said `'utf8'`, `'utf-8'` or nothing at all.
*
* Creating the directory is the caller's business; this only writes the file.
*
* `mode`, when given, is applied to the temp file before the rename — not
* `chmod`ed on the final path afterwards. A file holding a secret (an API
* key, an auth secret) that is written world-readable and only tightened a
* moment later is briefly the wrong permission on disk, and briefly is all a
* reader running at the wrong instant needs. `rename` keeps whatever mode the
* source had, so creating the temp file with the right one from the start
* means the secret is never world-readable at all.
*/
export async function atomicWrite(
filePath: string,
content: string | Buffer,
options?: { mode?: number },
): Promise<void> {
const tmpPath = `${filePath}.${process.pid}.${++tmpCounter}.tmp`;
try {
await fs.writeFile(
tmpPath,
content,
options?.mode !== undefined ? { mode: options.mode } : undefined,
);
await fs.rename(tmpPath, filePath);
} catch (err) {
// Do not leave litter behind a failed save. Cleanup failure is not worth
// masking the real error with.
await fs.unlink(tmpPath).catch(() => {});
throw err;
}
}