parent
e9378fc113
commit
827c91fefa
@ -0,0 +1 @@
|
||||
export * from "https://deno.land/std@0.149.0/media_types/mod.ts";
|
@ -0,0 +1 @@
|
||||
export * from "https://deno.land/std@0.149.0/path/mod.ts";
|
@ -0,0 +1,175 @@
|
||||
import { contentType } from "./deps/mediaTypes.ts";
|
||||
import { extname } from "./deps/path.ts";
|
||||
|
||||
export type Promise_<T> = T | Promise<T>;
|
||||
export type MaybeHTTPHandler = (req: Request) => Promise_<Response | null>;
|
||||
export type HTTPHandler = (req: Request) => Promise_<Response>;
|
||||
export type HTTPHandlerWith<T> = (req: Request, t: T) => Promise_<Response>;
|
||||
|
||||
export interface CompressOptions {
|
||||
sizeThreshold: number,
|
||||
preferredAlgorithms: ("gzip" | "deflate")[],
|
||||
ignoredMimetypes: ((s: string) => boolean)[],
|
||||
}
|
||||
export const compressResponse = (handler: HTTPHandler, options?: Partial<CompressOptions>) => {
|
||||
const o = {
|
||||
sizeThreshold: 512,
|
||||
preferredAlgorithms: ["gzip", "deflate"] as CompressOptions["preferredAlgorithms"],
|
||||
ignoredMimetypes: [(s) => s.includes("font")],
|
||||
...options,
|
||||
} as CompressOptions;
|
||||
|
||||
return (async (req) => {
|
||||
const supportedEncodings = (() => {
|
||||
const accepted = req.headers.get("accept-encoding");
|
||||
if (!accepted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const encodings = accepted.split(',').map(s => s.trim().split(";")[0]);
|
||||
|
||||
return encodings;
|
||||
})();
|
||||
const encoding = (() => {
|
||||
if (!supportedEncodings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const encoding = o.preferredAlgorithms.find(a => supportedEncodings.includes(a));
|
||||
|
||||
return encoding ? [encoding, new CompressionStream(encoding)] as [string, CompressionStream] : null;
|
||||
})();
|
||||
|
||||
const res = await handler(req);
|
||||
|
||||
const contentType = res.headers.get("content-type");
|
||||
const body = res.body;
|
||||
if (contentType && body && encoding) {
|
||||
for (const p of o.ignoredMimetypes) {
|
||||
if (p(contentType)) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
const contentLength = res.headers.get("content-length");
|
||||
if (contentLength && Number(contentLength) < o.sizeThreshold) {
|
||||
return res;
|
||||
}
|
||||
|
||||
const [bestEncoding, bestEncoder] = encoding;
|
||||
res.headers.set("content-encoding", bestEncoding);
|
||||
|
||||
const body_ = res.body.pipeThrough(bestEncoder);
|
||||
const res_ = new Response(body_, {
|
||||
headers: res.headers,
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
});
|
||||
return res_;
|
||||
}
|
||||
|
||||
return res;
|
||||
}) as HTTPHandler;
|
||||
};
|
||||
|
||||
export const setHeaders = (headers: [string, string][], handler: HTTPHandler) => {
|
||||
return (async (req) => {
|
||||
const res = await handler(req);
|
||||
for (const [header, value] of headers) {
|
||||
res.headers.set(header, value);
|
||||
}
|
||||
|
||||
return res;
|
||||
}) as HTTPHandler;
|
||||
};
|
||||
|
||||
export const orElse = (mh: MaybeHTTPHandler, h: HTTPHandler) => {
|
||||
return (async (req) => {
|
||||
const mhr = await mh(req);
|
||||
if (mhr) {
|
||||
return mhr;
|
||||
}
|
||||
|
||||
return h(req);
|
||||
}) as HTTPHandler;
|
||||
};
|
||||
|
||||
export const filter = (pred: (r: Request) => Promise_<boolean>, h: HTTPHandler) => {
|
||||
return (async (req) => {
|
||||
if (!await pred(req)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return h(req);
|
||||
}) as MaybeHTTPHandler;
|
||||
};
|
||||
|
||||
export const catching = (h: HTTPHandler, fallback: HTTPHandlerWith<unknown>) => {
|
||||
return ((req) => {
|
||||
try {
|
||||
return h(req);
|
||||
} catch (e) {
|
||||
return fallback(req, e);
|
||||
}
|
||||
}) as HTTPHandler;
|
||||
};
|
||||
|
||||
export const log = (h: HTTPHandler) => {
|
||||
return (async (req) => {
|
||||
const start = performance.now();
|
||||
const res = await h(req);
|
||||
|
||||
const diff = performance.now() - start;
|
||||
|
||||
console.log(`${req.method} ${req.url} ${res.status} [${diff}ms]`);
|
||||
|
||||
return res;
|
||||
}) as HTTPHandler;
|
||||
}
|
||||
|
||||
export const serveDir = (dir: string, notFoundFallback: HTTPHandler) => {
|
||||
return (async (req) => {
|
||||
const url = new URL(req.url);
|
||||
const path = url.pathname.slice(1);
|
||||
if (path.includes("..")) {
|
||||
return notFoundFallback(req);
|
||||
}
|
||||
|
||||
let joined = `${dir}/${path}`;
|
||||
if (joined.endsWith("/")) {
|
||||
joined = `${joined}index.html`;
|
||||
}
|
||||
|
||||
try {
|
||||
const st = await Deno.stat(joined);
|
||||
const ifModifiedSince = req.headers.get("if-modified-since");
|
||||
const lm = new Date(st.mtime ?? 0);
|
||||
lm.setMilliseconds(0);
|
||||
if (ifModifiedSince) {
|
||||
const imsTime = new Date(ifModifiedSince).getTime();
|
||||
const lmTime = lm.getTime();
|
||||
|
||||
if (imsTime >= lmTime) {
|
||||
return new Response(null, { status: 304 });
|
||||
}
|
||||
}
|
||||
|
||||
const f = await Deno.open(joined);
|
||||
const ext = extname(joined);
|
||||
|
||||
return new Response(f.readable, {
|
||||
headers: {
|
||||
"content-size": st.size.toString(),
|
||||
"content-type": contentType(ext) as string,
|
||||
"last-modified": lm.toUTCString(),
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
if (e === Deno.errors.NotFound) {
|
||||
return notFoundFallback(req);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}) as HTTPHandler;
|
||||
}
|
Loading…
Reference in New Issue