Implements basic file based router
This commit is contained in:
363
file_router.ts
Executable file
363
file_router.ts
Executable file
@@ -0,0 +1,363 @@
|
||||
import Router from "./router.ts";
|
||||
import type { Handler, RouteConfigurator } from "./types.ts";
|
||||
import { NotFound } from "./util/response.ts";
|
||||
|
||||
function crawl(dir: string, callback: (path: string) => void) {
|
||||
for (const entry of Deno.readDirSync(dir)) {
|
||||
const path = Deno.build.os === "windows"
|
||||
? `${dir}\\${entry.name}`
|
||||
: `${dir}/${entry.name}`;
|
||||
if (entry.isDirectory) {
|
||||
crawl(path, callback);
|
||||
} else {
|
||||
callback(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class FileRouter extends Router {
|
||||
constructor(root: string) {
|
||||
super();
|
||||
crawl(root, async (path) => {
|
||||
let relativePath = path.replace(root, "");
|
||||
if (path.endsWith(".ts") || path.endsWith(".js")) {
|
||||
relativePath = relativePath.replace(/\.[tj]s/, "");
|
||||
const asdf = await import(path);
|
||||
|
||||
if (asdf.default) {
|
||||
asdf.default instanceof Router
|
||||
? this.use(relativePath, asdf.default)
|
||||
: this.route(relativePath).get(asdf.default);
|
||||
}
|
||||
|
||||
if (asdf.handlers) {
|
||||
for (const [method, handler] of Object.entries(asdf.handlers)) {
|
||||
this.route(relativePath)[method as keyof RouteConfigurator](
|
||||
handler as Handler,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (path.endsWith(".htm") || path.endsWith(".html")) {
|
||||
const headers = new Headers();
|
||||
headers.set("Content-Type", "text/html");
|
||||
this.route(relativePath).get((_ctx) => {
|
||||
return new Response(Deno.readTextFileSync(path), { headers });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
serveDirectory(dir: string, root: string, opts?: { showIndex: boolean }) {
|
||||
this.route(root + "*").get(async (_req, ctx) => {
|
||||
const { showIndex } = opts ?? { showIndex: false };
|
||||
|
||||
let normalizedPath = (dir + "/" +
|
||||
ctx.url.pathname.replace(new RegExp("^" + root), "")).trim().replace(
|
||||
"//",
|
||||
"/",
|
||||
);
|
||||
|
||||
normalizedPath = normalizedPath.replace(
|
||||
/\/\s?$/,
|
||||
"",
|
||||
);
|
||||
|
||||
let fileInfo: Deno.FileInfo;
|
||||
|
||||
try {
|
||||
fileInfo = await Deno.stat(normalizedPath);
|
||||
} catch (error) {
|
||||
if (error instanceof Deno.errors.NotFound) {
|
||||
return NotFound();
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (fileInfo.isDirectory) {
|
||||
if (!showIndex) return NotFound();
|
||||
normalizedPath += "/index.html";
|
||||
}
|
||||
|
||||
try {
|
||||
const file = await Deno.readFile(normalizedPath);
|
||||
return new Response(file);
|
||||
} catch (e) {
|
||||
if (e instanceof Deno.errors.NotFound) {
|
||||
return showIndex ? generateIndex(normalizedPath) : NotFound();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function generateIndex(dir: string) {
|
||||
dir = dir.replace(/index\.html$/, "");
|
||||
const items: Deno.DirEntry[] = [];
|
||||
|
||||
for await (const entry of Deno.readDir(dir)) {
|
||||
items.push(entry);
|
||||
}
|
||||
|
||||
const fileIcon = `
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 12.7 12.7"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||
sodipodi:docname="file icon.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="17.318182"
|
||||
inkscape:cx="10.018373"
|
||||
inkscape:cy="28.207349"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:window-height="1417"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="1432"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
d="m 3.5196818,1.7373617 c -0.3351528,0 -0.605131,0.2694614 -0.605131,0.6046142 v 8.0160481 c 0,0.335153 0.2699782,0.604614 0.605131,0.604614 h 5.6606364 c 0.3351528,0 0.605131,-0.269461 0.605131,-0.604614 V 3.7372396 L 7.7850545,1.7373617 Z"
|
||||
style="fill:#4d1f6d;stroke-width:0.264583"
|
||||
id="path4" />
|
||||
<path
|
||||
d="M 9.6529679,3.8697209 7.6525732,1.869843 v 1.3952637 c 0,0.3351528 0.2699782,0.6046142 0.605131,0.6046142 z"
|
||||
style="fill:#918b93;fill-opacity:1;stroke:#4d1f6d;stroke-width:0.265;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="path5" />
|
||||
<rect
|
||||
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.285269;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect5"
|
||||
width="5.5833006"
|
||||
height="0.5505569"
|
||||
x="3.5583498"
|
||||
y="4.0022206" />
|
||||
<rect
|
||||
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.272493;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect6"
|
||||
width="5.0944114"
|
||||
height="0.5505569"
|
||||
x="3.5583498"
|
||||
y="4.8455539" />
|
||||
<rect
|
||||
style="fill:#463e4b;fill-opacity:1;stroke:none;stroke-width:0.279751;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect7"
|
||||
width="5.3694115"
|
||||
height="0.5505569"
|
||||
x="3.5583498"
|
||||
y="5.682776" />
|
||||
<rect
|
||||
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.279751;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect8"
|
||||
width="5.3694115"
|
||||
height="0.5505569"
|
||||
x="3.5583498"
|
||||
y="5.6888871" />
|
||||
<rect
|
||||
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.285269;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect9"
|
||||
width="5.5833006"
|
||||
height="0.5505569"
|
||||
x="3.5583498"
|
||||
y="6.5322208" />
|
||||
<rect
|
||||
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.272493;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect10"
|
||||
width="5.0944114"
|
||||
height="0.5505569"
|
||||
x="3.5583498"
|
||||
y="7.3755541" />
|
||||
<rect
|
||||
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.279751;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect11"
|
||||
width="5.3694115"
|
||||
height="0.5505569"
|
||||
x="3.5583498"
|
||||
y="8.2188873" />
|
||||
<rect
|
||||
style="fill:#918b93;fill-opacity:1;stroke:none;stroke-width:0.217416;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
|
||||
id="rect12"
|
||||
width="3.2431545"
|
||||
height="0.5505569"
|
||||
x="5.8984962"
|
||||
y="9.7313871" />
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
const folderIcon = `
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 12.7 12.7"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||
sodipodi:docname="folder icon.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="12.245804"
|
||||
inkscape:cx="17.924507"
|
||||
inkscape:cy="30.990207"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:window-height="1417"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="1432"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="fill:#463e4b;fill-opacity:1;stroke-width:0.264583"
|
||||
id="rect3"
|
||||
width="10.867838"
|
||||
height="7.4972959"
|
||||
x="0.91608107"
|
||||
y="2.1476252"
|
||||
ry="1.1883322" />
|
||||
<rect
|
||||
style="fill:#4d1f6d;fill-opacity:1;stroke-width:0.264583"
|
||||
id="rect1"
|
||||
width="10.867838"
|
||||
height="7.4972959"
|
||||
x="0.91608107"
|
||||
y="2.601352"
|
||||
ry="1.1883322" />
|
||||
<rect
|
||||
style="fill:#4d1f6d;fill-opacity:1;stroke-width:0.264583"
|
||||
id="rect2"
|
||||
width="4.2779961"
|
||||
height="4.0619354"
|
||||
x="0.91608107"
|
||||
y="1.5426561"
|
||||
ry="1.1883322" />
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
const template = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${dir}</title>
|
||||
<style>
|
||||
* {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 4px 2px;
|
||||
|
||||
&:nth-child(odd) {
|
||||
background-color: rgba(0,0,0, .2)
|
||||
}
|
||||
|
||||
& svg {
|
||||
height: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
background-color: #1b1220;
|
||||
color: #dcdcdc
|
||||
}
|
||||
ul {
|
||||
background-color: rgba(255,255,255,.1)
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${dir}</h1>
|
||||
<ul>
|
||||
${
|
||||
items
|
||||
.sort((a, b) => {
|
||||
if (a.isDirectory && b.isDirectory) return a.name > b.name ? 1 : -1;
|
||||
|
||||
if (a.isDirectory) return -1;
|
||||
|
||||
if (b.isDirectory) return 1;
|
||||
|
||||
return a.name > b.name ? 1 : -1;
|
||||
})
|
||||
.map((e) =>
|
||||
`<li>${e.isFile ? fileIcon : folderIcon}<a href="${
|
||||
"/" + dir.replace(/^\//, "") + e.name
|
||||
}">${e.name}</a></li>`
|
||||
)
|
||||
.join("\n") || "<li>Directory is empty</li>"
|
||||
}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
return new Response(template, {
|
||||
headers: {
|
||||
"Content-Type": "text/html",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const router = new FileRouter("fileRouterTest");
|
||||
router.serveDirectory("things", "/things", { showIndex: true });
|
||||
Deno.serve({
|
||||
port: 8000,
|
||||
handler: router.handle,
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user