commit 882f0c781afc628f463b17a7ad7850f90ab9cb9e
parent 9cf4ec234dc395a3c5014957da9c4d51f4b9692c
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 1 Oct 2026 20:46:51 -0400
common, umtool: a scope-less marker aimed at <root>/<slug>/data holds the text; the hook writes nothing under a marker
markerHoldsText: a tier migration, or the old whole-directory mover's marker
(no scope, a retired-shape target), holds the text in the guard, the digest
lane's pick-to-run backstop and the cues twin; a media move does not. The
tier hook leaves a file real while a .relocating.json stands on its channel.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
6 files changed, 70 insertions(+), 6 deletions(-)
diff --git a/common/controller/autoRunner.ts b/common/controller/autoRunner.ts
@@ -86,6 +86,7 @@ import { downloadQueueKey } from "../lib/queueKeys";
import { isGateHeld } from "../lib/pauseGates";
import {
inspectChannelMedia,
+ markerHoldsText,
readRelocationMarker,
type ChannelMediaLocation,
type ChannelMediaStatus,
@@ -1921,7 +1922,7 @@ async function runLoop(
// is, and a digest writes only text. A tier migration rebuilds `data/`
// itself, and holds it too.
const marker = await readRelocationMarker(paths, channelSlug);
- if (marker && (kind !== "digest" || marker.scope === "tier-migration")) {
+ if (marker && (kind !== "digest" || markerHoldsText(marker))) {
onLog(
`Auto-${kind}: skipping ${channelSlug}/${pick.videoId}, ` +
`${mediaHoldText("in-transition")} — a relocation ` +
diff --git a/common/lib/channelMedia.test.ts b/common/lib/channelMedia.test.ts
@@ -431,3 +431,20 @@ test("clearRelocationMarker removes the marker and touches nothing else", async
await clearRelocationMarker(paths, "alpha");
});
});
+
+test("a scope-less marker aimed at the retired <root>/<slug>/data holds the text (the old mover)", async () => {
+ await withTmp(async (paths, root) => {
+ const channelDir = await seedChannel(paths, "alpha");
+ await mkdir(path.join(channelDir, "data"), { recursive: true });
+ await writeFile(
+ path.join(channelDir, RELOCATION_MARKER_FILENAME),
+ JSON.stringify({ target: relocatedDataDir(root, "alpha"), direction: "out", phase: "copy" }),
+ );
+ assert.equal((await inspectChannelMedia(paths, "alpha", undefined, { fresh: true })).text.readable, false);
+ await writeFile(
+ path.join(channelDir, RELOCATION_MARKER_FILENAME),
+ JSON.stringify({ target: relocatedMediaDir(root, "alpha"), direction: "out", phase: "copy" }),
+ );
+ assert.equal((await inspectChannelMedia(paths, "alpha", undefined, { fresh: true })).text.readable, true);
+ });
+});
diff --git a/common/lib/channelMedia.ts b/common/lib/channelMedia.ts
@@ -167,6 +167,19 @@ export function relocatedDataDir(root: string, slug: string): string {
return path.join(root.trim(), slug, "data");
}
+// WHETHER A MARKER HOLDS THE TEXT TOO. A media move (`scope` "media", or none
+// with a `<root>/<slug>/media` target) carries `media/` only, so its channel's
+// text stays readable. A tier migration rebuilds `data/` itself; and a marker
+// with no scope whose target is the retired `<root>/<slug>/data` shape is the
+// old whole-directory mover's, copying `data/` — both hold the text.
+export function markerHoldsText(
+ marker: Pick<RelocationMarker, "scope" | "target">,
+): boolean {
+ if (marker.scope === "tier-migration") return true;
+ if (marker.scope === "media") return false;
+ return path.basename(marker.target.replace(/\/+$/, "")) === "data";
+}
+
// The sentence a legacy channel is refused with, naming the way out.
export function legacyDetail(slug: string): string {
return (
@@ -482,7 +495,7 @@ async function inspectOnDisk(
target: mediaDir ?? marker.target,
text: {
dir: dataDir,
- readable: textReadable && marker.scope !== "tier-migration",
+ readable: textReadable && !markerHoldsText(marker),
},
status: "in-transition",
detail:
diff --git a/common/lib/mediaTier-server.test.ts b/common/lib/mediaTier-server.test.ts
@@ -15,7 +15,9 @@ import {
import { existsSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
+import { RELOCATION_MARKER_FILENAME } from "./channelMedia";
import {
+ TIER_RELOCATION_MARKER,
channelMediaLink,
relocatedMediaDir,
removeMediaFile,
@@ -270,3 +272,14 @@ test("the link carries the file's mtime, so an lstat answers freshness without t
assert.equal(l.mtimeMs, when.getTime());
await rm(f.root, { recursive: true, force: true });
});
+
+test("a move marker on the channel: the hook writes nothing into media/", async () => {
+ assert.equal(TIER_RELOCATION_MARKER, RELOCATION_MARKER_FILENAME);
+ const f = await fixture();
+ await mkdir(channelMediaLink(f.paths, f.slug));
+ await writeFile(path.join(f.channelsDir, f.slug, RELOCATION_MARKER_FILENAME), "{}");
+ assert.equal(await tierMediaFile(f.videoDir, "audio.mp3"), "left");
+ assert.deepEqual(await readdir(channelMediaLink(f.paths, f.slug)), []);
+ assert.ok((await lstat(path.join(f.videoDir, "audio.mp3"))).isFile());
+ await rm(f.root, { recursive: true, force: true });
+});
diff --git a/common/lib/mediaTier-server.ts b/common/lib/mediaTier-server.ts
@@ -47,6 +47,10 @@ import { onDrive, stalledLocationForPath } from "./storageHealth";
// The one name a channel's media tier is reached by: `channels/<slug>/media`.
export const MEDIA_LINK_NAME = "media";
+// channelMedia.ts's RELOCATION_MARKER_FILENAME, as a literal because that
+// module imports this one (mediaTier-server.test.ts pins that they agree).
+export const TIER_RELOCATION_MARKER = ".relocating.json";
+
export function channelMediaLink(
paths: Pick<Paths, "channelsDir">,
slug: string,
@@ -107,6 +111,17 @@ export type TierOptions = {
// answers within the watchdog's budget. False for a classic channel (no
// `media`), a dangling link (an unmounted drive), a stalled drive.
async function mediaTierReady(mediaRoot: string): Promise<boolean> {
+ // A move of this channel's media in flight (or interrupted): its marker
+ // stands in the channel dir, and nothing is written into `media/` while it
+ // does — the file stays real and the next sweep tiers it. (The writers that
+ // call the hook are held by the media guard during a move; this is the
+ // backstop for one that started before the marker.)
+ try {
+ await lstat(path.join(path.dirname(mediaRoot), TIER_RELOCATION_MARKER));
+ return false;
+ } catch {
+ /* no marker: the normal case */
+ }
let target = mediaRoot;
try {
const l = await lstat(mediaRoot);
diff --git a/umtool/report-to-video/cues.mjs b/umtool/report-to-video/cues.mjs
@@ -293,19 +293,24 @@ export function createCueSource({
// A tier migration in flight (or interrupted) is rebuilding `data/`: half
// of two places at once. The editor refuses such a channel; so does this.
- // A media move (`scope` "media", or none) leaves the text alone.
+ // A media move leaves the text alone.
let marker = null;
try {
marker = JSON.parse(await readFile(path.join(channelDir, ".relocating.json"), "utf8"));
} catch {
/* no marker: the normal case */
}
- if (
+ // `markerHoldsText` in channelMedia.ts: a tier migration, or a scope-less
+ // marker aimed at the retired `<root>/<slug>/data` shape (the old mover,
+ // copying the whole `data/`).
+ const holdsText =
marker &&
typeof marker.target === "string" &&
marker.target.trim() &&
- marker.scope === "tier-migration"
- ) {
+ (marker.scope === "tier-migration" ||
+ (marker.scope !== "media" &&
+ path.basename(marker.target.trim().replace(/\/+$/, "")) === "data"));
+ if (holdsText) {
fail(
`its media layout is being migrated (phase "${marker.phase ?? "copy"}") — ` +
`wait for archilyzer storage migrate-tier to finish`,