feat(catalog): queue furniture imports with history and matching file attachments
CI / check (push) Successful in 1m30s
CI / deploy (push) Successful in 1m26s
CI / e2e (push) Successful in 25s

This commit is contained in:
Simo committed 2026-09-05 17:02:26 +02:00
1 parent 775d14f861
commit 2619bec165
16 files changed
+1016 -232

No files matched your search

+28 -6
View File
@@ -37,6 +37,7 @@ import {
} from "@/lib/services/swf-to-nitro";
import { getRuntimePath } from "@/lib/utils/runtime-path";
import type { ImportSingleResult } from "@/types/furni";
import { extractFurniIconPng } from "./clone-icon";
import { reserveFurnitureId } from "./furniture-id-reservation";
// Re-export the download helpers now owned by the shared import core.
@@ -512,6 +513,8 @@ export async function importSingleFurni(params: {
skipFurniDataWrite?: boolean;
updateExisting?: boolean;
repairExisting?: boolean;
/** Validated matching bundle supplied by a queued import. */
providedNitro?: Buffer;
onProgress?: (status: string) => void;
/** Per-source SWF download base URL (e.g. "https://virtualc.nl/dcr"). */
sourceSwfBaseUrl?: string;
@@ -645,6 +648,10 @@ export async function importSingleFurni(params: {
const mirrorIconDirs = assetTargets.mirrorDirs.map((d) => d.iconDir);
const mirrorNitroDirs = assetTargets.mirrorDirs.map((d) => d.nitroDir);
if (params.providedNitro && !existsSync(nitroPath)) {
await fs.writeFile(nitroPath, params.providedNitro, { flag: "wx" });
}
// ── Download assets in parallel (with retry + validation) ────────
// Icon strategy matches Nitro's URL template:
// `${hof.furni.url}/icons/%libname%%param%_icon.png`
@@ -700,11 +707,13 @@ export async function importSingleFurni(params: {
: tryDownloadCandidates(iconUrls, iconPath, "png", (detail) => {
iconFailure = detail;
}),
preservingExisting && existsSync(swfPath)
? Promise.resolve(true)
: tryDownloadCandidates(swfUrls, swfPath, "swf", (detail) => {
swfFailure = detail;
}),
params.providedNitro
? Promise.resolve(false)
: preservingExisting && existsSync(swfPath)
? Promise.resolve(true)
: tryDownloadCandidates(swfUrls, swfPath, "swf", (detail) => {
swfFailure = detail;
}),
]);
let iconOk = iconOkResult;
@@ -744,7 +753,7 @@ export async function importSingleFurni(params: {
);
}
if (!swfOk && !nitroDownloadOk)
if (!swfOk && !nitroDownloadOk && !params.providedNitro)
warnings.push(`SWF download failed: ${swfFailure || "unknown cause"}`);
// ── Convert SWF to Nitro ──────────────────────────────────────────
@@ -781,6 +790,19 @@ export async function importSingleFurni(params: {
}
}
if (!iconOk && existsSync(nitroPath)) {
try {
const icon = extractFurniIconPng(await fs.readFile(nitroPath));
if (icon) {
await fs.writeFile(iconPath, icon);
iconOk = true;
warnings.push("Icon extracted from Nitro bundle");
}
} catch {
warnings.push("Could not extract an icon from the Nitro bundle");
}
}
// ── Rollback: if conversion/download failed AND no .nitro exists, remove DB record ──
// Also validate that the .nitro file is non-trivial (≥ 128 bytes — a bare
// Nitro bundle header is larger than that, anything smaller is corrupt/empty).