commit 9849af0a3bb6302a36a36a3dc4d486af3807253e
parent d2bf82cf88f698f5d190a8741919137006049e04
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 2 Oct 2026 01:30:55 -0400
common: review L2 — a legacy channel's rename is refused, naming migrate-tier
The tier migration reads dataDir as <root>/<slug>/data; a rename that left the
tree under the old slug would break it. Refused before anything moves.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
2 files changed, 38 insertions(+), 2 deletions(-)
diff --git a/common/controller/renameChannel.test.ts b/common/controller/renameChannel.test.ts
@@ -247,6 +247,26 @@ test("rename rolls the channel dir back when the media move fails", async () =>
});
});
+// THE RETIRED LAYOUT (release 17 review L2): a legacy channel is migrated
+// before it is renamed, so the tier migration only ever sees
+// `dataDir = <root>/<slug>/data`.
+test("a legacy channel's rename is refused, naming migrate-tier, and nothing moves", async () => {
+ await withPaths(async (paths) => {
+ const dir = path.dirname(paths.channelsDir);
+ const target = path.join(dir, "platter", "old", "data");
+ await writeChannelConfig(paths, "old", { ...config, dataDir: target });
+ await mkdir(path.join(target, "vid1"), { recursive: true });
+ await symlink(target, path.join(paths.channelsDir, "old", "data"));
+ await assert.rejects(
+ () => renameChannel(paths, "old", "new", { ...config, dataDir: target }),
+ /cannot be renamed: .*archilyzer storage migrate-tier old/,
+ );
+ assert.equal(await channelExists(paths, "old"), true);
+ assert.equal(await channelExists(paths, "new"), false);
+ await stat(path.join(target, "vid1"));
+ });
+});
+
test("rename is refused while a relocation is in flight", async () => {
await withPaths(async (paths) => {
await writeChannelConfig(paths, "old", config);
diff --git a/common/controller/renameChannel.ts b/common/controller/renameChannel.ts
@@ -1,5 +1,5 @@
import path from "node:path";
-import { readdir, rename, stat, symlink, unlink } from "node:fs/promises";
+import { lstat, readdir, rename, stat, symlink, unlink } from "node:fs/promises";
import type { Paths } from "../lib/paths";
import type { ChannelConfig } from "../lib/channelConfig";
import {
@@ -9,7 +9,7 @@ import {
writeChannelConfig,
} from "./channels";
import { savedVideoRoot } from "../lib/savedVideo";
-import { readRelocationMarker } from "../lib/channelMedia";
+import { legacyDetail, readRelocationMarker } from "../lib/channelMedia";
import { MEDIA_LINK_NAME, relocatedMediaDir } from "../lib/mediaTier-server";
import { rewriteSavedVideoDir } from "../lib/savedVideo-server";
import { getSite, listSiteIds, writeSite } from "../lib/site";
@@ -91,6 +91,22 @@ export async function renameChannel(
);
}
+ // THE RETIRED LAYOUT (release 17) is migrated before it is renamed: the
+ // tier migration renames `<root>/<slug>/data` to `<root>/<slug>/media` and
+ // reads `dataDir` as exactly that shape, so a rename that left the tree
+ // under the old slug would break it. Refused before anything moves.
+ let dataIsLink = false;
+ try {
+ dataIsLink = (await lstat(path.join(oldChannelDir, "data"))).isSymbolicLink();
+ } catch {
+ /* no data/ */
+ }
+ if (dataIsLink || config.dataDir?.trim()) {
+ throw new Error(
+ `Channel "${oldSlug}" cannot be renamed: ${legacyDetail(oldSlug)}.`,
+ );
+ }
+
const storeRoot = savedVideoRoot(paths, config);
const oldStoreDir = path.join(storeRoot, oldSlug);
const newStoreDir = path.join(storeRoot, newSlug);