commit c82466ca961ef33fa5c2f4ab665311ac589c4258
parent 261f28fa9b033c642f631797dfda4e3668463603
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 11 Sep 2026 11:34:45 -0400
common: a dir that does not exist yet is measured on its parent's volume
The per-volume gate has six callers passing `channels/<slug>/data` — the volume
the bytes are about to land on — and that directory does not exist until the
channel's first download creates it. Neither createChannel nor syncPaged makes
it, deliberately. A bare statfs there is ENOENT, the fail-open answered
Infinity, and the gate then waved through exactly the first download onto the
disk it had been installed to protect. Per-volume measurement that fails open on
the one path that is always new is decoration.
getFreeBytes walks up with dirname until statfs succeeds. The ancestor is the
right answer and not an approximation: a directory about to be created lands on
the filesystem its parent is on. ONLY ENOENT walks — only ENOENT means "not
there YET". ENOTDIR, EACCES and an unsupported statfs still fail open at
Infinity, which is the original contract and is now pinned by its own case. The
walk terminates at the filesystem root, where dirname is a fixed point.
The two-volume latch cases used a nonexistent path as the stand-in for "a volume
with room", which is precisely the behaviour that just changed, so both dirs are
now real and the roomy one is held to a floor of about a kilobyte instead.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
2 files changed, 99 insertions(+), 15 deletions(-)
diff --git a/common/lib/diskSpace.test.ts b/common/lib/diskSpace.test.ts
@@ -1,8 +1,12 @@
import { test } from "node:test";
import assert from "node:assert/strict";
+import os from "node:os";
+import path from "node:path";
+import { mkdtemp, rm, statfs, writeFile } from "node:fs/promises";
import {
diskGate,
evaluateDiskGate,
+ getFreeBytes,
isDiskGateLatched,
resetDiskGate,
} from "./diskSpace";
@@ -124,18 +128,24 @@ test("a statfs failure fails open", () => {
// did it.
//
// These take a real measurement, which is what makes them worth having next to
-// the pure cases above: getFreeBytes fails OPEN (Infinity) on a path that does
-// not exist, so a missing dir stands in for "a volume with room" and a real one
-// under an absurd floor stands in for "a volume that is full". No filesystem is
-// written to.
+// the pure cases above. BOTH DIRS ARE REAL AND EXIST: a path that does not exist
+// is no longer a stand-in for "a volume with room", because getFreeBytes now
+// measures a missing dir's nearest existing ANCESTOR (see below — that fix is
+// what makes the per-channel gate work at all). So "full" is a real path under
+// an absurd floor and "roomy" is a real path under a floor of about a kilobyte.
+// No filesystem is written to.
-const FULL = "/"; // a real path, measured, and always under the floor below
-const ROOMY = "/definitely-not-a-mountpoint-ttb-test"; // statfs fails -> Infinity
+const FULL = "/"; // a real path, measured, and always under the absurd floor
+const ROOMY = os.tmpdir(); // a real path, measured, and always over a 1 KB floor
const settingsWithFloor = (minFreeDiskGB: number) =>
({ minFreeDiskGB, resumeMarginGB: 1 }) as SiteSettings;
const somePaths = { transcriptsDir: FULL } as Paths;
+// A floor of ~1 KB: enabled (a floor of 0 disables the gate entirely) and
+// cleared by any filesystem with room on it.
+const TINY_FLOOR = 1e-6;
+
test("a latch on one volume does not hold another", async () => {
resetDiskGate();
// An absurd floor no real filesystem clears.
@@ -146,7 +156,7 @@ test("a latch on one volume does not hold another", async () => {
// The other volume is unaffected — and, crucially, asking about it does not
// clear the first one's latch. With one shared boolean it would have.
- const roomy = await diskGate(somePaths, settingsWithFloor(1e9), {
+ const roomy = await diskGate(somePaths, settingsWithFloor(TINY_FLOOR), {
dir: ROOMY,
});
assert.equal(roomy.ok, true);
@@ -155,6 +165,56 @@ test("a latch on one volume does not hold another", async () => {
resetDiskGate();
});
+// --- A DIR THAT DOES NOT EXIST YET IS MEASURED ON ITS PARENT'S VOLUME -------
+//
+// The six per-channel gate callers pass `channels/<slug>/data`, which does not
+// exist until the channel's first download creates it. Fail-open on ENOENT
+// answered Infinity there, so the gate waved through exactly the download it
+// was installed to stop. It measures the nearest existing ancestor instead.
+
+test("getFreeBytes measures a not-yet-existing dir on its nearest existing ancestor", async () => {
+ const root = os.tmpdir();
+ const missing = path.join(root, "ttb-no-such-dir", "slug", "data");
+ const measured = await getFreeBytes(missing);
+ const onRoot = await statfs(root);
+ assert.equal(Number.isFinite(measured), true, "must not fail open");
+ // Same volume, so the same figure — modulo whatever the machine wrote
+ // between the two calls, which is why this is a band and not an equality.
+ const expected = onRoot.bsize * onRoot.bavail;
+ assert.ok(
+ Math.abs(measured - expected) < expected * 0.05 + 1024 ** 3,
+ `expected ~${expected}, got ${measured}`,
+ );
+});
+
+test("a non-ENOENT failure still fails open", async () => {
+ // A path UNDER A FILE is ENOTDIR, not ENOENT — nonsense rather than
+ // not-there-yet — so the walk does not run and the original contract holds:
+ // a measurement glitch never blocks a download. This is the half of the old
+ // fail-open that survives, and it is deliberately still Infinity.
+ const dir = await mkdtemp(path.join(os.tmpdir(), "ttb-disk-"));
+ try {
+ const file = path.join(dir, "a-file");
+ await writeFile(file, "x");
+ assert.equal(await getFreeBytes(path.join(file, "nope")), Infinity);
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+});
+
+test("the gate latches for a data dir that does not exist yet", async () => {
+ resetDiskGate();
+ const notYet = path.join(os.tmpdir(), "ttb-unborn-channel", "data");
+ const status = await diskGate(somePaths, settingsWithFloor(1e9), {
+ dir: notYet,
+ });
+ // Measured on tmpdir's volume, which no absurd floor clears. Before the
+ // ancestor walk this was Infinity and `ok: true`.
+ assert.equal(status.ok, false);
+ assert.equal(isDiskGateLatched(notYet), true);
+ resetDiskGate();
+});
+
test("the hysteresis is still per volume: a latched dir is held to the higher bar", async () => {
resetDiskGate();
await diskGate(somePaths, settingsWithFloor(1e9), { dir: FULL });
diff --git a/common/lib/diskSpace.ts b/common/lib/diskSpace.ts
@@ -1,3 +1,4 @@
+import path from "node:path";
import { statfs } from "node:fs/promises";
import type { Paths } from "./paths";
import type { SiteSettings } from "./settings";
@@ -6,16 +7,39 @@ import { formatBytes } from "./format";
const BYTES_PER_GB = 1024 ** 3;
// Free space (bytes) available to an unprivileged process on the filesystem
-// holding `dir`. Fail-open: any error (statfs unsupported, path missing,
-// permissions) reports Infinity so a measurement glitch never blocks a
-// download. statfs reports blocks in `bsize`-sized units; `bavail` excludes
+// holding `dir`. statfs reports blocks in `bsize`-sized units; `bavail` excludes
// blocks reserved for root, which is what a normal write can actually use.
+//
+// A DIRECTORY THAT DOES NOT EXIST YET IS MEASURED ON ITS NEAREST EXISTING
+// ANCESTOR, and that is the difference between this gate working and this gate
+// being decorative. Since the gate became per-volume, six callers pass
+// `channels/<slug>/data` — the volume the bytes are ABOUT to land on — and that
+// directory does not exist until the channel's first download creates it
+// (neither createChannel nor syncPaged makes it, deliberately). A bare statfs
+// there is ENOENT, the old fail-open answered Infinity, and the gate then waved
+// through exactly the first download onto a disk it had been asked to protect.
+// The ancestor is the right answer and not an approximation: a directory about
+// to be created lands on the filesystem its parent is on.
+//
+// Everything else still FAILS OPEN (Infinity): statfs unsupported, permissions,
+// a nonsense path (ENOTDIR). A measurement glitch must never block a download —
+// that is the original contract and it is unchanged. Only ENOENT walks, because
+// only ENOENT means "not there YET"; the walk terminates at the filesystem root,
+// where dirname is a fixed point.
export async function getFreeBytes(dir: string): Promise<number> {
- try {
- const stats = await statfs(dir);
- return stats.bsize * stats.bavail;
- } catch {
- return Number.POSITIVE_INFINITY;
+ let current = path.resolve(dir);
+ for (;;) {
+ try {
+ const stats = await statfs(current);
+ return stats.bsize * stats.bavail;
+ } catch (err) {
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
+ return Number.POSITIVE_INFINITY;
+ }
+ const parent = path.dirname(current);
+ if (parent === current) return Number.POSITIVE_INFINITY;
+ current = parent;
+ }
}
}