62 lines
3.0 KiB
JavaScript
62 lines
3.0 KiB
JavaScript
import { cp, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
import { dirname, join, relative, resolve } from "node:path";
|
|
|
|
const root = resolve(new URL("..", import.meta.url).pathname);
|
|
const runtimeDir = process.env.FOUNDRY_RUNTIME_DIR ? resolve(root, process.env.FOUNDRY_RUNTIME_DIR) : join(root, "runtime-data");
|
|
const seedDir = resolve(process.env.FOUNDRY_RUNTIME_SEED_DIR || join(root, "runtime-seed"));
|
|
const manifestPath = join(seedDir, "seed-manifest.json");
|
|
|
|
function isInside(parent, target) {
|
|
const path = relative(parent, target);
|
|
return path && !path.startsWith("..") && !path.includes("../");
|
|
}
|
|
|
|
async function exists(path) {
|
|
try { await stat(path); return true; } catch (error) { if (error?.code === "ENOENT") return false; throw error; }
|
|
}
|
|
|
|
async function copySeedFile(file) {
|
|
const source = resolve(seedDir, file);
|
|
const target = resolve(runtimeDir, file);
|
|
if (!isInside(seedDir, source) || !isInside(runtimeDir, target)) throw new Error("runtime_seed_path_invalid");
|
|
if (!await exists(source)) throw new Error(`runtime_seed_file_missing:${file}`);
|
|
await mkdir(dirname(target), { recursive: true });
|
|
await cp(source, target, { force: true, preserveTimestamps: true });
|
|
}
|
|
|
|
async function isGeneratedFallback() {
|
|
const profilePath = join(runtimeDir, "design-profiles", "default.json");
|
|
if (!await exists(profilePath)) return true;
|
|
try {
|
|
const profile = JSON.parse(await readFile(profilePath, "utf8"));
|
|
// The server fallback has the same default media paths as the canonical
|
|
// profile, but it has never been persisted as a real Design Profile
|
|
// layout. `savedAt` is written by the baseline/profile save path. A
|
|
// subsequently edited user profile must never be replaced implicitly.
|
|
return profile?.id === "default"
|
|
&& profile?.name === "NODE.DC Default"
|
|
&& profile?.status === "draft"
|
|
&& !String(profile?.layout?.savedAt || "").trim()
|
|
&& String(profile?.timestamps?.createdAt || "") === String(profile?.timestamps?.updatedAt || "");
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
if (!/^\d+\.\d+\.\d+$/.test(String(manifest.version || ""))) throw new Error("runtime_seed_version_invalid");
|
|
if (!Array.isArray(manifest.files) || !manifest.files.every((file) => typeof file === "string" && file.length > 0)) {
|
|
throw new Error("runtime_seed_manifest_invalid");
|
|
}
|
|
|
|
const marker = join(runtimeDir, "runtime-seed", `${manifest.version}.json`);
|
|
if (!await exists(marker)) {
|
|
const shouldApply = await isGeneratedFallback();
|
|
if (shouldApply) for (const file of manifest.files) await copySeedFile(file);
|
|
await mkdir(dirname(marker), { recursive: true });
|
|
const temporaryMarker = `${marker}.tmp`;
|
|
await writeFile(temporaryMarker, `${JSON.stringify({ version: manifest.version, appliedAt: new Date().toISOString(), applied: shouldApply }, null, 2)}\n`, "utf8");
|
|
await rename(temporaryMarker, marker);
|
|
console.log(`Foundry runtime seed ${manifest.version}: ${shouldApply ? "applied" : "preserved existing runtime"}`);
|
|
}
|