2023-01-06 18:24:27 +09:00
|
|
|
import { HandlerContext, Handlers, PageProps } from "$fresh/server.ts";
|
|
|
|
import { asset, Head } from "$fresh/runtime.ts";
|
2023-01-06 22:17:45 +09:00
|
|
|
import { 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-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
|
|
|
|
|
|
|
type DirOrFileProps = DirProps | FileProps;
|
|
|
|
|
2023-01-06 22:17:45 +09:00
|
|
|
async function renderFile(req: Request, path: string) {
|
|
|
|
try {
|
|
|
|
const 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) {
|
|
|
|
try {
|
|
|
|
const stat = await Deno.stat(path);
|
|
|
|
|
|
|
|
if (stat.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",
|
|
|
|
stat,
|
|
|
|
files,
|
|
|
|
path,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return await ctx.render({
|
|
|
|
type: "file",
|
|
|
|
stat,
|
|
|
|
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 authRequired = Deno.env.get("AUTH_REQUIRED") === "true";
|
2023-01-06 18:24:27 +09:00
|
|
|
if (authRequired) {
|
|
|
|
const login = ctx.state["login"];
|
2023-01-06 22:17:45 +09:00
|
|
|
//console.log("login", login);
|
2023-01-06 18:24:27 +09:00
|
|
|
if (!login) {
|
|
|
|
return new Response(null, {
|
|
|
|
status: 302,
|
|
|
|
headers: {
|
|
|
|
"Location": "/login",
|
|
|
|
"content-type": "text/plain",
|
|
|
|
"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",
|
|
|
|
},
|
|
|
|
});
|
2023-01-05 18:18:07 +09:00
|
|
|
}
|
2023-01-06 18:24:27 +09:00
|
|
|
}
|
2023-01-05 18:18:07 +09:00
|
|
|
const url = new URL(req.url);
|
2023-01-06 18:20:26 +09:00
|
|
|
const path = removePrefixFromPathname(decodeURI(url.pathname), "/dir");
|
2023-01-06 22:17:45 +09:00
|
|
|
if (url.searchParams.has("pretty")) {
|
|
|
|
return await renderPage(req, path, ctx);
|
2023-01-06 18:24:27 +09:00
|
|
|
} else {
|
2023-01-06 22:17:45 +09:00
|
|
|
return await renderFile(req, path);
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
function searchFiles(path: EntryInfo[], fn: (path: EntryInfo) => boolean) {
|
|
|
|
const candiate = path.filter(fn);
|
|
|
|
if (candiate.length > 0) {
|
|
|
|
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");
|
|
|
|
const contentFilenameCandidate = new Set([
|
|
|
|
"SUMMARY.md",
|
|
|
|
"README.md",
|
|
|
|
"readme.md",
|
|
|
|
"README.txt",
|
|
|
|
"readme.txt",
|
|
|
|
]);
|
|
|
|
content = searchFiles(
|
|
|
|
data.files,
|
|
|
|
(f) => contentFilenameCandidate.has(f.name),
|
|
|
|
);
|
|
|
|
}
|
2023-01-06 18:24:27 +09:00
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<Head>
|
|
|
|
<title>Simple file server : {data.path}</title>
|
|
|
|
</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))}`}
|
|
|
|
>
|
|
|
|
</RenderView>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
: null}
|
2023-01-06 18:24:27 +09:00
|
|
|
</div>
|
|
|
|
</>
|
2023-01-05 18:18:07 +09:00
|
|
|
);
|
|
|
|
}
|