clone

a personal forge on plain git: static pages, stacked-diff reviews, signed local ci, repo sites and releases

Honor range requests in the demo server

cat committed 63cbbf2
demo/serve.ts answers Range (206, 416, suffix ranges, If-Range) the way
Caddy does, and serves .wasm as application/wasm and .db as
application/vnd.sqlite3, so pages that read a file in byte ranges work
in local previews too.
commit
63cbbf2b44d09b477a1bd426142b2d086f487f1e
parent
6c5bce7410ac51d5675544a84d4bfdb321f2127d
author
cat <admin@dev.site>
kind
feat risk score 1.3 of 3, Jev's estimate
1 file changed, +32 -2
Mdemo/serve.ts34++++++++++++++++++++++++++++--
M demo/serve.ts +32 -2
@@ -1,6 +1,7 @@
11 // Local preview of the demo forge: static pages from demo/root/.forge/www22 // plus live events, the way Caddy and lib/events-server.ts serve them, with3-// the same cache headers (bin/forge server caddyfile).3+// the same cache headers (bin/forge server caddyfile) and, like Caddy, byte4+// ranges, which code search reads code.db with.45 // node demo/serve.ts [port]56 import http from 'node:http';67 import fs from 'node:fs';
@@ -18,8 +19,18 @@ serveEvents(state, eventsPort);
1819 const TYPES: Record<string, string> = {1920 '.html': 'text/html; charset=utf-8', '.css': 'text/css', '.js': 'text/javascript', '.svg': 'image/svg+xml',2021 '.png': 'image/png', '.gif': 'image/gif', '.txt': 'text/plain; charset=utf-8', '.xml': 'application/xml', '.json': 'application/json',22+ '.wasm': 'application/wasm', '.db': 'application/vnd.sqlite3',2123 };2224 25+// One byte range, as code search reads code.db: [start, end] inclusive, or26+// null for a malformed or unsatisfiable one. Several ranges get the whole file.27+function byteRange(h: string, size: number): [number, number] | null | undefined {28+ const m = /^bytes=(\d*)-(\d*)$/.exec(h.trim());29+ if (!m || (!m[1] && !m[2])) return h.includes(',') ? undefined : null;30+ const [start, end] = m[1] ? [Number(m[1]), m[2] ? Math.min(Number(m[2]), size - 1) : size - 1] : [Math.max(0, size - Number(m[2])), size - 1];31+ return start <= end && start < size ? [start, end] : null;32+}33+2334 // Hashed assets never change; pages and the indexes they fetch revalidate,2435 // and get a 304 when unchanged. Sites keep their own caching.2536 const HASHED = /^\/assets\/([^/]+\.)?[0-9A-Za-z_-]{10}\.[a-z]+$/;
@@ -50,7 +61,7 @@ http.createServer((req, res) => {
5061 let st = fs.statSync(file);5162 if (st.isDirectory()) st = fs.statSync((file = path.join(file, 'index.html')));5263 const etag = `"${Math.floor(st.mtimeMs).toString(36)}-${st.size.toString(36)}"`;53- const headers: http.OutgoingHttpHeaders = { 'Content-Type': TYPES[path.extname(file)] ?? 'application/octet-stream', ETag: etag, 'Last-Modified': st.mtime.toUTCString() };64+ const headers: http.OutgoingHttpHeaders = { 'Content-Type': TYPES[path.extname(file)] ?? 'application/octet-stream', ETag: etag, 'Last-Modified': st.mtime.toUTCString(), 'Accept-Ranges': 'bytes' };5465 const cc = cacheControl(url.pathname);5566 if (cc) headers['Cache-Control'] = cc;5667 const since = req.headers['if-modified-since'];
@@ -59,6 +70,25 @@ http.createServer((req, res) => {
5970 res.writeHead(304, headers).end();6071 return;6172 }73+ // A range of the file, unless If-Range names another version of it.74+ const ifRange = req.headers['if-range'];75+ const r = req.headers.range && (!ifRange || ifRange === etag || ifRange === headers['Last-Modified']) ? byteRange(req.headers.range, st.size) : undefined;76+ if (r === null) {77+ res.writeHead(416, { ...headers, 'Content-Range': `bytes */${st.size}` }).end();78+ return;79+ }80+ if (r) {81+ const [start, end] = r;82+ const buf = Buffer.alloc(end - start + 1);83+ const fd = fs.openSync(file, 'r');84+ try {85+ fs.readSync(fd, buf, 0, buf.length, start);86+ } finally {87+ fs.closeSync(fd);88+ }89+ res.writeHead(206, { ...headers, 'Content-Range': `bytes ${start}-${end}/${st.size}`, 'Content-Length': buf.length }).end(buf);90+ return;91+ }6292 res.writeHead(200, headers).end(fs.readFileSync(file));6393 } catch {6494 res.writeHead(404).end('not found');