Files
MineGeo/scripts/repair-panoramas.mjs

157 lines
4.6 KiB
JavaScript

import { createClient } from "@supabase/supabase-js";
import { createHash } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
import { gunzipSync } from "node:zlib";
import { renderSnapshot } from "../renderer/render.mjs";
if (existsSync(".env.local")) process.loadEnvFile(".env.local");
const id = process.argv[2];
if (
!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(
id || "",
)
)
throw new Error(
"Usage: npm run repair:panoramas -- <private plugin draft UUID>",
);
const client = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.SUPABASE_SECRET_KEY,
{ auth: { persistSession: false, autoRefreshToken: false } },
);
function check(r) {
if (r.error) throw new Error(r.error.message);
return r.data;
}
const dataset = check(
await client.from("datasets").select("*").eq("id", id).single(),
);
if (
dataset.source !== "plugin" ||
dataset.revision_id ||
dataset.parent_dataset_id ||
!["draft", "rejected"].includes(dataset.state)
)
throw new Error(
"Only a private, unpublished plugin draft can be repaired. Withdraw review or recover failed processing first.",
);
const job = check(
await client
.from("upload_jobs")
.select("state")
.eq("dataset_id", id)
.single(),
);
if (job.state !== "complete")
throw new Error("Wait for processing to finish first.");
const input = check(
await client
.from("dataset_inputs")
.select("manifest,validated")
.eq("dataset_id", id)
.single(),
);
if (!input.validated) throw new Error("The export must be validated first.");
if (
(input.manifest.availableViewpoints ?? input.manifest.viewpoints).some(
(p) => p.assetRoot,
)
)
throw new Error(
"Repair mixed-media drafts by uploading a new capture; accepted contribution files cannot be overwritten.",
);
const asset = input.manifest.assets.find((a) => a.path === "terrain.json.gz");
if (!asset) throw new Error("Missing original terrain export");
const raw = check(
await client.storage
.from("minegeo-raw")
.download(`${dataset.owner_id}/${id}/${asset.path}`),
);
const bytes = Buffer.from(await raw.arrayBuffer());
if (
bytes.length !== asset.bytes ||
createHash("sha256").update(bytes).digest("hex") !== asset.sha256
)
throw new Error("Original terrain checksum mismatch");
const output = `.cache/panorama-repair/${id}`;
await mkdir(output, { recursive: true });
const terrain = JSON.parse(
gunzipSync(bytes, { maxOutputLength: 256 * 1024 * 1024 }).toString(),
);
await renderSnapshot(
terrain,
input.manifest.availableViewpoints ?? input.manifest.viewpoints,
output,
{
size: Number(process.env.MINEGEO_FACE_SIZE || 1024),
onProgress: async ({ percent, message }) =>
console.log(`${Math.floor(percent)}% ${message}`),
},
);
// Render first, then briefly lock the draft while replacing its private images.
// Its complete job stays complete, so another worker cannot pick it up.
const locked = check(
await client
.from("datasets")
.update({ state: "processing", updated_at: new Date().toISOString() })
.eq("id", id)
.eq("state", dataset.state)
.eq("updated_at", dataset.updated_at)
.is("revision_id", null)
.select("id"),
);
if (locked.length !== 1)
throw new Error(
"Draft changed during rendering; images were retained locally but nothing was replaced.",
);
try {
for (const entry of (await readdir(output, { recursive: true })).filter((p) =>
p.endsWith(".webp"),
)) {
check(
await client.storage
.from("minegeo-media")
.upload(
`${id}/${entry.replaceAll("\\", "/")}`,
await readFile(`${output}/${entry}`),
{ upsert: true, contentType: "image/webp", cacheControl: "60" },
),
);
}
check(
await client
.from("datasets")
.update({ state: dataset.state, updated_at: new Date().toISOString() })
.eq("id", id)
.eq("state", "processing")
.is("revision_id", null)
.select("id")
.single(),
);
await writeFile(
`${output}/repair.json`,
JSON.stringify({
datasetId: id,
repaired: true,
at: new Date().toISOString(),
}),
);
console.log(
`Repaired draft ${id}. Reload its dashboard preview; no Minecraft export or new upload is needed.`,
);
} catch (error) {
await client
.from("datasets")
.update({
state: "failed",
rejection_reason:
"Image repair interrupted. Reprocess the retained raw upload.",
updated_at: new Date().toISOString(),
})
.eq("id", id)
.eq("state", "processing")
.is("revision_id", null);
throw error;
}