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:
1 parent
c95ad4a37f
commit
18825115a4
3 files changed
+53
-1
No files matched your search
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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", "")) ?? "";
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user