250 lines
6.5 KiB
TypeScript
250 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 = [
|
|
"SUMMARY.md",
|
|
"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/**{/}?"
|
|
} |