Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

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:
Mcommon/controller/failedTranscriptions.ts | 14+++++++-------
Mcommon/controller/storageWatch.ts | 4++++
Mcommon/lib/savedVideo-server.ts | 7++-----
Mcommon/social/xSessionBroker.ts | 14++++++++------
Mcommon/ytdlp/runYtdlp.ts | 7+++----
Meditor/app/channels/[slug]/videos/[id]/videoActions.ts | 9+++------
Meditor/app/sites/lib/cutReleaseAction.ts | 5++---
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,