55 lines
2.6 KiB
JavaScript
55 lines
2.6 KiB
JavaScript
import { createReadStream, readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { pipeline } from "node:stream";
|
|
import { createGunzip } from "node:zlib";
|
|
|
|
export function contentMimeType(logicalPath) {
|
|
return ({
|
|
".glb": "model/gltf-binary", ".json": "application/json", ".js": "text/javascript",
|
|
".wasm": "application/wasm", ".ktx2": "image/ktx2", ".png": "image/png",
|
|
".webp": "image/webp", ".avif": "image/avif", ".svg": "image/svg+xml",
|
|
".mp3": "audio/mpeg", ".ogg": "audio/ogg", ".wav": "audio/wav", ".webm": "video/webm",
|
|
})[path.extname(logicalPath)] ?? "application/octet-stream";
|
|
}
|
|
|
|
function acceptsGzip(value = "") {
|
|
const encodings = new Map(String(value).split(",").map((entry) => {
|
|
const [name, ...parameters] = entry.trim().toLowerCase().split(";");
|
|
const quality = parameters.find((part) => part.trim().startsWith("q="));
|
|
return [name, quality ? Number(quality.trim().slice(2)) : 1];
|
|
}));
|
|
return (encodings.get("gzip") ?? encodings.get("*") ?? 0) > 0;
|
|
}
|
|
|
|
/** Resolve logical URLs to one immutable bundled object without expanding it on disk. */
|
|
export function createBundledContentHandler(root) {
|
|
let manifest;
|
|
return (request, response, pathname) => {
|
|
if (!["GET", "HEAD"].includes(request.method) || !/^\/(assets|basis|data)\//.test(pathname)) return false;
|
|
if (manifest === undefined) {
|
|
try { manifest = JSON.parse(readFileSync(path.join(root, "content/bootstrap-manifest.json"), "utf8")); }
|
|
catch (error) { if (error.code !== "ENOENT") throw error; manifest = null; }
|
|
}
|
|
const entry = manifest?.files[pathname];
|
|
if (!entry) return false;
|
|
const object = entry.download ?? entry;
|
|
if (!/^\/content\/bundled\/[a-f0-9]{64}\.[a-z0-9]+(?:\.g(?:z|zip))?$/.test(object.url)) return false;
|
|
const encoded = entry.download?.encoding === "gzip";
|
|
const sendGzip = encoded && acceptsGzip(request.headers["accept-encoding"]);
|
|
response.writeHead(200, {
|
|
"Content-Type": contentMimeType(pathname),
|
|
"Content-Length": sendGzip ? object.bytes : entry.bytes,
|
|
"Cache-Control": "public, max-age=3600",
|
|
"X-Content-Type-Options": "nosniff",
|
|
Vary: "Accept-Encoding",
|
|
...(sendGzip ? { "Content-Encoding": "gzip" } : {}),
|
|
});
|
|
if (request.method === "HEAD") { response.end(); return true; }
|
|
const stream = createReadStream(path.join(root, object.url.slice(1)));
|
|
const done = (error) => { if (error) response.destroy(error); };
|
|
if (encoded && !sendGzip) pipeline(stream, createGunzip(), response, done);
|
|
else pipeline(stream, response, done);
|
|
return true;
|
|
};
|
|
}
|