Archilyzer · Source

archilyzer

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

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:
Mcommon/controller/renameChannel.test.ts | 20++++++++++++++++++++
Mcommon/controller/renameChannel.ts | 20++++++++++++++++++--
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);