Auto-fix furni asset ownership after import

After each single or batch import, chown the swf/icon/nitro asset
directories and the FurnitureData.json folders to www-data:www-data so
nginx and the emulator can read newly written files. Best-effort and
non-blocking; the API response reports which folders were fixed.
This commit is contained in:
openhands committed 2026-08-18 19:52:32 +02:00
1 parent c95ad4a37f
commit 18825115a4
3 files changed
+53 -1

No files matched your search

+9
View File
@@ -17,6 +17,7 @@ import {
deleteImportedItem,
downloadFile,
ensureDirectories,
ensureFurniOwnership,
IMPORTED_PAGE_CAPTION_SAVE,
importSingleFurni,
reconcileImportedOfferIds,
@@ -407,6 +408,9 @@ export const POST = withAdmin(
// Reconcile offer_id with the furnidata sprite id across the tree
const reconciled = await reconcileImportedOfferIds();
// Keep asset + furnidata folders readable by the web server
const ownership = await ensureFurniOwnership();
const succeeded = results.filter((r) => r.ok).length;
const failed = results.filter((r) => !r.ok).length;
@@ -416,6 +420,7 @@ export const POST = withAdmin(
failed,
results,
offerIdsFixed: reconciled.fixed,
ownershipFixed: ownership.fixed,
});
}
@@ -468,6 +473,9 @@ export const POST = withAdmin(
// Reconcile offer_id with the furnidata sprite id across the tree
const reconciled = await reconcileImportedOfferIds();
// Keep asset + furnidata folders readable by the web server
const ownership = await ensureFurniOwnership();
logAudit({
userId: ctx.session.user.id,
action: "furni_import",
@@ -489,6 +497,7 @@ export const POST = withAdmin(
id: result.itemId,
catalogItemId: result.catalogItemId,
offerIdsFixed: reconciled.fixed,
ownershipFixed: ownership.fixed,
spriteCount: result.spriteCount,
warnings: result.warnings,
});
+1 -1
View File
@@ -35,7 +35,7 @@ export async function getFurnitureDataPath(): Promise<string> {
return path.join(/*turbopackIgnore: true*/ process.cwd(), "public", relative);
}
async function getFurnitureDataWritePaths(): Promise<string[]> {
export async function getFurnitureDataWritePaths(): Promise<string[]> {
const primary = await getFurnitureDataPath();
const configuredMirror =
(await siteSettings.get("furni_data_mirror_path", "")) ?? "";
+43
View File
@@ -1,5 +1,7 @@
import { execFile } from "node:child_process";
import { existsSync, promises as fs } from "node:fs";
import path from "node:path";
import { promisify } from "node:util";
import { and, eq, type SQL, sql } from "drizzle-orm";
import { CatalogPages, db, ItemsBase, LogsFurniImports } from "@/lib/db";
import { officialHabboEnrichmentWarning } from "@/lib/habbo-gamedata-hotel";
@@ -15,6 +17,7 @@ import {
appendFurniEntry,
buildFurniEntry,
findFurniDataIdConflict,
getFurnitureDataWritePaths,
removeFurniEntry,
} from "@/lib/services/furni-data";
import {
@@ -303,6 +306,46 @@ export async function reconcileImportedOfferIds(): Promise<{
return { fixed: Number(result.affectedRows ?? 0) };
}
const execFileAsync = promisify(execFile);
/**
* Ensure the furni asset directories (icons, nitros, swf) and the folders
* holding FurnitureData.json are owned by www-data:www-data, so nginx and the
* emulator can always read freshly imported files. Best-effort: if chown is
* unavailable or fails (e.g. running unprivileged), a warning is logged and
* import continues.
*/
export async function ensureFurniOwnership(): Promise<{ fixed: string[] }> {
if (process.platform === "win32") return { fixed: [] };
const { swfDir, iconDir, nitroDir, mirrorDirs } =
await getFurniAssetWriteTargets();
const furniDataPaths = await getFurnitureDataWritePaths();
const dirs = [
swfDir,
iconDir,
nitroDir,
...mirrorDirs.flatMap((m) => [m.swfDir, m.iconDir, m.nitroDir]),
...furniDataPaths.map((p) => path.dirname(p)),
]
.map((d) => path.normalize(d))
.filter((d, i, arr) => arr.indexOf(d) === i && existsSync(d));
if (dirs.length === 0) return { fixed: [] };
try {
await execFileAsync("chown", ["-R", "www-data:www-data", ...dirs]);
return { fixed: dirs };
} catch (err) {
logger.warn("[import-furni] chown to www-data failed", {
dirs,
error: (err as Error).message,
});
return { fixed: [] };
}
}
/**
* Allocate the next `catalog_items.id` and run the INSERT through a
* process-level async mutex so concurrent imports in the same Node process