writeAtomic, everywhere a file lands
the .part-then-rename rule was applied at two upload routes yesterday and by this morning the platform had a dozen other writes putting files in the same folders — an OCR result beside its image, a TTS clip, a download landing, a save from the editor — every one straight to the final name. a guarantee that holds for two of fourteen paths is not a guarantee, and the next person adding a fifteenth had nothing to copy. so it is a function now, and every durable write goes through it: the file browser, sendoff, task definitions, skill and tool files, bug reports, scraped attachments, the chat provider config. the failure it prevents is silent rather than loud. a truncated m4a decodes fine, it is simply short — a commander transcribing recordings as they land would produce a clean transcript of the first few seconds of a two-minute memo and report success. two things learned writing it. Bun.write creates the parent chain itself, so the mkdir option i added was dead weight and several callers were doing it twice. and Parameters<typeof Bun.write>[1] resolves to the first overload only — BunFile — which rejects the string and Buffer every caller actually passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -4,6 +4,7 @@ import { readdir, stat, mkdir, rm, rename, readFile, cp } from 'node:fs/promises
|
||||
import { existsSync } from 'node:fs';
|
||||
import { getOwnerHomeDir, DATA_PATH } from '@@/data-path';
|
||||
import { resolveHomeDir } from '@@/user-home';
|
||||
import { writeAtomic } from '@@/write-atomic';
|
||||
import * as errors from '@@/custom-errors';
|
||||
import { readTtsConfig } from '@@/api/server-settings/tts';
|
||||
import { readOcrConfig } from '@@/api/server-settings/ocr';
|
||||
@@ -253,7 +254,7 @@ router.post('/write', async (ctx) => {
|
||||
|
||||
const absPath = resolveUserPath(rootDir, path);
|
||||
await mkdir(dirname(absPath), { recursive: true });
|
||||
await Bun.write(absPath, content);
|
||||
await writeAtomic(absPath, content);
|
||||
return ctx.json({ ok: true });
|
||||
});
|
||||
|
||||
@@ -306,16 +307,7 @@ router.post('/upload', async (ctx) => {
|
||||
if (!isInside(rootDir, filePath)) continue;
|
||||
await mkdir(dirname(filePath), { recursive: true });
|
||||
|
||||
const partPath = `${filePath}.part`;
|
||||
try {
|
||||
await Bun.write(partPath, file);
|
||||
await rename(partPath, filePath);
|
||||
} catch (err) {
|
||||
// Leave nothing half-named. Only an interrupted PROCESS leaves a fragment behind, which is
|
||||
// the case worth being able to see.
|
||||
await rm(partPath, { force: true }).catch(() => {});
|
||||
throw err;
|
||||
}
|
||||
await writeAtomic(filePath, file);
|
||||
}
|
||||
|
||||
return ctx.json({ ok: true });
|
||||
@@ -815,7 +807,7 @@ router.post('/tts', async (ctx) => {
|
||||
const siblingRel = dir ? `${dir}/${name}.mp3` : `${name}.mp3`;
|
||||
const siblingAbs = resolveUserPath(rootDir, siblingRel);
|
||||
const cached = await readFile(cacheAbs);
|
||||
await Bun.write(siblingAbs, cached);
|
||||
await writeAtomic(siblingAbs, cached);
|
||||
return ctx.json({ audioPath: `/${siblingRel}`, audioRoot: root ?? 'home', cached: true });
|
||||
}
|
||||
return ctx.json({ audioPath: cacheRel, audioRoot: 'user-data' });
|
||||
@@ -845,12 +837,12 @@ router.post('/tts', async (ctx) => {
|
||||
|
||||
await mkdir(dirname(cacheAbs), { recursive: true });
|
||||
const buffer = await res.arrayBuffer();
|
||||
await Bun.write(cacheAbs, buffer);
|
||||
await writeAtomic(cacheAbs, buffer);
|
||||
|
||||
if (saveNextTo) {
|
||||
const siblingRel = dir ? `${dir}/${name}.mp3` : `${name}.mp3`;
|
||||
const siblingAbs = resolveUserPath(rootDir, siblingRel);
|
||||
await Bun.write(siblingAbs, buffer);
|
||||
await writeAtomic(siblingAbs, buffer);
|
||||
return ctx.json({ audioPath: `/${siblingRel}`, audioRoot: root ?? 'home', cached: false });
|
||||
}
|
||||
|
||||
@@ -900,7 +892,7 @@ router.post('/tts-text', async (ctx) => {
|
||||
|
||||
await mkdir(dirname(cacheAbs), { recursive: true });
|
||||
const buffer = await res.arrayBuffer();
|
||||
await Bun.write(cacheAbs, buffer);
|
||||
await writeAtomic(cacheAbs, buffer);
|
||||
|
||||
return ctx.json({ audioPath: cacheRel, audioRoot: 'user-data' });
|
||||
});
|
||||
@@ -972,7 +964,7 @@ router.post('/ocr', async (ctx) => {
|
||||
if (saveNextTo) {
|
||||
const siblingRel = dir ? `${dir}/${name}.md` : `${name}.md`;
|
||||
const siblingAbs = resolveUserPath(rootDir, siblingRel);
|
||||
await Bun.write(siblingAbs, text);
|
||||
await writeAtomic(siblingAbs, text);
|
||||
return ctx.json({ ocrPath: `/${siblingRel}`, ocrRoot: root ?? 'home', cached: false });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user