import path from "node:path"; import fs, { Stats } from "node:fs"; import type { BunFile, Serve } from "bun"; import * as Sentry from "@sentry/node"; import prom from "bun-prometheus-client"; import readConfig from "./config"; Sentry.init({}); const base = "./website/"; const publicDir = path.resolve(base, "public") + path.sep; const config = readConfig(base); const defaultHeaders = { ...config.extra.headers, vary: "Accept-Encoding", }; type File = { filename: string; handle: BunFile; relPath: string; headers?: Record; size: number; mtime: Date; }; const collectDefaultMetrics = prom.collectDefaultMetrics; collectDefaultMetrics({ labels: { FLY_APP_NAME: Bun.env.FLY_APP_NAME, FLY_ALLOC_ID: Bun.env.FLY_ALLOC_ID, FLY_REGION: Bun.env.FLY_REGION, }, }); const metrics = { requestsByStatus: new prom.Counter({ name: "homestead_requests_by_status", help: "Number of requests by status code", labelNames: ["status_code"], }), requestsByPath: new prom.Counter({ name: "homestead_requests_by_path", help: "Number of requests by path", labelNames: ["path"], }), requestDuration: new prom.Histogram({ name: "homestead_request_duration_seconds", help: "Request duration in seconds", labelNames: ["path"], }), }; let files = new Map(); function registerFile( path: string, pathname: string, filename: string, stat: Stats, ): void { pathname = "/" + (pathname === "." || pathname === "./" ? "" : pathname); if (files.get(pathname) !== undefined) { console.warn("File already registered:", pathname); } files.set(pathname, { filename, relPath: "/" + path, handle: Bun.file(filename), headers: pathname === "/404.html" ? Object.assign({}, defaultHeaders, { "cache-control": "no-cache" }) : undefined, size: stat.size, mtime: stat.mtime, }); } function walkDirectory(root: string, dir: string) { const absDir = path.join(root, dir); for (let pathname of fs.readdirSync(absDir)) { const relPath = path.join(dir, pathname); const absPath = path.join(absDir, pathname); const stat = fs.statSync(absPath); if (stat.isDirectory()) { walkDirectory(root, relPath + path.sep); } else if (stat.isFile()) { if (pathname.startsWith("index.html")) { const dir = relPath.replace("index.html", ""); registerFile(relPath, dir, absPath, stat); if (dir !== ".") { registerFile(relPath, dir + path.sep, absPath, stat); } } registerFile(relPath, relPath, absPath, stat); } } } walkDirectory(publicDir, ""); async function serveFile( file: File | undefined, statusCode: number = 200, extraHeaders: Record = {}, ): Promise { if (file && (await file.handle.exists())) { metrics.requestsByStatus.inc({ status_code: statusCode }); return new Response(file.handle, { headers: { "last-modified": file.mtime.toUTCString(), ...extraHeaders, ...(file.headers || defaultHeaders), }, status: statusCode, }); } else { metrics.requestsByStatus.inc({ status_code: 404 }); // TODO return encoded return serveFile(files.get("/404.html"), 404); } } async function serveEncodedFile( file: File | undefined, statusCode: number = 200, extraHeaders: Record = {}, ): Promise { const res = await serveFile(file, statusCode, extraHeaders); res.headers.delete("content-disposition"); return res; } function parseIfModifiedSinceHeader(header: string | null): number { return header ? new Date(header).getTime() + 999 : 0; } const metricsServer = Bun.serve({ port: 9091, fetch: async function (request) { const pathname = new URL(request.url).pathname; switch (pathname) { case "/metrics": return new Response(await prom.register.metrics()); default: return new Response("", { status: 404 }); } }, }); console.info( `Serving metrics on http://${metricsServer.hostname}:${metricsServer.port}/metrics`, ); const server = Bun.serve({ fetch: async function (request) { const pathname = new URL(request.url).pathname; const endTimer = metrics.requestDuration.startTimer({ path: pathname }); try { const file = files.get(pathname); metrics.requestsByPath.inc({ path: pathname }); if (file) { if ( parseIfModifiedSinceHeader( request.headers.get("if-modified-since"), ) >= file?.mtime.getTime() ) { metrics.requestsByStatus.inc({ status_code: 304 }); return new Response("", { status: 304, headers: defaultHeaders }); } const encodings = (request.headers.get("accept-encoding") || "") .split(",") .map((x) => x.trim().toLowerCase()); if (encodings.includes("br") && files.has(file.relPath + ".br")) { return serveEncodedFile(files.get(file.relPath + ".br"), 200, { "content-encoding": "br", "content-type": file.handle.type, }); } else if ( encodings.includes("zstd") && files.has(file.relPath + ".zst") ) { return serveEncodedFile(files.get(file.relPath + ".zst"), 200, { "content-encoding": "zstd", "content-type": file.handle.type, }); } else if ( encodings.includes("gzip") && files.has(file.relPath + ".gz") ) { return serveEncodedFile(files.get(file.relPath + ".gz"), 200, { "content-encoding": "gzip", "content-type": file.handle.type, }); } } return serveFile(file); } catch (error) { metrics.requestsByStatus.inc({ status_code: 503 }); Sentry.captureException(error); return new Response("Something went wrong", { status: 503 }); } finally { const seconds = endTimer(); metrics.requestDuration.observe(seconds); } }, }); console.info(`Serving website on http://${server.hostname}:${server.port}/`);