118 lines
3.8 KiB
TypeScript
118 lines
3.8 KiB
TypeScript
import { createHash, randomUUID } from "node:crypto";
|
|
import { promises as fs } from "node:fs";
|
|
import path from "node:path";
|
|
import { withAdmin } from "@/lib/api-handler";
|
|
import { apiError, apiOk } from "@/lib/api-response";
|
|
import { generateScale32 } from "@/lib/furni/nitro-scale32";
|
|
import { validateClassnames } from "@/lib/furni/studio-inspection";
|
|
import { PERMS } from "@/lib/permission-slugs";
|
|
import { withCatalogExport } from "@/lib/services/catalog-git-queue";
|
|
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
|
import { withFurniDataLock } from "@/lib/services/furni-data";
|
|
import { parseNitroBundle } from "@/lib/services/swf/nitro-builder";
|
|
|
|
const hash = (data: Buffer) => createHash("sha256").update(data).digest("hex");
|
|
export const POST = withAdmin(
|
|
{ permission: PERMS.ASSETS_IMPORT },
|
|
async (request) => {
|
|
const body = await request.json();
|
|
if (
|
|
!validateClassnames([body.classname]) ||
|
|
!["preview", "apply", "restore"].includes(body.action)
|
|
)
|
|
return apiError("Invalid request", 400);
|
|
if (
|
|
body.action !== "preview" &&
|
|
!/^[a-f0-9]{64}$/.test(body.expectedHash || "")
|
|
)
|
|
return apiError("Preview the current bundle first", 400);
|
|
if (
|
|
body.action === "restore" &&
|
|
!/^[a-f0-9]{64}$/.test(body.backupHash || "")
|
|
)
|
|
return apiError("Invalid backup", 400);
|
|
const { nitroDir } = await getFurniAssetDirs(),
|
|
base = body.classname.split("*")[0],
|
|
file = path.join(nitroDir, `${base}.nitro`);
|
|
const execute = () =>
|
|
withFurniDataLock(async () => {
|
|
const original = await fs.readFile(file),
|
|
originalHash = hash(original);
|
|
if (body.action !== "preview" && originalHash !== body.expectedHash)
|
|
return apiError(
|
|
"The bundle changed. Reopen the editor and preview again.",
|
|
409,
|
|
);
|
|
const backupDir = path.join(
|
|
process.cwd(),
|
|
"storage",
|
|
"nitro-scale32-backups",
|
|
);
|
|
if (body.action === "restore") {
|
|
const backup = await fs.readFile(
|
|
path.join(backupDir, `${base}.${body.backupHash}.nitro`),
|
|
);
|
|
if (hash(backup) !== body.backupHash)
|
|
return apiError("Backup integrity check failed", 422);
|
|
const temp = `${file}.${randomUUID()}.tmp`;
|
|
try {
|
|
await fs.writeFile(temp, backup, { flag: "wx" });
|
|
await fs.rename(temp, file);
|
|
} finally {
|
|
await fs.rm(temp, { force: true });
|
|
}
|
|
return apiOk({
|
|
hash: hash(backup),
|
|
metadata: parseNitroBundle(backup).json,
|
|
});
|
|
}
|
|
const generated = await generateScale32(original);
|
|
if (body.action === "preview")
|
|
return apiOk({
|
|
hash: originalHash,
|
|
before: generated.before,
|
|
after: generated.after,
|
|
count: generated.count,
|
|
});
|
|
await fs.mkdir(backupDir, { recursive: true });
|
|
const backupFile = path.join(
|
|
backupDir,
|
|
`${base}.${originalHash}.nitro`,
|
|
);
|
|
try {
|
|
await fs.writeFile(backupFile, original, { flag: "wx" });
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
if (hash(await fs.readFile(backupFile)) !== originalHash)
|
|
throw Error("Backup integrity check failed");
|
|
}
|
|
const temp = `${file}.${randomUUID()}.tmp`;
|
|
try {
|
|
await fs.writeFile(temp, generated.bundle, { flag: "wx" });
|
|
await fs.rename(temp, file);
|
|
} finally {
|
|
await fs.rm(temp, { force: true });
|
|
}
|
|
return apiOk({
|
|
hash: hash(generated.bundle),
|
|
backupHash: originalHash,
|
|
metadata: parseNitroBundle(generated.bundle).json,
|
|
});
|
|
});
|
|
try {
|
|
return await (body.action === "preview"
|
|
? execute()
|
|
: withCatalogExport(execute));
|
|
} catch (error) {
|
|
return apiError(
|
|
(error as NodeJS.ErrnoException).code === "ENOENT"
|
|
? "Bundle or backup not found"
|
|
: error instanceof Error
|
|
? error.message
|
|
: "Scale generation failed",
|
|
422,
|
|
);
|
|
}
|
|
},
|
|
);
|