import type { NextFn } from "../deps.ts";import type { Request } from "../structure/Request.ts";import type { Response } from "../structure/Response.ts";import { resolve, join, contentType, extname, Status } from "../deps.ts";
export type DirectoryListingFn = (entries: ({ size?: number, name: string })[]) => [string, string] | Promise<[string, string]>;export type Handler = (pathname: string, req: Request, res: Response, thisArg: Static) => [string, string] | Promise<[string, string] | undefined> | undefined;export type Handlers = { [key: string]: Handler };export type HandlersMap = Map<string, Handler>;
export class Static {
public static readonly startsWithDot = /^\./gi; public static async findFile(indexes: Set<string>, vanityExtensions: Set<string>, path: string, throwOnPermissionDenied: boolean = true): Promise<string | void> { try { const info = await Deno.lstat(path); if (info.isFile) return path; for (let index of indexes) { const p = await Static.findFile(indexes, vanityExtensions, join(path, index), false); if (p) return p; } } catch (error) { if (throwOnPermissionDenied && error instanceof Deno.errors.PermissionDenied) throw error; if (error instanceof Deno.errors.NotFound) { if (extname(path) === "") { for (let ext of vanityExtensions) { const file = await Static.findFile(indexes, vanityExtensions, path + (Static.startsWithDot.test(ext) ? "" : ".") + ext); if (file) return file; } } } else throw error; } }
public static async fallback(file: string): Promise<[string, string]> { return [ await Deno.readTextFile(file), contentType(extname(file)) || "text/plain" ]; }
public static async getEntries(dir: string): Promise<{ size?: number, name: string }[]> { const entries: { size?: number, name: string }[] = []; for await (let { isDirectory, isFile, name } of Deno.readDir(dir)) { if (isDirectory) entries.push({ name }); if (isFile) { const { size } = await Deno.lstat(join(dir, name)); entries.push({ name, size }); } } return entries; }
public readonly dir: string;
public indexes: Set<string> = new Set<string>().add("index.html").add("default.html");
public directoryListing?: DirectoryListingFn;
public readonly handlers: HandlersMap = new Map();
public readonly vanityExtensions: Set<string> = new Set();;
public constructor( dir: string, options?: { indexes?: string[] | Set<string> handlers?: Handlers | HandlersMap, vanityExtensions?: string[] | Set<string>, directoryListing?: DirectoryListingFn } ) { this.dir = resolve(Deno.cwd(), dir);
if (options) { if (options.indexes instanceof Set || Array.isArray(options.indexes)) for (let index of options.indexes) this.indexes.add(index); if (typeof options.directoryListing === "function") this.directoryListing = options.directoryListing; if (typeof options.handlers === "object" && options.handlers !== null) for (let [key, value] of options.handlers instanceof Map ? options.handlers : Object.entries(options.handlers)) this.handlers.set(key, value); if (options.vanityExtensions instanceof Set || Array.isArray(options.vanityExtensions)) for (let ext of options.vanityExtensions) this.vanityExtensions.add(ext); } }
public async run(req: Request, res: Response, next: NextFn) { const _path = join(this.dir, resolve("/", req.url.pathname)); try { const file = await Static.findFile(this.indexes, this.vanityExtensions, _path); if (!file && this.directoryListing) { const entries = await Static.getEntries(_path); const [content, type] = await this.directoryListing(entries); if (content) { await res.status(Status.OK).set("content-type", type || "text/plain").end(content); } } else if (file) { let extension = extname(file); extension = extension.substring(1, extension.length); let handler = Static.fallback as Handler; if (this.handlers.has(extension)) handler = this.handlers.get(extension)!; const [content, type] = (await handler(file, req, res, this)) || []; if (res.WRITABLE && content) await res.status(Status.OK).set("content-type", type || "text/plain").end(content); } } catch (error) { if (error instanceof Deno.errors.PermissionDenied) await res.status(Status.Forbidden).end(); else if (error instanceof Deno.errors.NotFound) { } else throw error; } }
}