perf(studio): cache nitro scan, stream progress, virtualize cleanup list
This commit is contained in:
1 parent
c916e42572
commit
ba81d16f00
5 files changed
+727
-257
No files matched your search
@@ -4,31 +4,115 @@ import { PERMS } from "@/lib/permissions";
|
||||
import { logAudit } from "@/lib/services/audit";
|
||||
import { clearFurniImportCache } from "@/lib/services/furni-import-cache";
|
||||
import {
|
||||
ageDays,
|
||||
type CleanupAssetKind,
|
||||
type CleanupProgress,
|
||||
deleteNitroCleanupFiles,
|
||||
scanFakeBrokenNitros,
|
||||
} from "@/lib/services/nitro-cleanup";
|
||||
|
||||
const VALID_KINDS: readonly CleanupAssetKind[] = ["nitro", "swf", "icon"];
|
||||
|
||||
function intParam(value: string | null): number | null {
|
||||
if (value === null) return null;
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan all asset directories for fake (orphaned) nitro / swf / icon files and
|
||||
* broken (unparseable) .nitro bundles.
|
||||
*
|
||||
* Query params:
|
||||
* - `force=1` skips the on-disk scan cache (fresh re-validation);
|
||||
* - `minAgeDays=N` only returns entries older than N days;
|
||||
* - `limit=N` caps each returned list (use `full=1` to override);
|
||||
* - `full=1` never applies the limit (used by the manifest export).
|
||||
*
|
||||
* When the client asks for `text/event-stream` the scan is streamed as
|
||||
* `progress` / `result` / `error` events, keeping the connection alive while a
|
||||
* large nitro directory is walked so reverse proxies don't time the request out.
|
||||
*/
|
||||
export const GET = withAdmin({ permission: PERMS.ASSETS_IMPORT }, async () => {
|
||||
try {
|
||||
const scan = await scanFakeBrokenNitros();
|
||||
return apiOk({
|
||||
fake: scan.fake,
|
||||
broken: scan.broken,
|
||||
orphanedSwf: scan.orphanedSwf,
|
||||
orphanedIcon: scan.orphanedIcon,
|
||||
total: scan.total,
|
||||
});
|
||||
} catch (err) {
|
||||
return apiError(`Asset scan failed: ${(err as Error).message}`, 500);
|
||||
}
|
||||
});
|
||||
export const GET = withAdmin(
|
||||
{ permission: PERMS.ASSETS_IMPORT },
|
||||
async (request) => {
|
||||
const wantsSse = request.headers
|
||||
.get("accept")
|
||||
?.includes("text/event-stream");
|
||||
const force = request.nextUrl.searchParams.get("force") === "1";
|
||||
const full = request.nextUrl.searchParams.get("full") === "1";
|
||||
const minAgeDays = intParam(request.nextUrl.searchParams.get("minAgeDays"));
|
||||
const limit = intParam(request.nextUrl.searchParams.get("limit"));
|
||||
|
||||
const run = async (onProgress?: (p: CleanupProgress) => void) => {
|
||||
const scan = await scanFakeBrokenNitros({ force, onProgress });
|
||||
const ageFilter = <T extends { lastModified: number }>(
|
||||
entries: T[],
|
||||
): T[] =>
|
||||
minAgeDays !== null && minAgeDays > 0
|
||||
? entries.filter((entry) => ageDays(entry.lastModified) >= minAgeDays)
|
||||
: entries;
|
||||
const cap = <T>(entries: T[]): T[] =>
|
||||
full || limit === null || limit <= 0
|
||||
? entries
|
||||
: entries.slice(0, limit);
|
||||
return {
|
||||
fake: cap(ageFilter(scan.fake)),
|
||||
broken: cap(ageFilter(scan.broken)),
|
||||
orphanedSwf: cap(ageFilter(scan.orphanedSwf)),
|
||||
orphanedIcon: cap(ageFilter(scan.orphanedIcon)),
|
||||
total: scan.total,
|
||||
cached: scan.cached ?? false,
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
if (!wantsSse) {
|
||||
return apiOk(await run());
|
||||
}
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const send = (data: unknown) => {
|
||||
try {
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify(data)}\n\n`),
|
||||
);
|
||||
} catch {
|
||||
/* stream closed by client */
|
||||
}
|
||||
};
|
||||
try {
|
||||
const result = await run((progress) =>
|
||||
send({ type: "progress", ...progress }),
|
||||
);
|
||||
send({ type: "result", ...result });
|
||||
} catch (err) {
|
||||
send({
|
||||
type: "error",
|
||||
message: `Asset scan failed: ${(err as Error).message}`,
|
||||
});
|
||||
}
|
||||
try {
|
||||
controller.close();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
return apiError(`Asset scan failed: ${(err as Error).message}`, 500);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete the given asset files (default .nitro) from every matching directory
|
||||
|
||||
Reference in new issue
Block a user