Files
EpicNext-Cms/src/lib/services/repair-icons.ts
T
openhands 804daeffca
Deploy / release (push) Skipped
Deploy / deploy (push) Failing after 32s
feat: add .nitro upload + sync-all + repair-icons features
- Upload .nitro bundles directly with full DB, catalog, and furnidata integration
- Generate SQL migration files on upload (optional)
- Auto-sync missing furniture from all configured clone sources (SSE batch)
- Repair missing icons by extracting from local .nitro or downloading from sources
- All behind ASSETS_IMPORT permission
2026-07-21 14:53:26 +02:00

127 lines
2.9 KiB
TypeScript

import { existsSync, promises as fs } from "node:fs";
import path from "node:path";
import { prisma } from "@/lib/prisma";
import { extractFurniIconPng } from "@/lib/services/clone-icon";
import { listSources } from "@/lib/services/clone-sources";
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
import { downloadFile } from "@/lib/services/import/core/download";
import { ensureDirectories } from "./furni-import";
export interface RepairEvent {
type: "item_progress" | "batch_complete" | "error";
classname?: string;
status?: string;
index?: number;
message?: string;
warnings?: string[];
succeeded?: number;
failed?: number;
duration?: number;
}
export async function repairMissingIcons(
onEvent?: (evt: RepairEvent) => void,
): Promise<{ succeeded: number; failed: number }> {
await ensureDirectories();
const { iconDir, nitroDir } = await getFurniAssetDirs();
const items = await prisma.$queryRaw<
Array<{ id: number; item_name: string }>
>`SELECT id, item_name FROM items_base ORDER BY id`;
if (items.length === 0) {
onEvent?.({
type: "batch_complete",
succeeded: 0,
failed: 0,
duration: 0,
});
return { succeeded: 0, failed: 0 };
}
let succeeded = 0;
let failed = 0;
for (let i = 0; i < items.length; i++) {
const { item_name: classname } = items[i];
const iconPath = path.join(iconDir, `${classname}_icon.png`);
if (existsSync(iconPath)) continue;
onEvent?.({
type: "item_progress",
classname,
status: "started",
index: i,
});
let ok = false;
const warnings: string[] = [];
const nitroPath = path.join(nitroDir, `${classname}.nitro`);
if (existsSync(nitroPath)) {
try {
const nitroBuf = await fs.readFile(nitroPath);
const icon = extractFurniIconPng(nitroBuf);
if (icon) {
await fs.writeFile(iconPath, icon);
ok = true;
} else {
warnings.push("no icon asset in .nitro bundle");
}
} catch (err) {
warnings.push(`extract from .nitro failed: ${(err as Error).message}`);
}
} else {
warnings.push(".nitro file not found on disk");
}
if (!ok) {
const sources = await listSources();
for (const source of sources) {
try {
const dl = await downloadFile(
`${source.iconBaseUrl}/${classname}_icon.png`,
iconPath,
{ maxRetries: 1, validate: "png" },
);
if (dl.ok) {
ok = true;
break;
}
} catch {
warnings.push(`download from ${source.name} failed`);
}
}
}
if (ok) {
succeeded++;
onEvent?.({
type: "item_progress",
classname,
status: "done",
index: i,
warnings: warnings.length ? warnings : undefined,
});
} else {
failed++;
onEvent?.({
type: "item_progress",
classname,
status: "failed",
index: i,
message: warnings[0] || "could not obtain icon",
});
}
}
onEvent?.({
type: "batch_complete",
succeeded,
failed,
});
return { succeeded, failed };
}