fix: mirror manual furni uploads to live assets
CI / check (push) Successful in 24s
CI / release (push) Skipped
CI / deploy (push) Successful in 46s

This commit is contained in:
Simo committed 2026-08-02 13:24:15 +02:00
1 parent 01570874a8
commit 86cd43def9
2 files changed
+193 -2

No files matched your search

+139
View File
@@ -0,0 +1,139 @@
// @vitest-environment node
import { promises as fs } from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const {
appendFurniEntryMock,
assetTargets,
dbExecuteMock,
getOrCreateCategoryPageMock,
} = vi.hoisted(() => ({
appendFurniEntryMock: vi.fn(),
assetTargets: {
swfDir: "",
iconDir: "",
nitroDir: "",
mirrorDirs: [] as Array<{
swfDir: string;
iconDir: string;
nitroDir: string;
}>,
},
dbExecuteMock: vi.fn(),
getOrCreateCategoryPageMock: vi.fn(),
}));
vi.mock("drizzle-orm", () => ({
eq: vi.fn(() => ({})),
sql: (strings: TemplateStringsArray, ...values: unknown[]) => ({
strings,
values,
}),
}));
vi.mock("@/lib/db", () => ({
ItemsBase: { id: "id", itemName: "item_name" },
db: {
execute: dbExecuteMock,
select: () => ({
from: () => ({
where: () => ({
limit: async () => [],
}),
}),
}),
},
}));
vi.mock("@/lib/services/furni-asset-dirs", () => ({
getFurniAssetDirs: vi.fn(async () => assetTargets),
getFurniAssetWriteTargets: vi.fn(async () => assetTargets),
}));
vi.mock("@/lib/services/furni-data", () => ({
appendFurniEntry: appendFurniEntryMock,
buildFurniEntry: vi.fn(() => ({ id: 501 })),
}));
vi.mock("@/lib/services/furni-import", () => ({
allocateCatalogItemId: vi.fn(
async (insertFn: (nextId: number) => Promise<unknown>) => insertFn(701),
),
autoPriceFurni: vi.fn(() => ({ credits: 3, points: 0, pointsType: 0 })),
ensureDirectories: vi.fn(async () => {
const dirs = [
assetTargets.swfDir,
assetTargets.iconDir,
assetTargets.nitroDir,
...assetTargets.mirrorDirs.flatMap((target) => [
target.swfDir,
target.iconDir,
target.nitroDir,
]),
];
await Promise.all(dirs.map((dir) => fs.mkdir(dir, { recursive: true })));
}),
getOrCreateCategoryPage: getOrCreateCategoryPageMock,
}));
vi.mock("@/lib/services/swf/nitro-builder", () => ({
parseNitroBundle: vi.fn(() => ({ json: { xdim: 1, ydim: 1 } })),
}));
import { uploadSingleFurni } from "./upload-import";
describe("uploadSingleFurni live asset mirrors", () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "upload-furni-"));
assetTargets.swfDir = path.join(tempDir, "cms", "swf");
assetTargets.iconDir = path.join(tempDir, "cms", "icons");
assetTargets.nitroDir = path.join(tempDir, "cms", "furniture");
assetTargets.mirrorDirs = [
{
swfDir: assetTargets.swfDir,
iconDir: path.join(tempDir, "Gamedata", "icons"),
nitroDir: path.join(tempDir, "Gamedata", "bundled", "furniture"),
},
];
dbExecuteMock.mockReset();
dbExecuteMock
.mockResolvedValueOnce([[{ next: 501 }], undefined])
.mockResolvedValue(undefined);
getOrCreateCategoryPageMock.mockResolvedValue("imp_other");
appendFurniEntryMock.mockResolvedValue(undefined);
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
vi.clearAllMocks();
});
it("copies the Nitro bundle and icon into the directories served by production", async () => {
const nitroBuffer = Buffer.from("nitro-bundle");
const iconBuffer = Buffer.from("png-icon");
const result = await uploadSingleFurni({
classname: "chair",
name: "Chair",
itemType: "s",
nitroBuffer,
iconBuffer,
});
expect(result.ok).toBe(true);
await expect(
fs.readFile(
path.join(assetTargets.mirrorDirs[0].nitroDir, "chair.nitro"),
),
).resolves.toEqual(nitroBuffer);
await expect(
fs.readFile(
path.join(assetTargets.mirrorDirs[0].iconDir, "chair_icon.png"),
),
).resolves.toEqual(iconBuffer);
});
});
+54 -2
View File
@@ -2,7 +2,7 @@ import { existsSync, promises as fs } from "node:fs";
import path from "node:path";
import { eq, sql } from "drizzle-orm";
import { db, ItemsBase } from "@/lib/db";
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
import { getFurniAssetWriteTargets } from "@/lib/services/furni-asset-dirs";
import { appendFurniEntry, buildFurniEntry } from "@/lib/services/furni-data";
import {
allocateCatalogItemId,
@@ -43,6 +43,36 @@ const MIGRATIONS_DIR = path.resolve(
let itemsBaseIdAllocChain: Promise<unknown> = Promise.resolve();
function pathKey(value: string): string {
const normalized = path.normalize(value);
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
}
async function mirrorUploadedAsset(
sourcePath: string,
fileName: string,
dirs: string[],
warnings: string[],
copiedPaths: string[],
): Promise<void> {
const sourceKey = pathKey(sourcePath);
const seen = new Set<string>();
for (const dir of dirs) {
const targetPath = path.join(/*turbopackIgnore: true*/ dir, fileName);
const targetKey = pathKey(targetPath);
if (targetKey === sourceKey || seen.has(targetKey)) continue;
seen.add(targetKey);
try {
await fs.copyFile(sourcePath, targetPath);
copiedPaths.push(targetPath);
} catch (err) {
warnings.push(
`asset mirror failed (${targetPath}): ${(err as Error).message}`,
);
}
}
}
async function allocateItemsBaseId<T>(
insertFn: (nextId: number) => Promise<T>,
): Promise<T> {
@@ -200,7 +230,8 @@ export async function uploadSingleFurni(params: {
}
await ensureDirectories();
const { iconDir, nitroDir } = await getFurniAssetDirs();
const assetTargets = await getFurniAssetWriteTargets();
const { iconDir, nitroDir } = assetTargets;
const nitroFileName = `${classname}.nitro`;
const nitroPath = path.join(
@@ -225,6 +256,24 @@ export async function uploadSingleFurni(params: {
await fs.writeFile(iconPath, iconBuffer);
}
const mirroredPaths: string[] = [];
await mirrorUploadedAsset(
nitroPath,
nitroFileName,
assetTargets.mirrorDirs.map((target) => target.nitroDir),
warnings,
mirroredPaths,
);
if (iconPath && iconFileName) {
await mirrorUploadedAsset(
iconPath,
iconFileName,
assetTargets.mirrorDirs.map((target) => target.iconDir),
warnings,
mirroredPaths,
);
}
const xdim = userXdim ?? Number(nitroMeta?.xdim ?? 1);
const ydim = userYdim ?? Number(nitroMeta?.ydim ?? 1);
@@ -248,6 +297,9 @@ export async function uploadSingleFurni(params: {
} catch (err) {
await fs.unlink(nitroPath).catch(() => {});
if (iconPath) await fs.unlink(iconPath).catch(() => {});
await Promise.all(
mirroredPaths.map((file) => fs.unlink(file).catch(() => {})),
);
return {
ok: false,
classname,