← 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;
}