import { createReadStream } from 'node:fs';
import { stat } from 'node:fs/promises';
import { extname, join, normalize, sep } from 'node:path';
const TYPES = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.css': 'text/css; 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',
'.woff2': 'font/woff2',
'.mp4': 'video/mp4',
'.webm': 'video/webm',
'.mov': 'video/quicktime',
'.m4v': 'video/x-m4v',
'.ogg': 'video/ogg',
'.mp3': 'audio/mpeg',
};
export const contentType = (path) => TYPES[extname(path).toLowerCase()] || 'application/octet-stream';
/** Resolve a URL path inside `root`, refusing anything that escapes it. */
export function safeJoin(root, urlPath) {
const decoded = decodeURIComponent(urlPath).replace(/\0/g, '');
const full = normalize(join(root, decoded));
if (full !== root && !full.startsWith(root + sep)) return null;
return full;
}
/**
* Send a file, honouring a single Range header so <video> can seek.
* Returns false when the file does not exist.
*/
export async function sendFile(req, res, path, { cacheControl = 'public, max-age=3600' } = {}) {
let info;
try {
info = await stat(path);
if (!info.isFile()) return false;
} catch {
return false;
}
const type = contentType(path);
const etag = `W/"${info.size.toString(16)}-${Math.floor(info.mtimeMs).toString(16)}"`;
const base = {
'Content-Type': type,
'Accept-Ranges': 'bytes',
'Cache-Control': cacheControl,
ETag: etag,
'Last-Modified': info.mtime.toUTCString(),
};
if (req.headers['if-none-match'] === etag) {
res.writeHead(304, base);
res.end();
return true;
}
const range = /^bytes=(\d*)-(\d*)$/.exec(req.headers.range || '');
if (range) {
const [, rawStart, rawEnd] = range;
let start = rawStart === '' ? info.size - Number(rawEnd) : Number(rawStart);
let end = rawStart === '' || rawEnd === '' ? info.size - 1 : Number(rawEnd);
start = Math.max(0, start);
end = Math.min(info.size - 1, end);
if (!Number.isFinite(start) || !Number.isFinite(end) || start > end || start >= info.size) {
res.writeHead(416, { ...base, 'Content-Range': `bytes */${info.size}` });
res.end();
return true;
}
res.writeHead(206, {
...base,
'Content-Range': `bytes ${start}-${end}/${info.size}`,
'Content-Length': end - start + 1,
});
if (req.method === 'HEAD') return res.end(), true;
createReadStream(path, { start, end }).pipe(res);
return true;
}
res.writeHead(200, { ...base, 'Content-Length': info.size });
if (req.method === 'HEAD') return res.end(), true;
createReadStream(path).pipe(res);
return true;
}