import { Context } from "./context.ts";import { createHttpError } from "./httpError.ts";import { basename, extname, parse, sep } from "./deps.ts";import { decodeComponent, resolvePath } from "./util.ts";
export interface SendOptions { maxage?: number;
immutable?: boolean;
hidden?: boolean;
root: string;
index?: string;
gzip?: boolean;
brotli?: boolean;
format?: boolean;
extensions?: string[];}
function isHidden(root: string, path: string) { const pathArr = path.substr(root.length).split(sep); for (const segment of pathArr) { if (segment[0] === ".") { return true; } return false; }}
async function exists(path: string): Promise<boolean> { try { return (await Deno.stat(path)).isFile(); } catch { return false; }}
function toUTCString(value: number): string { return new Date(value).toUTCString();}
export async function send( { request, response }: Context, path: string, options: SendOptions = { root: "" }): Promise<string | undefined> { const { brotli = true, extensions, format = true, gzip = true, index, hidden = false, immutable = false, maxage = 0, root } = options; const trailingSlash = path[path.length - 1] === "/"; path = decodeComponent(path.substr(parse(path).root.length)); if (index && trailingSlash) { path += index; }
path = resolvePath(root, path);
if (!hidden && isHidden(root, path)) { return; }
let encodingExt = ""; if ( brotli && request.acceptsEncodings("br", "identity") === "br" && (await exists(`${path}.br`)) ) { path = `${path}.br`; response.headers.set("Content-Encoding", "br"); response.headers.delete("Content-Length"); encodingExt = ".br"; } else if ( gzip && request.acceptsEncodings("gzip", "identity") === "gzip" && (await exists(`${path}.gz`)) ) { path = `${path}.gz`; response.headers.set("Content-Encoding", "gzip"); response.headers.delete("Content-Length"); encodingExt = ".gz"; }
if (extensions && !/\.[^/]*$/.exec(path)) { for (let ext of extensions) { if (!/^\./.exec(ext)) { ext = `.${ext}`; } if (await exists(`${path}${ext}`)) { path += ext; break; } } }
let stats: Deno.FileInfo; try { stats = await Deno.stat(path);
if (stats.isDirectory()) { if (format && index) { path += `/${index}`; stats = await Deno.stat(path); } else { return; } } } catch (err) { if (err instanceof Deno.DenoError) { if (err.kind === Deno.ErrorKind.NotFound) { throw createHttpError(404, err.message); } } throw createHttpError(500, err.message); }
response.headers.set("Content-Length", String(stats.len)); if (!response.headers.has("Last-Modified") && stats.modified) { response.headers.set("Last-Modified", toUTCString(stats.modified)); } if (!response.headers.has("Cache-Control")) { const directives = [`max-age=${(maxage / 1000) | 0}`]; if (immutable) { directives.push("immutable"); } response.headers.set("Cache-Control", directives.join(",")); } if (!response.type) { response.type = encodingExt !== "" ? extname(basename(path, encodingExt)) : extname(path); } response.body = await Deno.readFile(path);
return path;}