← back to Homesonspec
apps/workers/src/snapshot-path.ts
39 lines
import { isAbsolute, join, relative, resolve } from "node:path";
// Single source of truth for the raw-snapshot store location. Prod offloads this
// to a dedicated volume by setting SNAPSHOT_DIR (see ops/offload-snapshots.mjs);
// the default keeps the in-repo layout used by dev + fixtures.
const REPO_ROOT = join(import.meta.dirname, "../../..");
export const SNAPSHOT_DIR = process.env.SNAPSHOT_DIR ?? join(REPO_ROOT, "var/snapshots");
export function snapshotExt(contentType?: string) {
return contentType?.includes("json") ? ".json" : ".html";
}
export function snapshotRelativePath(sourceKey: string, contentHash: string, contentType?: string) {
return `${sourceKey}/${contentHash}${snapshotExt(contentType)}`;
}
// Strip a leading "./" then a leading "var/snapshots/" (the mirror-prefix some
// local dw rows carry) so the tail resolves under the active snapshot dir directly.
// ops/*.mjs mirror this in ops/lib/snapshot-resolve.mjs (kept in sync; JS/TS boundary).
const relTail = (p: string) => p.replace(/^(?:\.\/)?var\/snapshots\//, "");
// Legacy rows stored an ABSOLUTE storagePath (pre-offload); those must keep
// resolving verbatim. New rows store a POSIX-relative path resolved against the
// active snapshot dir, so moving the volume + repointing SNAPSHOT_DIR is exact.
export function resolveSnapshotPath(storagePath: string, snapshotDir = SNAPSHOT_DIR) {
if (isAbsolute(storagePath)) return storagePath;
const root = resolve(snapshotDir);
const candidate = resolve(root, relTail(storagePath));
const rel = relative(root, candidate);
if (
rel === ".." ||
rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) ||
isAbsolute(rel)
) {
throw new Error(`relative snapshot path escapes snapshot root: ${storagePath}`);
}
return candidate;
}