Files
overlord/Overlord-Server/src/server/routes/assets-routes.ts
T

180 lines
5.7 KiB
TypeScript
Raw Normal View History

2026-03-05 00:04:26 -06:00
import path from "path";
2026-03-25 09:50:41 -05:00
import { getConfig } from "../../config";
2026-03-05 00:04:26 -06:00
type AssetsRouteDeps = {
PUBLIC_ROOT: string;
secureHeaders: (contentType?: string) => Record<string, string>;
mimeType: (path: string) => string;
};
2026-04-08 14:37:03 -05:00
const COMPRESSIBLE_TYPES = new Set(["text/html", "text/css", "text/javascript", "application/json", "image/svg+xml"]);
function isCompressible(contentType: string): boolean {
for (const t of COMPRESSIBLE_TYPES) {
if (contentType.startsWith(t)) return true;
}
return false;
}
function acceptsGzip(req: Request): boolean {
return (req.headers.get("accept-encoding") ?? "").includes("gzip");
}
2026-05-30 22:44:55 -05:00
const STATIC_ASSET_CACHE = "public, max-age=31536000, immutable";
const MUTABLE_ASSET_CACHE = "public, max-age=86400, stale-while-revalidate=604800";
2026-04-08 14:37:03 -05:00
const NO_CACHE = "no-cache";
2026-06-22 19:21:06 -05:00
const MAX_GZIP_CACHE_ENTRIES = 128;
const MAX_GZIP_CACHE_BYTES = 16 * 1024 * 1024;
type GzipCacheEntry = {
bytes: ArrayBuffer;
size: number;
};
const gzipAssetCache = new Map<string, GzipCacheEntry>();
let gzipAssetCacheBytes = 0;
2026-04-08 14:37:03 -05:00
function assetCacheControl(relativePath: string): string {
if (relativePath === "custom.css") return NO_CACHE;
if (relativePath === "notification-sw.js") return NO_CACHE;
if (relativePath.endsWith(".min.js") || relativePath.endsWith(".min.css")) {
return STATIC_ASSET_CACHE;
}
2026-04-18 01:00:14 -05:00
if (/\.(ico|png|jpg|jpeg|gif|webp|woff2?|ttf|eot|svg)$/.test(relativePath)) {
2026-04-08 14:37:03 -05:00
return STATIC_ASSET_CACHE;
}
return MUTABLE_ASSET_CACHE;
}
2026-06-22 19:21:06 -05:00
function toExactArrayBuffer(bytes: Uint8Array): ArrayBuffer {
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
}
function rememberCompressedAsset(cacheKey: string, bytes: Uint8Array): ArrayBuffer {
const existing = gzipAssetCache.get(cacheKey);
if (existing) {
gzipAssetCacheBytes -= existing.size;
}
const cachedBytes = toExactArrayBuffer(bytes);
gzipAssetCache.set(cacheKey, { bytes: cachedBytes, size: bytes.byteLength });
gzipAssetCacheBytes += bytes.byteLength;
while (
gzipAssetCache.size > MAX_GZIP_CACHE_ENTRIES ||
gzipAssetCacheBytes > MAX_GZIP_CACHE_BYTES
) {
const oldestKey = gzipAssetCache.keys().next().value;
if (!oldestKey) break;
const oldest = gzipAssetCache.get(oldestKey);
if (oldest) gzipAssetCacheBytes -= oldest.size;
gzipAssetCache.delete(oldestKey);
}
return cachedBytes;
}
2026-04-08 14:37:03 -05:00
async function compressedResponse(
2026-06-22 19:21:06 -05:00
cacheKey: string,
file: ReturnType<typeof Bun.file>,
2026-04-08 14:37:03 -05:00
headers: Record<string, string>,
): Promise<Response> {
2026-06-22 19:21:06 -05:00
const cached = gzipAssetCache.get(cacheKey);
if (cached) {
gzipAssetCache.delete(cacheKey);
gzipAssetCache.set(cacheKey, cached);
return new Response(cached.bytes, {
headers: { ...headers, "Content-Encoding": "gzip", "Vary": "Accept-Encoding" },
});
2026-04-08 14:37:03 -05:00
}
2026-06-22 19:21:06 -05:00
const body = await file.arrayBuffer();
if (body.byteLength < 1024) {
return new Response(body, { headers });
}
const compressed = Bun.gzipSync(new Uint8Array(body));
const compressedBody = rememberCompressedAsset(cacheKey, compressed);
return new Response(compressedBody, {
2026-04-08 14:37:03 -05:00
headers: { ...headers, "Content-Encoding": "gzip", "Vary": "Accept-Encoding" },
});
}
2026-03-05 00:04:26 -06:00
export async function handleAssetsRoutes(
req: Request,
url: URL,
deps: AssetsRouteDeps,
): Promise<Response | null> {
2026-03-25 09:50:41 -05:00
if (req.method === "GET" && url.pathname === "/assets/custom.css") {
const css = getConfig().appearance?.customCSS || "";
return new Response(css, {
headers: {
"Content-Type": "text/css; charset=utf-8",
2026-04-08 14:37:03 -05:00
"Cache-Control": NO_CACHE,
2026-03-25 09:50:41 -05:00
},
});
}
2026-03-05 15:25:52 -06:00
if (req.method === "GET" && url.pathname === "/favicon.ico") {
const file = Bun.file(path.join(deps.PUBLIC_ROOT, "assets", "favicon.ico"));
if (await file.exists()) {
2026-04-08 14:37:03 -05:00
const headers = { ...deps.secureHeaders("image/x-icon"), "Cache-Control": STATIC_ASSET_CACHE };
return new Response(file, { headers });
2026-03-05 15:25:52 -06:00
}
return new Response("Not found", { status: 404 });
}
const isAssets = req.method === "GET" && url.pathname.startsWith("/assets/");
const isVendor = req.method === "GET" && url.pathname.startsWith("/vendor/");
if (!isAssets && !isVendor) {
2026-03-05 00:04:26 -06:00
return null;
}
let decodedPath = url.pathname;
try {
decodedPath = decodeURIComponent(url.pathname);
} catch {
return new Response("Bad request", { status: 400 });
}
if (decodedPath.includes("\u0000")) {
return new Response("Not found", { status: 404 });
}
const subdir = isVendor ? "vendor" : "assets";
const prefix = isVendor ? /^\/vendor\// : /^\/assets\//;
const assetsRoot = path.join(deps.PUBLIC_ROOT, subdir);
const relativePath = decodedPath.replace(prefix, "");
2026-03-05 00:04:26 -06:00
const resolvedPath = path.resolve(assetsRoot, relativePath);
const rootWithSep = assetsRoot.endsWith(path.sep) ? assetsRoot : `${assetsRoot}${path.sep}`;
if (!resolvedPath.startsWith(rootWithSep)) {
return new Response("Not found", { status: 404 });
}
const file = Bun.file(resolvedPath);
if (await file.exists()) {
2026-04-08 14:37:03 -05:00
const contentType = deps.mimeType(url.pathname);
const headers: Record<string, string> = {
...deps.secureHeaders(contentType),
"Cache-Control": assetCacheControl(relativePath),
};
2026-03-22 00:36:02 -05:00
if (relativePath === "notification-sw.js") {
headers["Service-Worker-Allowed"] = "/";
}
2026-04-08 14:37:03 -05:00
const stat = file;
const etag = `"${stat.size.toString(36)}-${stat.lastModified.toString(36)}"`;
headers["ETag"] = etag;
if (req.headers.get("if-none-match") === etag) {
return new Response(null, { status: 304, headers });
}
if (isCompressible(contentType) && acceptsGzip(req)) {
2026-06-22 19:21:06 -05:00
return compressedResponse(`${resolvedPath}:${etag}`, file, headers);
2026-04-08 14:37:03 -05:00
}
2026-03-22 00:36:02 -05:00
return new Response(file, { headers });
2026-03-05 00:04:26 -06:00
}
return new Response("Not found", { status: 404 });
}