157 lines
4.6 KiB
JavaScript
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;
|
|
}
|