@thi.ng/server
Version:
Minimal HTTP server with declarative routing, static file serving and freely extensible via pre/post interceptors
72 lines (71 loc) • 2.19 kB
JavaScript
import { isFunction, isString } from "@thi.ng/checks";
import { fileHash as $fileHash } from "@thi.ng/file-io";
import { preferredTypeForPath } from "@thi.ng/mime";
import { existsSync, statSync } from "node:fs";
import { join, resolve } from "node:path";
import { isUnmodified } from "./utils/cache.js";
const staticFiles = ({
prefix = "static",
rootDir = ".",
intercept = [],
filters = [pathFilterASCII],
compress = false,
auth = false,
etag,
headers
} = {}) => {
rootDir = resolve(rootDir);
const filter = (path) => filters.every((f) => f(path));
return {
id: "__static",
match: isString(prefix) ? prefix + "/+" : [...prefix, "+"],
auth,
handlers: {
get: {
fn: async (ctx) => {
const path = resolve(join(rootDir, ...ctx.match.rest));
const $headers = await __fileHeaders(
rootDir,
path,
ctx,
filter,
etag,
headers
);
if (!$headers) return;
if (ctx.origMethod === "head") {
ctx.res.writeHead(200, {
"content-type": preferredTypeForPath(path),
...$headers
});
return;
}
return ctx.server.sendFile(ctx, path, $headers, compress);
},
intercept
}
}
};
};
const __fileHeaders = async (rootDir, path, ctx, filter, etag, headers) => {
if (!(path.length > rootDir.length && filter(path) && existsSync(path))) {
return ctx.res.missing();
}
const $headers = isFunction(headers) ? headers(path) : headers;
if (etag) {
const etagValue = await etag(path);
return isUnmodified(etagValue, ctx.req.headers["if-none-match"]) ? ctx.res.unmodified() : { ...$headers, etag: etagValue };
}
return { ...$headers };
};
const etagFileTimeModified = (path) => statSync(path).mtimeMs.toString(36);
const etagFileHash = (algo = "md5") => (path) => $fileHash(path, void 0, algo);
const pathFilterASCII = (path) => !/[^\x20-\x7e]/.test(path);
const pathMaxLength = (limit) => (path) => path.length < limit;
export {
etagFileHash,
etagFileTimeModified,
pathFilterASCII,
pathMaxLength,
staticFiles
};