commit 541c9fb6e0c322ab5253ffc39f53dcba7fc17f11
parent 80034b71d5c7757db39c71d9a80da52e89c7c4c2
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 24 Sep 2026 18:59:01 -0400
common: text and binary tmp-rename writers on writeFileAtomic
failed-transcriptions (prune and clear), the channel playlist, the X
cookie jar (mode 0o600 on the temp at creation, as before), a site's
CHANGELOG cut, and the video page's VTT promote now go through
writeFileAtomic; the saved-video store's cross-device move copies
through copyFileAtomic. Bytes unchanged. storageWatch's timer gets one
comment saying why it is a per-copy singleton that is not a temp name.
After this the only per-pid temp names left in common/ and editor/ are
the two export page writers and the ffmpeg / whisper output bases.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
7 files changed, 29 insertions(+), 31 deletions(-)
diff --git a/common/controller/failedTranscriptions.ts b/common/controller/failedTranscriptions.ts
@@ -1,5 +1,6 @@
import path from "node:path";
-import { readFile, rename, writeFile } from "node:fs/promises";
+import { readFile } from "node:fs/promises";
+import { writeFileAtomic } from "../lib/jsonFile-server";
import type { Paths } from "../lib/paths";
export function failedTranscriptionsFile(paths: Paths, slug: string): string {
@@ -30,9 +31,10 @@ export async function pruneFailedTranscriptions(
}
const original = raw.split("\n").filter(Boolean);
const filtered = original.filter((id) => !idsToRemove.has(id));
- const tmp = `${failureListFile}.tmp-${process.pid}`;
- await writeFile(tmp, filtered.length ? filtered.join("\n") + "\n" : "");
- await rename(tmp, failureListFile);
+ await writeFileAtomic(
+ failureListFile,
+ filtered.length ? filtered.join("\n") + "\n" : "",
+ );
return {
remaining: filtered.length,
pruned: original.length - filtered.length,
@@ -51,8 +53,6 @@ export async function clearFailedTranscriptions(
} catch {
return { cleared: 0 };
}
- const tmp = `${file}.tmp-${process.pid}`;
- await writeFile(tmp, "");
- await rename(tmp, file);
+ await writeFileAtomic(file, "");
return { cleared };
}
diff --git a/common/controller/storageWatch.ts b/common/controller/storageWatch.ts
@@ -318,6 +318,10 @@ export async function runStorageWatchPass(
// of the process.
export const STORAGE_WATCH_INTERVAL_MS = 5 * 60_000;
+// A per-module-copy singleton, deliberately left so: it is not a temp-file
+// name (slice W folded every tmp + rename onto lib/jsonFile-server.ts, whose
+// state is on globalThis), and its one caller is editor/instrumentation.ts,
+// so only one copy ever arms it.
let timer: ReturnType<typeof setInterval> | null = null;
// ARMED ONCE PER PROCESS. `unref()` so it never holds the event loop open — a
diff --git a/common/lib/savedVideo-server.ts b/common/lib/savedVideo-server.ts
@@ -1,7 +1,6 @@
-import { writeJsonAtomic } from "./jsonFile-server";
+import { copyFileAtomic, writeJsonAtomic } from "./jsonFile-server";
import path from "node:path";
import {
- copyFile,
mkdir,
readFile,
rename,
@@ -76,9 +75,7 @@ async function moveFileCrossDevice(src: string, dest: string): Promise<void> {
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== "EXDEV") throw err;
}
- const tmp = `${dest}.tmp-${process.pid}`;
- await copyFile(src, tmp);
- await rename(tmp, dest);
+ await copyFileAtomic(src, dest);
await rm(src, { force: true });
}
diff --git a/common/social/xSessionBroker.ts b/common/social/xSessionBroker.ts
@@ -20,7 +20,8 @@
// build-time one.
import path from "node:path";
-import { mkdir, readFile, rename, writeFile, rm, stat } from "node:fs/promises";
+import { mkdir, readFile, rm, stat } from "node:fs/promises";
+import { writeFileAtomic } from "../lib/jsonFile-server";
import type { Paths } from "../lib/paths";
import { importPlaywright } from "./playwrightRuntime";
@@ -212,11 +213,12 @@ async function writeCookieJar(
log: (line: string) => void,
): Promise<void> {
const file = xCookieFile(paths);
- await mkdir(path.dirname(file), { recursive: true });
- const body = toNetscapeCookieFile(cookies);
- const tmp = `${file}.tmp-${process.pid}`;
- await writeFile(tmp, body, { mode: 0o600 });
- await rename(tmp, file);
+ // mode is applied as the temp is CREATED, before the rename: the jar is
+ // never world-readable, not even for an instant.
+ await writeFileAtomic(file, toNetscapeCookieFile(cookies), {
+ mkdir: true,
+ mode: 0o600,
+ });
const authed = cookies.some((c) => c.name === AUTH_COOKIE);
log(
`Exported ${cookies.length} X cookie(s) to ${file}` +
diff --git a/common/ytdlp/runYtdlp.ts b/common/ytdlp/runYtdlp.ts
@@ -1,5 +1,6 @@
import path from "node:path";
-import { mkdir, readdir, readFile, rename, writeFile } from "node:fs/promises";
+import { mkdir, readdir, readFile } from "node:fs/promises";
+import { writeFileAtomic } from "../lib/jsonFile-server";
import { execa } from "execa";
import pLimit from "p-limit";
import { readArchive } from "../lib/archive";
@@ -457,9 +458,7 @@ async function writePlaylistFile(
onLog: (s: string) => void,
): Promise<void> {
const playlistPath = path.join(root, "playlist");
- const tmpPath = `${playlistPath}.tmp-${process.pid}`;
- await writeFile(tmpPath, urls.join("\n") + (urls.length ? "\n" : ""));
- await rename(tmpPath, playlistPath);
+ await writeFileAtomic(playlistPath, urls.join("\n") + (urls.length ? "\n" : ""));
onLog(`Wrote ${urls.length} URLs to ${playlistPath}\n`);
}
diff --git a/editor/app/channels/[slug]/videos/[id]/videoActions.ts b/editor/app/channels/[slug]/videos/[id]/videoActions.ts
@@ -1,7 +1,7 @@
"use server";
import path from "node:path";
-import { readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
+import { readdir, readFile, rm, stat } from "node:fs/promises";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import type {
@@ -71,7 +71,7 @@ import {
import { requestChannelSnapshot } from "yt-dlp-transcript-common/jobs/snapshotScheduler";
import { makeTaskTracker } from "yt-dlp-transcript-common/jobs/taskHooks";
import { fixIncompleteTranscriptOne } from "../../lib/fixIncompleteTranscript";
-import { writeJsonAtomic } from "yt-dlp-transcript-common/lib/jsonFile-server";
+import { writeFileAtomic, writeJsonAtomic } from "yt-dlp-transcript-common/lib/jsonFile-server";
function videoQueueKey(config: ChannelConfig, override: string | undefined): string {
return resolveQueueKey(downloadQueueKey(config), override);
@@ -496,10 +496,7 @@ export async function setPrimaryTranscriptAction(
return { ok: false, error: `File not found: ${filename}` };
}
if (filename !== VTT_FILENAME) {
- const dest = path.join(videoDir, VTT_FILENAME);
- const tmp = path.join(videoDir, `${VTT_FILENAME}.tmp-${process.pid}`);
- await writeFile(tmp, raw);
- await rename(tmp, dest);
+ await writeFileAtomic(path.join(videoDir, VTT_FILENAME), raw);
}
// The video page reads the dir directly, so it reflects the new primary right
// away. The channel list + diagnostics bucket read the cached snapshot and
diff --git a/editor/app/sites/lib/cutReleaseAction.ts b/editor/app/sites/lib/cutReleaseAction.ts
@@ -9,6 +9,7 @@ import {
CutReleaseError,
} from "yt-dlp-transcript-common/lib/changelog";
import { commitPath, listDirtyPaths } from "yt-dlp-transcript-common/lib/git";
+import { writeFileAtomic } from "yt-dlp-transcript-common/lib/jsonFile-server";
export type CutReleaseState =
| { ok: true; version: string; committed: boolean }
@@ -93,9 +94,7 @@ export async function cutReleaseAction(
}
throw err;
}
- const tmp = `${filePath}.tmp-${process.pid}`;
- await fs.promises.writeFile(tmp, next);
- await fs.promises.rename(tmp, filePath);
+ await writeFileAtomic(filePath, next);
if (shouldCommit) {
const result = await commitPath(
paths.monorepoRoot,