simple-fs-server/routes/dir/[...path].tsx

249 lines
6.5 KiB
TypeScript

import { HandlerContext, Handlers, PageProps, RouteConfig } from "$fresh/server.ts";
import { asset, Head } from "$fresh/runtime.ts";
import {
decodePath,
encodePath,
removePrefixFromPathname,
} from "../../util/util.ts";
import { join } from "path/posix.ts";
import DirList, { EntryInfo } from "../../islands/DirList.tsx";
import FileViewer from "../../islands/FileViewer.tsx";
import RenderView from "../../islands/ContentRenderer.tsx";
import { serveFile } from "http/file_server.ts";
import { Status } from "http/http_status.ts";
type DirProps = {
type: "dir";
path: string;
stat: Deno.FileInfo;
files: EntryInfo[];
};
type FileProps = {
type: "file";
path: string;
stat: Deno.FileInfo;
};
export type DirOrFileProps = DirProps | FileProps;
type RenderOption = {
fileInfo?: Deno.FileInfo;
};
async function renderFile(
req: Request,
path: string,
{ fileInfo }: RenderOption = {},
) {
try {
if (!fileInfo) {
fileInfo = await Deno.stat(path);
}
if (fileInfo.isDirectory) {
// if index.html exists, serve it.
// otherwise, serve a directory listing.
const indexPath = join(path, "/index.html");
try {
await Deno.stat(indexPath);
const res = await serveFile(req, indexPath);
return res;
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
const list: Deno.DirEntry[] = [];
for await (const entry of Deno.readDir(path)) {
list.push(entry);
}
return new Response(
JSON.stringify(
list,
),
{
headers: {
"content-type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods":
"GET,HEAD,PUT,PATCH,POST,DELETE",
"Access-Control-Allow-Headers":
"Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With",
},
status: 200,
},
);
}
}
}
const res = await serveFile(req, path, {
fileInfo,
});
return res;
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
return new Response("Not Found", {
status: 404,
});
}
throw e;
}
}
async function renderPage(
_req: Request,
path: string,
ctx: HandlerContext,
{ fileInfo }: RenderOption = {},
) {
try {
if (!fileInfo) {
fileInfo = await Deno.stat(path);
}
if (fileInfo.isDirectory) {
const filesIter = await Deno.readDir(path);
const files: EntryInfo[] = [];
for await (const file of filesIter) {
const fileStat = await Deno.stat(join(path, file.name));
files.push({
...file,
lastModified: fileStat.mtime ? new Date(fileStat.mtime) : undefined,
size: fileStat.size,
});
}
return await ctx.render({
type: "dir",
fileInfo,
files,
path,
});
} else {
return await ctx.render({
type: "file",
fileInfo,
path,
});
}
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
return await ctx.renderNotFound();
}
throw e;
}
}
async function GET(req: Request, ctx: HandlerContext): Promise<Response> {
const url = new URL(req.url);
const path = removePrefixFromPathname(decodePath(url.pathname), "/dir");
const fileInfo = await Deno.stat(path);
if (fileInfo.isFile && url.pathname.endsWith("/")) {
url.pathname = url.pathname.slice(0, -1);
return Response.redirect(url, Status.TemporaryRedirect);
}
if ((!fileInfo.isFile) && (!url.pathname.endsWith("/"))) {
url.pathname += "/";
return Response.redirect(url, Status.TemporaryRedirect);
}
if (url.searchParams.has("pretty")) {
return await renderPage(req, path, ctx, { fileInfo });
} else {
return await renderFile(req, path, { fileInfo });
}
}
export const handler: Handlers = {
GET,
};
function isImageFile(path: string) {
return /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico|tiff)$/i.test(path);
}
function searchFiles(
path: EntryInfo[],
fn: (path: EntryInfo) => boolean,
opt: { order?: (a: EntryInfo, b: EntryInfo) => number } = {},
) {
const candiate = path.filter(fn);
if (candiate.length > 0) {
if (opt.order) {
candiate.sort(opt.order);
}
return candiate[0];
}
return null;
}
export default function DirLists(props: PageProps<DirOrFileProps>) {
const data = props.data;
let cover = null, index = null, content = null;
if (data.type === "dir") {
cover = searchFiles(data.files, (f) => isImageFile(f.name));
index = searchFiles(data.files, (f) => f.name === "index.html");
const contentFilenameCandidate = [
"README.md",
"readme.md",
"README.txt",
"readme.txt",
];
const contentFilenameCandidateSet = new Set(contentFilenameCandidate);
content = searchFiles(
data.files,
(f) => contentFilenameCandidateSet.has(f.name),
{
order: (a, b) => {
const aIndex = contentFilenameCandidate.indexOf(a.name);
const bIndex = contentFilenameCandidate.indexOf(b.name);
return (aIndex - bIndex);
},
},
);
}
return (
<>
<Head>
<title>Simple file server : {data.path}</title>
<link rel="stylesheet" href={asset("/github-markdown.css")} />
<link rel="stylesheet" href={asset("/base.css")} />
</Head>
<div class="p-4 mx-auto max-w-screen-md">
{data.type === "dir"
? <DirList path={data.path} files={data.files}></DirList>
: <FileViewer path={data.path}></FileViewer>}
{index
? (
<a
href={`/dir/${encodePath(join(data.path, index.name))}`}
>
{cover
? (
<img
src={`/dir/${encodePath(join(data.path, cover.name))}`}
/>
)
: (
<span class="border-2 border-gray-300 rounded-md p-2 block mt-2">
Index
</span>
)}
</a>
)
: null}
{content
? (
<div
class="border-2 border-gray-300 rounded-md p-2 mt-2"
id="README"
>
<RenderView
src={`/dir/${encodePath(join(data.path, content.name))}`}
/>
</div>
)
: null}
</div>
</>
);
}
export const config : RouteConfig = {
routeOverride: "/dir/**{/}?"
}