import { createReadStream } from 'node:fs' import { readFile, stat } from 'node:fs/promises' import type { ServerResponse } from 'node:http' import { extname, resolve } from 'node:path' import { HttpError, sendError } from '../shared/http.ts' const CONTENT_TYPES: Record = { '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.ico': 'image/x-icon', '.txt': 'text/plain; charset=utf-8', '.woff2': 'font/woff2', } /** Serve a file under root with path-traversal protection. */ export async function serveFile( res: ServerResponse, root: string, unsafePath: string, options: { immutable?: boolean } = {}, ): Promise { const rootResolved = resolve(root) const target = resolve(rootResolved, `./${unsafePath.replace(/^\/+/, '')}`) if (target !== rootResolved && !target.startsWith(`${rootResolved}/`)) { throw new HttpError(403, 'forbidden') } let info: import('node:fs').Stats try { info = await stat(target) } catch { throw new HttpError(404, `not found: ${unsafePath}`) } if (!info.isFile()) throw new HttpError(404, `not a file: ${unsafePath}`) const type = CONTENT_TYPES[extname(target)] ?? 'application/octet-stream' res.writeHead(200, { 'content-type': type, 'content-length': info.size, 'cache-control': options.immutable ? 'public, max-age=31536000, immutable' : 'no-cache', }) await new Promise((done) => { const stream = createReadStream(target) stream.on('error', () => { if (!res.headersSent) sendError(res, 500, 'read error') else res.end() done() }) stream.on('end', () => done()) stream.pipe(res) }) } export async function readJsonFile(path: string): Promise { const raw = await readFile(path, 'utf8') return JSON.parse(raw) }