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,
|
deleteImportedItem,
|
||||||
downloadFile,
|
downloadFile,
|
||||||
ensureDirectories,
|
ensureDirectories,
|
||||||
|
ensureFurniOwnership,
|
||||||
IMPORTED_PAGE_CAPTION_SAVE,
|
IMPORTED_PAGE_CAPTION_SAVE,
|
||||||
importSingleFurni,
|
importSingleFurni,
|
||||||
reconcileImportedOfferIds,
|
reconcileImportedOfferIds,
|
||||||
@@ -407,6 +408,9 @@ export const POST = withAdmin(
|
|||||||
// Reconcile offer_id with the furnidata sprite id across the tree
|
// Reconcile offer_id with the furnidata sprite id across the tree
|
||||||
const reconciled = await reconcileImportedOfferIds();
|
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 succeeded = results.filter((r) => r.ok).length;
|
||||||
const failed = results.filter((r) => !r.ok).length;
|
const failed = results.filter((r) => !r.ok).length;
|
||||||
|
|
||||||
@@ -416,6 +420,7 @@ export const POST = withAdmin(
|
|||||||
failed,
|
failed,
|
||||||
results,
|
results,
|
||||||
offerIdsFixed: reconciled.fixed,
|
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
|
// Reconcile offer_id with the furnidata sprite id across the tree
|
||||||
const reconciled = await reconcileImportedOfferIds();
|
const reconciled = await reconcileImportedOfferIds();
|
||||||
|
|
||||||
|
// Keep asset + furnidata folders readable by the web server
|
||||||
|
const ownership = await ensureFurniOwnership();
|
||||||
|
|
||||||
logAudit({
|
logAudit({
|
||||||
userId: ctx.session.user.id,
|
userId: ctx.session.user.id,
|
||||||
action: "furni_import",
|
action: "furni_import",
|
||||||
@@ -489,6 +497,7 @@ export const POST = withAdmin(
|
|||||||
id: result.itemId,
|
id: result.itemId,
|
||||||
catalogItemId: result.catalogItemId,
|
catalogItemId: result.catalogItemId,
|
||||||
offerIdsFixed: reconciled.fixed,
|
offerIdsFixed: reconciled.fixed,
|
||||||
|
ownershipFixed: ownership.fixed,
|
||||||
spriteCount: result.spriteCount,
|
spriteCount: result.spriteCount,
|
||||||
warnings: result.warnings,
|
warnings: result.warnings,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export async function getFurnitureDataPath(): Promise<string> {
|
|||||||
return path.join(/*turbopackIgnore: true*/ process.cwd(), "public", relative);
|
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 primary = await getFurnitureDataPath();
|
||||||
const configuredMirror =
|
const configuredMirror =
|
||||||
(await siteSettings.get("furni_data_mirror_path", "")) ?? "";
|
(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 { existsSync, promises as fs } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { promisify } from "node:util";
|
||||||
import { and, eq, type SQL, sql } from "drizzle-orm";
|
import { and, eq, type SQL, sql } from "drizzle-orm";
|
||||||
import { CatalogPages, db, ItemsBase, LogsFurniImports } from "@/lib/db";
|
import { CatalogPages, db, ItemsBase, LogsFurniImports } from "@/lib/db";
|
||||||
import { officialHabboEnrichmentWarning } from "@/lib/habbo-gamedata-hotel";
|
import { officialHabboEnrichmentWarning } from "@/lib/habbo-gamedata-hotel";
|
||||||
@@ -15,6 +17,7 @@ import {
|
|||||||
appendFurniEntry,
|
appendFurniEntry,
|
||||||
buildFurniEntry,
|
buildFurniEntry,
|
||||||
findFurniDataIdConflict,
|
findFurniDataIdConflict,
|
||||||
|
getFurnitureDataWritePaths,
|
||||||
removeFurniEntry,
|
removeFurniEntry,
|
||||||
} from "@/lib/services/furni-data";
|
} from "@/lib/services/furni-data";
|
||||||
import {
|
import {
|
||||||
@@ -303,6 +306,46 @@ export async function reconcileImportedOfferIds(): Promise<{
|
|||||||
return { fixed: Number(result.affectedRows ?? 0) };
|
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
|
* Allocate the next `catalog_items.id` and run the INSERT through a
|
||||||
* process-level async mutex so concurrent imports in the same Node process
|
* process-level async mutex so concurrent imports in the same Node process
|
||||||
|
|||||||
Reference in new issue
Block a user