Recover failed conversion workers and validate Nitro downloads correctly
This commit is contained in:
1 parent
a78d8256e0
commit
7880d5d5df
5 files changed
+91
-8
No files matched your search
@@ -750,7 +750,7 @@ export async function importSingleFurni(params: {
|
||||
!existsSync(/*turbopackIgnore: true*/ nitroPath)
|
||||
) {
|
||||
const nitroUrl = `${sourceNitroBaseUrl}/${encodeURIComponent(safeNitroName)}.nitro`;
|
||||
const dl = await downloadFile(nitroUrl, nitroPath, { validate: "png" });
|
||||
const dl = await downloadFile(nitroUrl, nitroPath, { validate: "nitro" });
|
||||
if (dl.ok) nitroDownloadOk = true;
|
||||
}
|
||||
|
||||
@@ -843,7 +843,10 @@ export async function importSingleFurni(params: {
|
||||
warnings,
|
||||
error: isUpdate
|
||||
? "No .nitro file could be created; existing furniture was preserved"
|
||||
: "Import rolled back: no .nitro file could be created",
|
||||
: `Import rolled back: no .nitro file could be created. ${warnings
|
||||
.filter((warning) => /conversion|download|SWF/.test(warning))
|
||||
.slice(-3)
|
||||
.join(". ")}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { validatePngBytes, validateSwfBytes } from "./download";
|
||||
import { createNitroBundle } from "../../swf/nitro-builder";
|
||||
import {
|
||||
validateNitroBytes,
|
||||
validatePngBytes,
|
||||
validateSwfBytes,
|
||||
} from "./download";
|
||||
|
||||
describe("import/core/download validators", () => {
|
||||
it("accepts FWS/CWS/ZWS swf magic", () => {
|
||||
@@ -22,3 +27,14 @@ describe("import/core/download validators", () => {
|
||||
expect(validatePngBytes(Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("validates nitro bundles separately from PNG files", () => {
|
||||
const png = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
||||
const bundle = createNitroBundle({ name: "chair" }, png, "chair");
|
||||
expect(validateNitroBytes(bundle)).toBe(true);
|
||||
expect(validatePngBytes(bundle)).toBe(false);
|
||||
expect(validateNitroBytes(png)).toBe(false);
|
||||
expect(validateNitroBytes(Buffer.from("<html>Access denied</html>"))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import { parseNitroBundle } from "../../swf/nitro-builder";
|
||||
|
||||
import { browserHeaders } from "./browser-headers";
|
||||
|
||||
@@ -18,6 +19,20 @@ export function validatePngBytes(buffer: Buffer): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function validateNitroBytes(buffer: Buffer): boolean {
|
||||
try {
|
||||
const bundle = parseNitroBundle(buffer);
|
||||
return (
|
||||
!!bundle.json &&
|
||||
typeof bundle.json === "object" &&
|
||||
!Array.isArray(bundle.json) &&
|
||||
validatePngBytes(bundle.png)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function tryDownloadCandidates(
|
||||
urls: string[],
|
||||
destPath: string,
|
||||
@@ -33,7 +48,7 @@ export async function tryDownloadCandidates(
|
||||
export async function downloadFile(
|
||||
url: string,
|
||||
destPath: string,
|
||||
options?: { maxRetries?: number; validate?: "swf" | "png" },
|
||||
options?: { maxRetries?: number; validate?: "swf" | "png" | "nitro" },
|
||||
): Promise<{ ok: boolean; size: number }> {
|
||||
const maxRetries = options?.maxRetries ?? 3;
|
||||
const baseDelay = 1000;
|
||||
@@ -79,6 +94,8 @@ export async function downloadFile(
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (options?.validate === "nitro" && !validateNitroBytes(buffer))
|
||||
return { ok: false, size: 0 };
|
||||
await fs.writeFile(/*turbopackIgnore: true*/ destPath, buffer);
|
||||
return { ok: true, size: buffer.length };
|
||||
} catch (err) {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// @vitest-environment node
|
||||
import { EventEmitter } from "node:events";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
convert: vi.fn(() => ({ bundle: Buffer.from("converted") })),
|
||||
icon: vi.fn(() => Buffer.from("icon")),
|
||||
}));
|
||||
vi.mock("./index", () => ({
|
||||
convertSwfToNitro: mocks.convert,
|
||||
extractIconFromSwf: mocks.icon,
|
||||
}));
|
||||
vi.mock("node:worker_threads", () => ({
|
||||
Worker: class extends EventEmitter {
|
||||
postMessage() {
|
||||
queueMicrotask(() =>
|
||||
this.emit("error", new Error("worker module missing")),
|
||||
);
|
||||
}
|
||||
},
|
||||
}));
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
describe("conversion worker recovery", () => {
|
||||
it("falls back when worker startup rejects the conversion job", async () => {
|
||||
vi.stubEnv("NODE_ENV", "production");
|
||||
vi.stubEnv("VITEST", undefined);
|
||||
const { convertSwfToNitroAsync } = await import("./conversion-pool");
|
||||
const data = Buffer.from("SWF");
|
||||
const result = await convertSwfToNitroAsync(data, "chair");
|
||||
expect(result.bundle.toString()).toBe("converted");
|
||||
expect(mocks.convert).toHaveBeenCalledWith(data, "chair");
|
||||
});
|
||||
it("recovers icon extraction from a worker startup error", async () => {
|
||||
vi.stubEnv("NODE_ENV", "production");
|
||||
vi.stubEnv("VITEST", undefined);
|
||||
const { extractIconFromSwfAsync } = await import("./conversion-pool");
|
||||
expect(
|
||||
(await extractIconFromSwfAsync(Buffer.from("SWF"), "chair"))?.toString(),
|
||||
).toBe("icon");
|
||||
});
|
||||
});
|
||||
@@ -154,8 +154,10 @@ export async function convertSwfToNitroAsync(
|
||||
buffer: Buffer,
|
||||
classname: string,
|
||||
): Promise<ConversionResult> {
|
||||
const res = await run({ type: "convert", buffer, classname });
|
||||
if (res.ok && res.result) return res.result;
|
||||
const res = await run({ type: "convert", buffer, classname }).catch(
|
||||
() => null,
|
||||
);
|
||||
if (res?.ok && res.result) return res.result;
|
||||
// Worker failed for this item — fall back to the main thread.
|
||||
return convertSwfToNitro(buffer, classname);
|
||||
}
|
||||
@@ -164,7 +166,7 @@ export async function extractIconFromSwfAsync(
|
||||
buffer: Buffer,
|
||||
classname: string,
|
||||
): Promise<Buffer | null> {
|
||||
const res = await run({ type: "icon", buffer, classname });
|
||||
if (res.ok) return res.icon ?? null;
|
||||
const res = await run({ type: "icon", buffer, classname }).catch(() => null);
|
||||
if (res?.ok) return res.icon ?? null;
|
||||
return extractIconFromSwf(buffer, classname);
|
||||
}
|
||||
Reference in new issue
Block a user