commit 605cd6e51a17989d0f968ce4e7eae5bec8d7a321
parent 9babe0ee3ff820042c1b64655f8e298ea1a60877
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 1 Oct 2026 16:41:19 -0400
umtool: MEDIA_ROOT beside REPORTS_ROOT; CACHE_DIR moves to the user cache
UMTOOL_MEDIA_DIR names where a project's render scratch (out/) lives; unset,
MEDIA_ROOT is REPORTS_ROOT and nothing changes. MEDIA_TIERED says they differ;
mediaMirror(abs) is a project's path under it. MEDIA_ROOT joins READ_ROOTS,
never WRITE_ROOTS. CACHE_DIR is UMTOOL_CACHE_DIR, else
$XDG_CACHE_HOME/archilyzer/umtool (~/.cache when unset), no longer under
SONG_DATA; OLD_CACHE_DIR names the old place for umtool doctor.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
2 files changed, 69 insertions(+), 4 deletions(-)
diff --git a/umtool/lib/paths.mjs b/umtool/lib/paths.mjs
@@ -14,9 +14,25 @@ import { SONG_DATA, SONG_REPORTS } from "../song/paths.mjs";
// record paths relative to it (make-thumb, accept-thumb) import only siblings.
export { SONG_DATA, SONG_REPORTS };
-// Derived output (sliced mp3s, waveform peaks, the project index). Lives with
-// the data, not in the repo, and is safe to delete at any time.
-export const CACHE_DIR = path.join(SONG_DATA, ".cache", "umtool");
+// Derived output (sliced mp3s, waveform peaks, the project index, posters, the
+// mix bench's analyses). Not in the repo, and safe to delete at any time.
+//
+// It used to live under SONG_DATA (`<SONG_DIR>/.cache/umtool`), which tied
+// every project's index and every report's derived files to wherever the song
+// project's 39 GB happened to sit -- a report-only machine, or one whose song
+// data is on a drive that is not mounted, had its cache follow it there
+// (release 17). It is a cache, so it goes where caches go:
+// `UMTOOL_CACHE_DIR`, else `$XDG_CACHE_HOME/archilyzer/umtool` (an empty
+// XDG_CACHE_HOME is unset, as common/lib/paths.ts reads it), else
+// `~/.cache/archilyzer/umtool`. Nothing is migrated: the first `umtool index`
+// rebuilds the index there, and every other file is re-made on demand.
+// OLD_CACHE_DIR is only for `umtool doctor`, which reports a leftover one.
+const XDG_CACHE = process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
+export const CACHE_DIR = path.resolve(
+ /* turbopackIgnore: true */
+ process.env.UMTOOL_CACHE_DIR || path.join(/* turbopackIgnore: true */ XDG_CACHE, "archilyzer", "umtool"),
+);
+export const OLD_CACHE_DIR = path.join(/* turbopackIgnore: true */ SONG_DATA, ".cache", "umtool");
/**
* A file in CACHE_DIR, by name. A route names its cache files through this
@@ -61,6 +77,51 @@ export const REPORTS_ROOT = path.resolve(
const dedupe = (list) => [...new Set(list.map((p) => path.resolve(p)))];
// ---------------------------------------------------------------------------
+// MEDIA_ROOT -- where a project's RENDER SCRATCH (`out/`) lives (release 17).
+//
+// A report project's manifest, revisions/, notes and sources are small text and
+// stay under REPORTS_ROOT. Its `out/` -- fetched windows, segments, the
+// deliverable, ~18 of the 20 GB in ~/reports -- is bulk that can be re-made,
+// and belongs on a media drive. With UMTOOL_MEDIA_DIR set, a project's `out` is
+// an absolute SYMLINK to the same project-relative path under it:
+//
+// <REPORTS_ROOT>/<folder>/<project>/out -> <MEDIA_ROOT>/<folder>/<project>/out
+//
+// made by the first writer (lib/report/storage.mjs ensureOutDir) or moved there
+// by `umtool storage move-out`. Every reader keeps opening `<project>/out/...`
+// by path; the link is the only place the media drive is named.
+//
+// UNSET, MEDIA_ROOT is REPORTS_ROOT, MEDIA_TIERED is false, and nothing changes:
+// `out/` is a plain directory in the project, as it always was.
+//
+// MEDIA_ROOT is READABLE (a realpath taken through a project's `out` link lands
+// under it -- the deck preview's confinement check is one) and never WRITABLE by
+// a client-named path: what a render may write to is still WRITE_ROOTS.
+// ---------------------------------------------------------------------------
+export const MEDIA_ROOT = path.resolve(
+ /* turbopackIgnore: true */
+ process.env.UMTOOL_MEDIA_DIR || REPORTS_ROOT,
+);
+
+/** True when render scratch goes to a media root of its own. */
+export const MEDIA_TIERED = MEDIA_ROOT !== REPORTS_ROOT;
+
+/**
+ * Where `abs` (a path under REPORTS_ROOT) is mirrored under MEDIA_ROOT, or null
+ * when it is not under REPORTS_ROOT -- a project somewhere else is never tiered.
+ * Pure: it never touches the disk. The roots are parameters so a test can name
+ * its own; the defaults are the process's.
+ */
+export function mediaMirror(abs, { reportsRoot = REPORTS_ROOT, mediaRoot = MEDIA_ROOT } = {}) {
+ const rel = path.relative(
+ /* turbopackIgnore: true */ path.resolve(/* turbopackIgnore: true */ reportsRoot),
+ path.resolve(/* turbopackIgnore: true */ abs),
+ );
+ if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return null;
+ return path.join(/* turbopackIgnore: true */ path.resolve(/* turbopackIgnore: true */ mediaRoot), rel);
+}
+
+// ---------------------------------------------------------------------------
// READ vs WRITE, and why they are two lists.
//
// resolveInRoots() guards both what may be OPENED and what may be RENDERED TO.
@@ -148,7 +209,7 @@ export const CHANNELS_DIR = path.resolve(
export const READ_ROOTS = dedupe(
process.env.MIX_ROOTS
? process.env.MIX_ROOTS.split(":").filter(Boolean)
- : [SONG_REPORTS, REPORTS_ROOT, SONG_DATA, SONG_SCRATCH, CHANNELS_DIR],
+ : [SONG_REPORTS, REPORTS_ROOT, SONG_DATA, SONG_SCRATCH, CHANNELS_DIR, MEDIA_ROOT],
);
export const WRITE_ROOTS = dedupe(
diff --git a/umtool/lib/paths.ts b/umtool/lib/paths.ts
@@ -8,6 +8,7 @@ import path from "node:path";
// SONG_REPORTS the um-song deliverables -- quartering-*.mp4 and their .plan.json
// SONG_SCRATCH render scratch, ABOVE SONG_DATA
// REPORTS_ROOT the tree every PROJECT hangs off -- songs AND report videos
+// MEDIA_ROOT where a project's out/ is linked to (UMTOOL_MEDIA_DIR); = REPORTS_ROOT when unset
//
// Everything the bench reads or writes must resolve inside one of these. Not
// because this is exposed -- it is a local tool on a loopback port -- but
@@ -21,7 +22,9 @@ export {
CACHE_DIR,
cacheFile,
INDEX_DIR,
+ MEDIA_ROOT,
MEDIA_ROOTS,
+ MEDIA_TIERED,
MIX_CACHE,
READ_ROOTS,
REPORTS_ROOT,
@@ -31,6 +34,7 @@ export {
WRITE_ROOTS,
inside,
labelFor,
+ mediaMirror,
resolveInRoots,
} from "./paths.mjs";