feat(catalog): generate derived scale 32 with preview and backup restore
This commit is contained in:
1 parent
08b0b5021e
commit
cd75361b9f
9 files changed
+749
-11
No files matched your search
@@ -0,0 +1,117 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user