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

249 lines
6.5 KiB
TypeScript
Raw Permalink Normal View History

2023-01-14 03:43:43 +09:00
import { HandlerContext, Handlers, PageProps, RouteConfig } from "$fresh/server.ts";
2023-01-07 02:37:35 +09:00
import { asset, Head } from "$fresh/runtime.ts";
2023-01-06 22:50:32 +09:00
import {
decodePath,
encodePath,
removePrefixFromPathname,
} from "../../util/util.ts";
2023-01-05 18:18:07 +09:00
import { join } from "path/posix.ts";
import DirList, { EntryInfo } from "../../islands/DirList.tsx";
import FileViewer from "../../islands/FileViewer.tsx";
2023-01-06 22:17:45 +09:00
import RenderView from "../../islands/ContentRenderer.tsx";
import { serveFile } from "http/file_server.ts";
2023-01-14 03:03:22 +09:00
import { Status } from "http/http_status.ts";
2023-01-05 18:18:07 +09:00
type DirProps = {
type: "dir";
path: string;
stat: Deno.FileInfo;
files: EntryInfo[];
2023-01-06 18:24:27 +09:00
};
2023-01-05 18:18:07 +09:00
type FileProps = {
type: "file";
path: string;
stat: Deno.FileInfo;
2023-01-06 18:24:27 +09:00
};
2023-01-05 18:18:07 +09:00
2023-01-14 03:03:22 +09:00
export type DirOrFileProps = DirProps | FileProps;
2023-01-05 18:18:07 +09:00
2023-01-14 03:03:22 +09:00
type RenderOption = {
fileInfo?: Deno.FileInfo;
2023-01-14 03:03:58 +09:00
};
2023-01-14 03:03:22 +09:00
2023-01-14 03:03:58 +09:00
async function renderFile(
req: Request,
path: string,
{ fileInfo }: RenderOption = {},
) {
2023-01-06 22:17:45 +09:00
try {
2023-01-14 03:03:22 +09:00
if (!fileInfo) {
fileInfo = await Deno.stat(path);
}
2023-01-06 22:17:45 +09:00
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;
}
}
2023-01-14 03:03:58 +09:00
async function renderPage(
_req: Request,
path: string,
ctx: HandlerContext,
{ fileInfo }: RenderOption = {},
) {
2023-01-06 22:17:45 +09:00
try {
2023-01-14 03:03:22 +09:00
if (!fileInfo) {
fileInfo = await Deno.stat(path);
}
2023-01-06 22:17:45 +09:00
2023-01-14 03:03:22 +09:00
if (fileInfo.isDirectory) {
2023-01-06 22:17:45 +09:00
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",
2023-01-14 03:03:22 +09:00
fileInfo,
2023-01-06 22:17:45 +09:00
files,
path,
});
} else {
return await ctx.render({
type: "file",
2023-01-14 03:03:22 +09:00
fileInfo,
2023-01-06 22:17:45 +09:00
path,
});
}
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
return await ctx.renderNotFound();
}
throw e;
}
}
2023-01-06 18:24:27 +09:00
async function GET(req: Request, ctx: HandlerContext): Promise<Response> {
2023-01-05 18:18:07 +09:00
const url = new URL(req.url);
2023-01-06 22:50:32 +09:00
const path = removePrefixFromPathname(decodePath(url.pathname), "/dir");
2023-01-14 03:03:22 +09:00
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);
}
2023-01-06 22:17:45 +09:00
if (url.searchParams.has("pretty")) {
2023-01-14 03:03:22 +09:00
return await renderPage(req, path, ctx, { fileInfo });
2023-01-06 18:24:27 +09:00
} else {
2023-01-14 03:03:22 +09:00
return await renderFile(req, path, { fileInfo });
2023-01-05 18:18:07 +09:00
}
2023-01-06 18:24:27 +09:00
}
2023-01-05 18:18:07 +09:00
export const handler: Handlers = {
2023-01-06 18:24:27 +09:00
GET,
};
2023-01-05 18:18:07 +09:00
2023-01-06 22:17:45 +09:00
function isImageFile(path: string) {
return /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico|tiff)$/i.test(path);
}
2023-01-06 22:29:48 +09:00
function searchFiles(
path: EntryInfo[],
fn: (path: EntryInfo) => boolean,
opt: { order?: (a: EntryInfo, b: EntryInfo) => number } = {},
) {
2023-01-06 22:17:45 +09:00
const candiate = path.filter(fn);
if (candiate.length > 0) {
2023-01-06 22:29:48 +09:00
if (opt.order) {
candiate.sort(opt.order);
}
2023-01-06 22:17:45 +09:00
return candiate[0];
}
return null;
}
2023-01-05 18:18:07 +09:00
export default function DirLists(props: PageProps<DirOrFileProps>) {
const data = props.data;
2023-01-06 22:17:45 +09:00
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");
2023-01-06 22:29:48 +09:00
const contentFilenameCandidate = [
2023-01-06 22:17:45 +09:00
"README.md",
"readme.md",
"README.txt",
"readme.txt",
2023-01-06 22:29:48 +09:00
];
const contentFilenameCandidateSet = new Set(contentFilenameCandidate);
2023-01-06 22:17:45 +09:00
content = searchFiles(
data.files,
2023-01-06 22:29:48 +09:00
(f) => contentFilenameCandidateSet.has(f.name),
{
order: (a, b) => {
const aIndex = contentFilenameCandidate.indexOf(a.name);
const bIndex = contentFilenameCandidate.indexOf(b.name);
return (aIndex - bIndex);
},
},
2023-01-06 22:17:45 +09:00
);
}
2023-01-06 18:24:27 +09:00
return (
<>
<Head>
<title>Simple file server : {data.path}</title>
2023-01-07 02:37:35 +09:00
<link rel="stylesheet" href={asset("/github-markdown.css")} />
2023-02-08 01:33:54 +09:00
<link rel="stylesheet" href={asset("/base.css")} />
2023-01-06 18:24:27 +09:00
</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>}
2023-01-06 22:17:45 +09:00
{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))}`}
2023-01-06 22:29:48 +09:00
/>
2023-01-06 22:17:45 +09:00
</div>
)
: null}
2023-01-06 18:24:27 +09:00
</div>
</>
2023-01-05 18:18:07 +09:00
);
2023-01-14 03:03:58 +09:00
}
2023-01-14 03:43:43 +09:00
export const config : RouteConfig = {
routeOverride: "/dir/**{/}?"
}