This commit is contained in:
monoid 2023-01-06 18:24:27 +09:00
parent d959076d28
commit 8b3db1e2f1
33 changed files with 1174 additions and 1034 deletions

View File

@ -23,4 +23,4 @@ Start the project:
deno task start deno task start
``` ```
This will watch the project directory and restart as necessary. This will watch the project directory and restart as necessary.

View File

@ -3,63 +3,67 @@ import MarkdownRenderer from "./MarkdownRenderer.tsx";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
const TypeToExt = { const TypeToExt = {
"image": new Set([".png", ".jpg", ".jpeg", ".gif", ".svg"]), "image": new Set([".png", ".jpg", ".jpeg", ".gif", ".svg"]),
"video": new Set([".mp4", ".webm", ".ogg"]), "video": new Set([".mp4", ".webm", ".ogg"]),
"audio": new Set([".mp3", ".wav", ".flac"]), "audio": new Set([".mp3", ".wav", ".flac"]),
"md": new Set([".md"]), "md": new Set([".md"]),
"text": new Set([".txt"]), "text": new Set([".txt"]),
"code": new Set([".json", ".js", ".ts", ".css", ".tsx", ".jsx"]), "code": new Set([".json", ".js", ".ts", ".css", ".tsx", ".jsx"]),
} };
function extToType(ext: string) { function extToType(ext: string) {
for (const [type, exts] of Object.entries(TypeToExt)) { for (const [type, exts] of Object.entries(TypeToExt)) {
if (exts.has(ext)) { if (exts.has(ext)) {
return type; return type;
}
} }
return "unknown"; }
return "unknown";
} }
function FetchAndRender(props: {src: string, type: string}){ function FetchAndRender(props: { src: string; type: string }) {
const src = props.src; const src = props.src;
const ext = extname(src); const ext = extname(src);
const [content, setContent] = useState(""); const [content, setContent] = useState("");
useEffect(() => { useEffect(() => {
fetch(src).then(res => res.text()).then(setContent); fetch(src).then((res) => res.text()).then(setContent);
}, [src]); }, [src]);
switch (props.type) { switch (props.type) {
case "text": case "text":
return <div style={{ whiteSpace: "pre-wrap" }}>{content}</div>; return <div style={{ whiteSpace: "pre-wrap" }}>{content}</div>;
case "md": case "md":
return <MarkdownRenderer text={content} />; return <MarkdownRenderer text={content} />;
case "code": case "code":
return <pre style={{ whiteSpace: "pre-wrap" }}><code lang={ext.slice(1)}>{content}</code></pre>; return (
//case "csv": <pre
// return <CsvRenderer content={content} />; style={{ whiteSpace: "pre-wrap" }}
default: ><code lang={ext.slice(1)}>{content}</code></pre>
return <>error: invalid type: {props.type} content: {content}</>; );
} //case "csv":
// return <CsvRenderer content={content} />;
default:
return <>error: invalid type: {props.type} content: {content}</>;
}
} }
export function RenderView(props: {src: string}) { export function RenderView(props: { src: string }) {
const src = props.src; const src = props.src;
const type = extToType(extname(src)); const type = extToType(extname(src));
switch (type) { switch (type) {
case "text": case "text":
case "md": case "md":
case "code": case "code":
return <FetchAndRender src={src} type={type}></FetchAndRender> return <FetchAndRender src={src} type={type}></FetchAndRender>;
//case "csv": //case "csv":
// return <CsvRenderer content={content} />; // return <CsvRenderer content={content} />;
case "image": case "image":
return <img style={{ width: "100%" }} src={src} />; return <img style={{ width: "100%" }} src={src} />;
case "video": case "video":
return <video style={{ width: "100%" }} controls src={src} />; return <video style={{ width: "100%" }} controls src={src} />;
case "audio": case "audio":
return <audio style={{ width: "100%" }} controls src={src} />; return <audio style={{ width: "100%" }} controls src={src} />;
default: default:
return <>error: invalid type: {type} src: {src}</>; return <>error: invalid type: {type} src: {src}</>;
} }
} }
export default RenderView; export default RenderView;

View File

@ -1,94 +1,108 @@
import { Head, asset } from "$fresh/runtime.ts"; import { asset, Head } from "$fresh/runtime.ts";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { extname, join } from "path/posix.ts"; import { extname, join } from "path/posix.ts";
import { ComponentChild } from "preact"; import { ComponentChild } from "preact";
import UpList from "./UpList.tsx"; import UpList from "./UpList.tsx";
import {extToIcon} from "../src/media.ts"; import { extToIcon } from "../src/media.ts";
import { encodePath } from "../util/util.ts"; import { encodePath } from "../util/util.ts";
function ListItem(props:{ function ListItem(props: {
href: string, href: string;
icon: string, icon: string;
children: ComponentChild children: ComponentChild;
}) { }) {
return ( return (
<li class="p-1 hover:bg-gray-400 transition-colors"> <li class="p-1 hover:bg-gray-400 transition-colors">
<a class="flex gap-2" href={props.href}> <a class="flex gap-2" href={props.href}>
<img src={asset(props.icon)}/><p class="">{props.children}</p></a> <img src={asset(props.icon)} />
</li> <p class="">{props.children}</p>
); </a>
</li>
);
} }
export interface EntryInfo{ export interface EntryInfo {
name: string; name: string;
isFile: boolean; isFile: boolean;
isDirectory: boolean; isDirectory: boolean;
isSymlink: boolean; isSymlink: boolean;
size: number; size: number;
lastModified?: Date; lastModified?: Date;
} }
interface DirListProps { interface DirListProps {
path: string; path: string;
files: EntryInfo[]; files: EntryInfo[];
} }
export function DirList(props: DirListProps) { export function DirList(props: DirListProps) {
const data = props; const data = props;
const [files, setFiles] = useState(data.files); const [files, setFiles] = useState(data.files);
return (<div> return (
<UpList path={data.path}></UpList> <div>
<ul class="border-2 rounded-md"> <UpList path={data.path}></UpList>
<li class="p-1 flex gap-2"> <ul class="border-2 rounded-md">
<button class="flex" onClick={sortDir}> <li class="p-1 flex gap-2">
<img src={asset("/icon/sort-down.svg")}/> Sort Directory <button class="flex" onClick={sortDir}>
</button> <img src={asset("/icon/sort-down.svg")} /> Sort Directory
<button class="flex" onClick={sortAlpha}> </button>
<img src={asset("/icon/sort-alpha-down.svg")} /> Sort Alphabet <button class="flex" onClick={sortAlpha}>
</button> <img src={asset("/icon/sort-alpha-down.svg")} /> Sort Alphabet
</li> </button>
<ListItem key=".." href={`/dir/${encodePath(join(data.path,".."))}`} </li>
icon="/icon/back.svg" <ListItem
>...</ListItem> key=".."
{files.map((file) => ( href={`/dir/${encodePath(join(data.path, ".."))}`}
<ListItem key={file.name} href={`/dir/${encodePath(join(data.path,file.name))}`} icon="/icon/back.svg"
icon={file.isDirectory ? "/icon/folder.svg": extToIcon(extname(file.name))} >
>{file.name}</ListItem> ...
))} </ListItem>
</ul> {files.map((file) => (
</div>) <ListItem
function sortDir() { key={file.name}
// sort by directory first then by index href={`/dir/${encodePath(join(data.path, file.name))}`}
const sorted_files = files.map((x,i)=> icon={file.isDirectory
([x,i] as [EntryInfo, number])) ? "/icon/folder.svg"
.sort( : extToIcon(extname(file.name))}
([a, ai],[b,bi]) => { >
if (a.isDirectory && !b.isDirectory) { {file.name}
return -1; </ListItem>
} else if (!a.isDirectory && b.isDirectory) { ))}
return 1; </ul>
} else { </div>
return ai - bi; );
} function sortDir() {
}); // sort by directory first then by index
setFiles(sorted_files.map(([x,_])=>x)); const sorted_files = files.map((x, i) => ([x, i] as [EntryInfo, number]))
} .sort(
function sortAlpha() { ([a, ai], [b, bi]) => {
// sort by alphabet first then by index if (a.isDirectory && !b.isDirectory) {
const sorted_files = files.map((x,i)=> return -1;
([x,i] as [EntryInfo, number])) } else if (!a.isDirectory && b.isDirectory) {
.sort( return 1;
([a, ai],[b,bi]) => { } else {
const ret = a.name.localeCompare(b.name); return ai - bi;
if (ret === 0) { }
return ai - bi; },
} else { );
return ret; setFiles(sorted_files.map(([x, _]) => x));
} }
}); function sortAlpha() {
setFiles(sorted_files.map(([x,_])=>x)); // sort by alphabet first then by index
} const sorted_files = files.map((x, i) => ([x, i] as [EntryInfo, number]))
.sort(
([a, ai], [b, bi]) => {
const ret = a.name.localeCompare(b.name);
if (ret === 0) {
return ai - bi;
} else {
return ret;
}
},
);
setFiles(sorted_files.map(([x, _]) => x));
}
} }
export default DirList; export default DirList;

View File

@ -3,43 +3,52 @@ import { useEffect, useState } from "preact/hooks";
import { Index } from "../src/client_search.ts"; import { Index } from "../src/client_search.ts";
import { encodePath } from "../util/util.ts"; import { encodePath } from "../util/util.ts";
function SearchBar(props:{ function SearchBar(props: {
search?: string; search?: string;
onSearch?: (search: string) => void; onSearch?: (search: string) => void;
}){ }) {
const [search, setSearch] = useState(props.search ?? ""); const [search, setSearch] = useState(props.search ?? "");
return ( return (
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<input type="text" class="w-1/2 px-4 py-2 border-2 rounded-lg" placeholder="Search..." <input
//onChange={(event)=>{}} type="text"
onKeyUp={(event)=>{ class="w-1/2 px-4 py-2 border-2 rounded-lg"
if (event.currentTarget.value === search) return; placeholder="Search..."
setSearch(event.currentTarget.value); //onChange={(event)=>{}}
props.onSearch?.(event.currentTarget.value); onKeyUp={(event) => {
}}>{search}</input> if (event.currentTarget.value === search) return;
</div> setSearch(event.currentTarget.value);
) props.onSearch?.(event.currentTarget.value);
}}
>
{search}
</input>
</div>
);
} }
export default function DocSearch(props: { export default function DocSearch(props: {
docs: Doc[] docs: Doc[];
}){ }) {
const [docs, setDocs] = useState(props.docs); const [docs, setDocs] = useState(props.docs);
const index = Index.createIndex(props.docs); const index = Index.createIndex(props.docs);
return ( return (
<> <>
<SearchBar onSearch={(s)=>{ <SearchBar
setDocs(index.search(s)); onSearch={(s) => {
}}></SearchBar> setDocs(index.search(s));
<h1 class="text-2xl font-bold">Doc</h1> }}
<ul class="mt-4"> >
{docs.map((doc) => ( </SearchBar>
<li class="mt-2"> <h1 class="text-2xl font-bold">Doc</h1>
<a href={`/dir/${encodePath(doc.path)}`}> {doc.path}</a> <ul class="mt-4">
</li> {docs.map((doc) => (
))} <li class="mt-2">
</ul> <a href={`/dir/${encodePath(doc.path)}`}>{doc.path}</a>
</> </li>
) ))}
} </ul>
</>
);
}

View File

@ -4,15 +4,15 @@ import { extname } from "path/mod.ts";
import { encodePath } from "../util/util.ts"; import { encodePath } from "../util/util.ts";
export default function FileViewer(props: { path: string }) { export default function FileViewer(props: { path: string }) {
const { path } = props; const { path } = props;
const srcPath = `/fs/${encodePath(path)}`; const srcPath = `/fs/${encodePath(path)}`;
return ( return (
<div class="p-4 mx-auto max-w-screen-md"> <div class="p-4 mx-auto max-w-screen-md">
<UpList path={path} /> <UpList path={path} />
<a href={srcPath}>Direct link</a> <a href={srcPath}>Direct link</a>
<div class="p-2 border-2 rounded-lg"> <div class="p-2 border-2 rounded-lg">
<RenderView src={srcPath}/> <RenderView src={srcPath} />
</div> </div>
</div> </div>
) );
} }

View File

@ -1,13 +1,18 @@
import { marked } from "https://deno.land/x/marked@1.0.1/mod.ts"; import { marked } from "https://deno.land/x/marked@1.0.1/mod.ts";
export function MarkdownRenderer(props: { text: string | undefined }) { export function MarkdownRenderer(props: { text: string | undefined }) {
let text = props.text; let text = props.text;
if (text === undefined) { if (text === undefined) {
text = ""; text = "";
} }
const index = text.indexOf('\n---', 3); const index = text.indexOf("\n---", 3);
const c = text.slice(index + 4, text.length); const c = text.slice(index + 4, text.length);
return <div class="markdown-body" dangerouslySetInnerHTML={{ __html: marked.parse(c) }} />; return (
<div
class="markdown-body"
dangerouslySetInnerHTML={{ __html: marked.parse(c) }}
/>
);
} }
export default MarkdownRenderer; export default MarkdownRenderer;

View File

@ -1,40 +1,44 @@
import { Head, asset } from "$fresh/runtime.ts"; import { asset, Head } from "$fresh/runtime.ts";
import { join } from "path/posix.ts"; import { join } from "path/posix.ts";
import { ComponentChild } from "preact"; import { ComponentChild } from "preact";
import { encodePath } from "../util/util.ts"; import { encodePath } from "../util/util.ts";
function stairs(path: string) {
function stairs(path: string){ if (path === ".") return [];
if (path === ".") return []; const uplist = path.split("/");
const uplist = path.split("/"); let current = ".";
let current = "."; const stairs = [];
const stairs = []; for (const up of uplist) {
for (const up of uplist){ current = join(current, up);
current = join(current, up); stairs.push([current, up]);
stairs.push([current, up]); }
} return stairs;
return stairs;
} }
export default function UpList(props:{path: string}) { export default function UpList(props: { path: string }) {
const data = props; const data = props;
const uplist = stairs(data.path); const uplist = stairs(data.path);
return (<div> return (
<h1 class={"text-2xl flex flex-wrap"}> <div>
<a href="/dir/" class="flex flex-wrap p-2 hover:bg-gray-400 rounded-sm"> <h1 class={"text-2xl flex flex-wrap"}>
<img src={asset("/icon/house.svg")}/> <a href="/dir/" class="flex flex-wrap p-2 hover:bg-gray-400 rounded-sm">
<span class="ml-1">Home</span></a> <img src={asset("/icon/house.svg")} />
{ <span class="ml-1">Home</span>
uplist.map(([cur, up], i) => ( </a>
<> {uplist.map(([cur, up], i) => (
<span class="p-2">/</span> <>
<a class="flex flex-wrap p-2 hover:bg-gray-400 rounded-sm" href={`/dir/${encodePath(cur)}`}> <span class="p-2">/</span>
<img src={asset("/icon/folder.svg")} /> <a
<span class="ml-1">{up}</span> class="flex flex-wrap p-2 hover:bg-gray-400 rounded-sm"
</a> href={`/dir/${encodePath(cur)}`}
</> >
)) <img src={asset("/icon/folder.svg")} />
}</h1> <span class="ml-1">{up}</span>
</div>) </a>
} </>
))}
</h1>
</div>
);
}

View File

@ -4,24 +4,27 @@ import { prepareSecretKey } from "./util/secret.ts";
export const key_out_cmd = new Command(); export const key_out_cmd = new Command();
key_out_cmd.name("keyout") key_out_cmd.name("keyout")
.description("Output the secret key.") .description("Output the secret key.")
.option("-f, --file <file:string>", "The file to output the key to. (default: stdout)") .option(
.option("--dotenv", "Output the key in dotenv format.") "-f, --file <file:string>",
.action(async ({file, dotenv}) => { "The file to output the key to. (default: stdout)",
)
.option("--dotenv", "Output the key in dotenv format.")
.action(async ({ file, dotenv }) => {
const key = await prepareSecretKey(); const key = await prepareSecretKey();
const keyout = await crypto.subtle.exportKey("jwk", key); const keyout = await crypto.subtle.exportKey("jwk", key);
let out = JSON.stringify(keyout); let out = JSON.stringify(keyout);
if (dotenv){ if (dotenv) {
out = [`SECRET_KEY='${out}'`].join("\n"); out = [`SECRET_KEY='${out}'`].join("\n");
} }
if (file){ if (file) {
await Deno.writeTextFile(file, out); await Deno.writeTextFile(file, out);
} else { } else {
console.log(out); console.log(out);
} }
}); });
if ( import.meta.main ){ if (import.meta.main) {
await key_out_cmd.parse(Deno.args); await key_out_cmd.parse(Deno.args);
} }

118
main.ts
View File

@ -4,14 +4,19 @@
/// <reference lib="dom.asynciterable" /> /// <reference lib="dom.asynciterable" />
/// <reference lib="deno.ns" /> /// <reference lib="deno.ns" />
import { PluginRenderResult, Plugin, ServerContext, Manifest, StartOptions } from "$fresh/server.ts"; import {
Manifest,
Plugin,
PluginRenderResult,
ServerContext,
StartOptions,
} from "$fresh/server.ts";
import manifest from "./fresh.gen.ts"; import manifest from "./fresh.gen.ts";
import twindPlugin from "$fresh/plugins/twind.ts"; import twindPlugin from "$fresh/plugins/twind.ts";
import twindConfig from "./twind.config.ts"; import twindConfig from "./twind.config.ts";
import "https://deno.land/std@0.170.0/dotenv/load.ts"; import "https://deno.land/std@0.170.0/dotenv/load.ts";
import { Command } from "https://deno.land/x/cliffy@v0.25.6/mod.ts"; import { Command } from "https://deno.land/x/cliffy@v0.25.6/mod.ts";
import { fromFileUrl, join } from "path/mod.ts"; import { fromFileUrl, join } from "path/mod.ts";
import { prepareSecretKey } from "./util/secret.ts"; import { prepareSecretKey } from "./util/secret.ts";
@ -20,71 +25,80 @@ import { serve } from "http/server.ts";
import { user_command } from "./user.ts"; import { user_command } from "./user.ts";
import { key_out_cmd } from "./keyout.ts"; import { key_out_cmd } from "./keyout.ts";
const github_markdown= await Deno.readTextFile(join(fromFileUrl(import.meta.url), "..", "static", "github-markdown.css")) const github_markdown = await Deno.readTextFile(
join(fromFileUrl(import.meta.url), "..", "static", "github-markdown.css"),
);
const CSSPlugin: Plugin = { const CSSPlugin: Plugin = {
name:"css plugin", name: "css plugin",
// deno-lint-ignore require-await // deno-lint-ignore require-await
render(ctx): PluginRenderResult{ render(ctx): PluginRenderResult {
ctx.render(); ctx.render();
return { return {
styles: [{ styles: [{
cssText: github_markdown, cssText: github_markdown,
}] }],
} };
} },
}; };
await prepareSecretKey(); await prepareSecretKey();
async function startServer(manifest: Manifest, options: StartOptions = {}){ async function startServer(manifest: Manifest, options: StartOptions = {}) {
const ctx = await ServerContext.fromManifest(manifest, options); const ctx = await ServerContext.fromManifest(manifest, options);
options.port ??= 8000; options.port ??= 8000;
if (options.experimentalDenoServe === true) { if (options.experimentalDenoServe === true) {
// @ts-ignore as `Deno.serve` is still unstable. // @ts-ignore as `Deno.serve` is still unstable.
await Deno.serve(ctx.handler() as Deno.ServeHandler, options); await Deno.serve(ctx.handler() as Deno.ServeHandler, options);
} else { } else {
await serve(ctx.handler(), options); await serve(ctx.handler(), options);
} }
} }
async function start({port = 8000, hostname = "localhost"}: {port?: number, hostname?: string} = {}){ async function start(
await startServer(manifest, { plugins: [twindPlugin(twindConfig), CSSPlugin], { port = 8000, hostname = "localhost" }: {
port: port, port?: number;
hostname: hostname, hostname?: string;
}); } = {},
) {
await startServer(manifest, {
plugins: [twindPlugin(twindConfig), CSSPlugin],
port: port,
hostname: hostname,
});
} }
if (import.meta.main){ if (import.meta.main) {
const cmd = new Command(); const cmd = new Command();
cmd.name("fs-server") cmd.name("fs-server")
.description("A simple file server that supports search, upload, and download.") .description(
"A simple file server that supports search, upload, and download.",
)
.version("0.0.1") .version("0.0.1")
.globalOption("-d, --debug", "Enable debug mode.") .globalOption("-d, --debug", "Enable debug mode.")
.command("start", "Start the server.") .command("start", "Start the server.")
.option("-p, --port <port:number>", "The port to listen on.", .option("-p, --port <port:number>", "The port to listen on.", {
{ default: 8000 }) default: 8000,
})
.option("--auth", "Enable authentication.") .option("--auth", "Enable authentication.")
.arguments("[hostname:string]") .arguments("[hostname:string]")
.action(async ({debug, port, auth }, hostname) => { .action(async ({ debug, port, auth }, hostname) => {
hostname ??= "localhost"; hostname ??= "localhost";
if (auth){ if (auth) {
Deno.env.set("AUTH_REQUIRED", "true"); Deno.env.set("AUTH_REQUIRED", "true");
} }
if (debug){ if (debug) {
console.log("Debug mode enabled."); console.log("Debug mode enabled.");
} }
await start({ await start({
port: port, port: port,
hostname: hostname, hostname: hostname,
}); });
} })
)
.command("user", user_command) .command("user", user_command)
.command("keyout", key_out_cmd); .command("keyout", key_out_cmd);
await cmd.parse(Deno.args); await cmd.parse(Deno.args);
} else {
await start();
} }
else {
await start();
}

View File

@ -1,20 +1,21 @@
import { MiddlewareHandlerContext } from "$fresh/server.ts"; import { MiddlewareHandlerContext } from "$fresh/server.ts";
import { getCookies } from "http/cookie.ts"; import { getCookies } from "http/cookie.ts";
import { decode, verify } from "djwt"; import { verify } from "djwt";
import { prepareSecretKey } from "../util/secret.ts"; import { prepareSecretKey } from "../util/secret.ts";
const secret_key = await prepareSecretKey(); const secret_key = await prepareSecretKey();
export const handler = export const handler = async (
async (req: Request , ctx: MiddlewareHandlerContext<Record<string, unknown>>) => { req: Request,
const cookies = getCookies(req.headers); ctx: MiddlewareHandlerContext<Record<string, unknown>>,
const jwt = cookies["auth"]; ) => {
try{ const cookies = getCookies(req.headers);
const payload = await verify(jwt, secret_key); const jwt = cookies["auth"];
ctx.state["login"] = payload; try {
} const payload = await verify(jwt, secret_key);
catch (e){ ctx.state["login"] = payload;
ctx.state["login"] = null; } catch (e) {
} ctx.state["login"] = null;
return await ctx.next(); }
} return await ctx.next();
};

View File

@ -6,44 +6,44 @@ import { getUser, verifyUser } from "../../src/user/user.ts";
import { create as createJWT } from "djwt"; import { create as createJWT } from "djwt";
import { prepareSecretKey } from "../../util/secret.ts"; import { prepareSecretKey } from "../../util/secret.ts";
const SECRET_KEY = await prepareSecretKey();
const SECRET_KEY = await prepareSecretKey();
async function POST(req: Request, ctx: HandlerContext): Promise<Response> { async function POST(req: Request, ctx: HandlerContext): Promise<Response> {
const url = new URL(req.url); const url = new URL(req.url);
const form = await req.formData(); const form = await req.formData();
const username = form.get("username"); const username = form.get("username");
const password = form.get("password"); const password = form.get("password");
if (username && password){ if (username && password) {
const DB = connectDB(); const DB = connectDB();
const user = await getUser(DB, username.toString()); const user = await getUser(DB, username.toString());
if (user){ if (user) {
if (await verifyUser(user, password.toString())){ if (await verifyUser(user, password.toString())) {
const headers = new Headers(); const headers = new Headers();
const jwt = await createJWT({alg:"HS512", typ: "JWT"},{ const jwt = await createJWT({ alg: "HS512", typ: "JWT" }, {
username: user.name username: user.name,
}, SECRET_KEY); }, SECRET_KEY);
setCookie(headers, { setCookie(headers, {
name: "auth", name: "auth",
value: jwt, value: jwt,
httpOnly: true, httpOnly: true,
sameSite: "Strict", sameSite: "Strict",
maxAge: 60 * 60 * 24 * 7, maxAge: 60 * 60 * 24 * 7,
domain: url.hostname, domain: url.hostname,
path: "/", path: "/",
secure: url.protocol === "https:" secure: url.protocol === "https:",
}); });
headers.set("Location", "/"); headers.set("Location", "/");
return new Response(null,{ return new Response(null, {
status: Status.SeeOther, // See Other status: Status.SeeOther, // See Other
headers: headers headers: headers,
}); });
} }
}
} }
}
return new Response(`<!DOCTYPE html><html> return new Response(
`<!DOCTYPE html><html>
<head> <title> Login Failed </title> </head> <head> <title> Login Failed </title> </head>
<body> <body>
<h1> Login Failed </h1> <h1> Login Failed </h1>
@ -52,14 +52,16 @@ async function POST(req: Request, ctx: HandlerContext): Promise<Response> {
document.location.href = "/login"; document.location.href = "/login";
</script> </script>
</body> </body>
</html>`, { </html>`,
headers:{ {
"Content-Type": "text/html" headers: {
}, "Content-Type": "text/html",
status: Status.Forbidden, },
}); status: Status.Forbidden,
},
);
} }
export const handler = { export const handler = {
POST POST,
}; };

View File

@ -14,4 +14,4 @@ export const handler: Handlers = {
headers, headers,
}); });
}, },
}; };

View File

@ -1,6 +1,6 @@
import { PageProps, Handlers, HandlerContext } from "$fresh/server.ts"; import { HandlerContext, Handlers, PageProps } from "$fresh/server.ts";
import { Head, asset } from "$fresh/runtime.ts"; import { asset, Head } from "$fresh/runtime.ts";
import {removePrefixFromPathname} from "../../util/util.ts"; import { removePrefixFromPathname } from "../../util/util.ts";
import { join } from "path/posix.ts"; import { join } from "path/posix.ts";
import DirList, { EntryInfo } from "../../islands/DirList.tsx"; import DirList, { EntryInfo } from "../../islands/DirList.tsx";
import FileViewer from "../../islands/FileViewer.tsx"; import FileViewer from "../../islands/FileViewer.tsx";
@ -10,74 +10,78 @@ type DirProps = {
path: string; path: string;
stat: Deno.FileInfo; stat: Deno.FileInfo;
files: EntryInfo[]; files: EntryInfo[];
} };
type FileProps = { type FileProps = {
type: "file"; type: "file";
path: string; path: string;
stat: Deno.FileInfo; stat: Deno.FileInfo;
} };
type DirOrFileProps = DirProps | FileProps; type DirOrFileProps = DirProps | FileProps;
async function GET(req: Request, ctx: HandlerContext): Promise<Response>{ async function GET(req: Request, ctx: HandlerContext): Promise<Response> {
const authRequired = Deno.env.get("AUTH_REQUIRED") === "true"; const authRequired = Deno.env.get("AUTH_REQUIRED") === "true";
if (authRequired) { if (authRequired) {
const login = ctx.state["login"]; const login = ctx.state["login"];
if (!login) { if (!login) {
return new Response(null, { return new Response(null, {
status: 401, status: 302,
headers: { headers: {
"content-type": "text/plain", "Location": "/login",
"Access-Control-Allow-Origin": "*", "content-type": "text/plain",
"Access-Control-Allow-Methods": "GET,HEAD,PUT,PATCH,POST,DELETE", "Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With" "Access-Control-Allow-Methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
} "Access-Control-Allow-Headers":
}); "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With",
} },
});
} }
}
const url = new URL(req.url); const url = new URL(req.url);
const path = removePrefixFromPathname(decodeURI(url.pathname), "/dir"); const path = removePrefixFromPathname(decodeURI(url.pathname), "/dir");
const stat = await Deno.stat(path); const stat = await Deno.stat(path);
if (stat.isDirectory){ if (stat.isDirectory) {
const filesIter = await Deno.readDir(path); const filesIter = await Deno.readDir(path);
const files: EntryInfo[] = [] const files: EntryInfo[] = [];
for await (const file of filesIter){ for await (const file of filesIter) {
const fileStat = await Deno.stat(join(path, file.name)); const fileStat = await Deno.stat(join(path, file.name));
files.push({ files.push({
...file, ...file,
lastModified: fileStat.mtime ? new Date(fileStat.mtime) : undefined, lastModified: fileStat.mtime ? new Date(fileStat.mtime) : undefined,
size: fileStat.size size: fileStat.size,
}); });
} }
return await ctx.render({ return await ctx.render({
type: "dir", type: "dir",
stat, stat,
files files,
, path path,
}) });
} } else {
else{
return await ctx.render({ return await ctx.render({
type: "file", type: "file",
stat, path stat,
path,
}); });
} }
} }
export const handler: Handlers = { export const handler: Handlers = {
GET GET,
} };
export default function DirLists(props: PageProps<DirOrFileProps>) { export default function DirLists(props: PageProps<DirOrFileProps>) {
const data = props.data; const data = props.data;
return (<> return (
<Head> <>
<title>Simple file server : {data.path}</title> <Head>
</Head> <title>Simple file server : {data.path}</title>
<div class="p-4 mx-auto max-w-screen-md"> </Head>
{data.type === "dir" ? (<DirList path={data.path} files={data.files}></DirList>) : <div class="p-4 mx-auto max-w-screen-md">
(<FileViewer path={data.path}></FileViewer>)} {data.type === "dir"
</div> ? <DirList path={data.path} files={data.files}></DirList>
</> : <FileViewer path={data.path}></FileViewer>}
</div>
</>
); );
} }

View File

@ -1,20 +1,37 @@
import { Head } from "$fresh/runtime.ts"; import { Head } from "$fresh/runtime.ts";
import { PageProps, Handlers, HandlerContext } from "$fresh/server.ts"; import { HandlerContext, Handlers, PageProps } from "$fresh/server.ts";
import DocSearch from "../../islands/DocSearch.tsx"; import DocSearch from "../../islands/DocSearch.tsx";
import { Doc } from "../../src/collect.ts"; import { Doc } from "../../src/collect.ts";
import { docCollector } from "../../src/store/doc.ts"; import { docCollector } from "../../src/store/doc.ts";
async function GET(req: Request, ctx: HandlerContext): Promise<Response> { async function GET(req: Request, ctx: HandlerContext): Promise<Response> {
const docs = docCollector.getDocs(); const authRequired = Deno.env.get("AUTH_REQUIRED") === "true";
return await ctx.render({docs}); if (authRequired) {
const login = ctx.state["login"];
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",
},
});
}
}
const docs = docCollector.getDocs();
return await ctx.render({ docs });
} }
export const handler: Handlers = { export const handler: Handlers = {
GET, GET,
} };
export default function Docs(props: PageProps<{docs:Doc[]}>) { export default function Docs(props: PageProps<{ docs: Doc[] }>) {
const {docs} = props.data; const { docs } = props.data;
return ( return (
<> <>
<Head> <Head>
@ -25,4 +42,4 @@ export default function Docs(props: PageProps<{docs:Doc[]}>) {
</div> </div>
</> </>
); );
} }

View File

@ -1,74 +1,82 @@
import { HandlerContext, Handlers } from "$fresh/server.ts"; import { HandlerContext, Handlers } from "$fresh/server.ts";
import { serveFile } from "http/file_server.ts"; import { serveFile } from "http/file_server.ts";
import {removePrefixFromPathname} from "../../util/util.ts"; import { removePrefixFromPathname } from "../../util/util.ts";
export async function GET(req: Request, ctx: HandlerContext): Promise<Response> { export async function GET(
const url = new URL(req.url); req: Request,
const path = removePrefixFromPathname(decodeURI(url.pathname), "/fs"); ctx: HandlerContext,
// if auth is required, check if the user is logged in. ): Promise<Response> {
// if not, return a 401. const url = new URL(req.url);
const authRequired = Deno.env.get("AUTH_REQUIRED") === "true"; const path = removePrefixFromPathname(decodeURI(url.pathname), "/fs");
if (authRequired) { // if auth is required, check if the user is logged in.
const login = ctx.state["login"]; // if not, return a 401.
if (!login) { const authRequired = Deno.env.get("AUTH_REQUIRED") === "true";
return new Response(null, { if (authRequired) {
status: 401, const login = ctx.state["login"];
headers: { if (!login) {
"content-type": "text/plain", return new Response(null, {
"Access-Control-Allow-Origin": "*", status: 302,
"Access-Control-Allow-Methods": "GET,HEAD,PUT,PATCH,POST,DELETE", headers: {
"Access-Control-Allow-Headers": "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With" "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",
},
});
} }
}
try{ try {
const fileInfo = await Deno.stat(path);
const fileInfo = await Deno.stat(path); if (fileInfo.isDirectory) {
if (fileInfo.isDirectory) { // if index.html exists, serve it.
// if index.html exists, serve it. // otherwise, serve a directory listing.
// otherwise, serve a directory listing. const indexPath = path + "/index.html";
const indexPath = path + "/index.html"; try {
try { await Deno.stat(indexPath);
await Deno.stat(indexPath); const res = await serveFile(req, 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; return res;
} } catch (e) {
catch (e) {
if (e instanceof Deno.errors.NotFound) { if (e instanceof Deno.errors.NotFound) {
return new Response("Not Found", { const list: Deno.DirEntry[] = [];
status: 404, 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,
},
);
} }
throw e; }
} }
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;
}
} }
export const handler: Handlers = { export const handler: Handlers = {
GET GET,
} };

View File

@ -13,7 +13,8 @@ export default function Home() {
alt="the fresh logo: a sliced lemon dripping with juice" alt="the fresh logo: a sliced lemon dripping with juice"
/> />
<p class="my-6"> <p class="my-6">
This is a simple file server. It serves files from the <code>CWD</code>. This is a simple file server. It serves files from the{" "}
<code>CWD</code>.
</p> </p>
<a href="/dir/">Go To CWD</a> | <a href="/doc/">Doc</a> <a href="/dir/">Go To CWD</a> | <a href="/doc/">Doc</a>
<hr></hr> <hr></hr>

View File

@ -1,38 +1,55 @@
import { Head } from "$fresh/runtime.ts"; import { Head } from "$fresh/runtime.ts";
export default function Login() { export default function Login() {
return ( return (
<> <>
<Head> <Head>
<title>Simple file server - Login</title> <title>Simple file server - Login</title>
</Head> </Head>
<div class=""> <div class="">
<div class="p-4 absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] <div class="p-4 absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]
flex flex-col items-center border-gray-500 border-2 rounded-md flex flex-col items-center border-gray-500 border-2 rounded-md
sm:max-w-screen-sm max-w-screen-md"> sm:max-w-screen-sm max-w-screen-md">
<img <img
src="/logo.svg" src="/logo.svg"
class="w-32 h-32" class="w-32 h-32"
alt="the fresh logo: a sliced lemon dripping with juice" alt="the fresh logo: a sliced lemon dripping with juice"
/> />
<form action="/api/login" method="POST" class="flex flex-col gap-2 items-stretch"> <form
<div class="flex gap-2 flex-wrap"> action="/api/login"
<div class="basis-40 flex items-center flex-1"> method="POST"
<label for="username" class="w-20">Username</label> class="flex flex-col gap-2 items-stretch"
<input type="text" name="username" id="username" >
class="border-b-2 focus:border-green-500 transition-colors flex-1" /> <div class="flex gap-2 flex-wrap">
</div> <div class="basis-40 flex items-center flex-1">
<div class="flex items-center flex-1"> <label for="username" class="w-20">Username</label>
<label for="password" class="w-20">Password</label> <input
<input type="password" name="password" id="password" type="text"
class="border-b-2 focus:border-green-500 transition-colors flex-1" /> name="username"
</div> id="username"
</div> class="border-b-2 focus:border-green-500 transition-colors flex-1"
<button type="submit" class="bg-gray-400 p-2 rounded />
m-auto">Login</button> </div>
</form> <div class="flex items-center flex-1">
</div> <label for="password" class="w-20">Password</label>
<input
type="password"
name="password"
id="password"
class="border-b-2 focus:border-green-500 transition-colors flex-1"
/>
</div>
</div> </div>
</> <button
); type="submit"
class="bg-gray-400 p-2 rounded
m-auto"
>
Login
</button>
</form>
</div>
</div>
</>
);
} }

152
search.ts
View File

@ -2,84 +2,90 @@ import { Command } from "https://deno.land/x/cliffy@v0.25.6/mod.ts";
import { Index } from "./src/client_search.ts"; import { Index } from "./src/client_search.ts";
import { Doc, DocCollector, loadDocuments } from "./src/collect.ts"; import { Doc, DocCollector, loadDocuments } from "./src/collect.ts";
export async function collectDocuments(path: string, out: string){ export async function collectDocuments(path: string, out: string) {
const collector = new DocCollector({ summaryOnly: true, dropContent: true }); const collector = new DocCollector({ summaryOnly: true, dropContent: true });
await collector.walkDir(path); await collector.walkDir(path);
const docs = collector.getDocs(); const docs = collector.getDocs();
await Deno.writeTextFile(out, JSON.stringify(docs)); await Deno.writeTextFile(out, JSON.stringify(docs));
} }
export async function watchDocuments(doc: string , options?:{ export async function watchDocuments(doc: string, options?: {
abort?: AbortSignal, abort?: AbortSignal;
}){ }) {
const doc_dump = await loadDocuments(doc); const doc_dump = await loadDocuments(doc);
const collector = new DocCollector({ summaryOnly: true, dropContent: true }); const collector = new DocCollector({ summaryOnly: true, dropContent: true });
collector.setDocs(doc_dump); collector.setDocs(doc_dump);
const index = Index.createIndex(doc_dump); const index = Index.createIndex(doc_dump);
async function update(){ async function update() {
index.setDocs(collector.getDocs()); index.setDocs(collector.getDocs());
await Deno.writeTextFile(doc, JSON.stringify(collector.getDocs())); await Deno.writeTextFile(doc, JSON.stringify(collector.getDocs()));
} }
collector.watchDir(".", { collector.watchDir(".", {
async onAdd(doc){ async onAdd(doc) {
console.log("onAdd :", doc.path); console.log("onAdd :", doc.path);
await update(); await update();
}, },
async onRemove(path){ async onRemove(path) {
console.log("onRemove :", path); console.log("onRemove :", path);
await update(); await update();
}, },
async onChange(doc){ async onChange(doc) {
console.log("onModify :", doc.path); console.log("onModify :", doc.path);
await update(); await update();
}, },
abort: options?.abort, abort: options?.abort,
}); });
return index; return index;
} }
export const search = new Command(); export const search = new Command();
search.name("search") search.name("search")
.description("Search for a document.") .description("Search for a document.")
.command("collect", "Collect a document.") .command("collect", "Collect a document.")
.description("Collect documents and index documents.") .description("Collect documents and index documents.")
.arguments("<path:string>") .arguments("<path:string>")
.option("-o, --out <path:string>", "out file to write the index to.", {default: "collected_docs.json"}) .option("-o, --out <path:string>", "out file to write the index to.", {
.action(async ({out} ,path: string) => { default: "collected_docs.json",
console.log("collecting", path); })
await collectDocuments(path, out); .action(async ({ out }, path: string) => {
}) console.log("collecting", path);
.command("search", "Search for a document.") await collectDocuments(path, out);
.description("Search for a document.") })
.arguments("<query:string>") .command("search", "Search for a document.")
.option("-d, --docs <path:string>", "collected document file to search", {default: "collected_docs.json"}) .description("Search for a document.")
.action(async ({docs}, query: string) => { .arguments("<query:string>")
console.log("searching", query); .option("-d, --docs <path:string>", "collected document file to search", {
const doc_dump = await loadDocuments(docs); default: "collected_docs.json",
const results = Index.createIndex(doc_dump).search(query); })
console.log(results); .action(async ({ docs }, query: string) => {
}) console.log("searching", query);
.command("interact", "watch the index and search for a document.") const doc_dump = await loadDocuments(docs);
.description("watch the index and search for a document.") const results = Index.createIndex(doc_dump).search(query);
.option("-d, --doc <path:string>", "doc file to read the docs from.", {default: "collected_docs.json"}) console.log(results);
.action(async ({doc}) => { })
console.log("interacting"); .command("interact", "watch the index and search for a document.")
const index = await watchDocuments(doc); .description("watch the index and search for a document.")
.option("-d, --doc <path:string>", "doc file to read the docs from.", {
default: "collected_docs.json",
})
.action(async ({ doc }) => {
console.log("interacting");
const index = await watchDocuments(doc);
// interact // interact
while (true){ while (true) {
const query = await prompt("query: "); const query = await prompt("query: ");
if (query === null){ if (query === null) {
continue; continue;
} }
if (query === ":exit"){ if (query === ":exit") {
break; break;
} }
const results = index.search(query); const results = index.search(query);
console.log(results); console.log(results);
} }
}); });
if ( import.meta.main ){ if (import.meta.main) {
await search.parse(Deno.args); await search.parse(Deno.args);
} }

View File

@ -1,31 +1,38 @@
//// @deno-types="https://deno.land/x/fuse@v6.4.1/dist/fuse.d.ts" //// @deno-types="https://deno.land/x/fuse@v6.4.1/dist/fuse.d.ts"
import Fuse from 'https://deno.land/x/fuse@v6.4.1/dist/fuse.esm.min.js'; import Fuse from "https://deno.land/x/fuse@v6.4.1/dist/fuse.esm.min.js";
import { Doc } from "./collect.ts"; import { Doc } from "./collect.ts";
export class Index{ export class Index {
private index: Fuse; private index: Fuse;
private constructor(index: Fuse){ private constructor(index: Fuse) {
this.index = index; this.index = index;
} }
public setDocs(docs: Doc[]){ public setDocs(docs: Doc[]) {
this.index.setCollection(docs); this.index.setCollection(docs);
} }
public search(query: string): Doc[]{ public search(query: string): Doc[] {
return this.index.search(query, {limit: 10}).map(( return this.index.search(query, { limit: 10 }).map((
result) => result,
result.item as Doc); ) => result.item as Doc);
} }
public static createIndex(docs: Doc[]){ public static createIndex(docs: Doc[]) {
const index = new Fuse(docs,{ const index = new Fuse(docs, {
keys: ["attributes.title", "attributes.japanese_title", "attributes.tags", "attributes.rjcode", "attributes.author", "path"], keys: [
includeScore: true, "attributes.title",
includeMatches: true, "attributes.japanese_title",
}) "attributes.tags",
"attributes.rjcode",
"attributes.author",
"path",
],
includeScore: true,
includeMatches: true,
});
return new Index(index); return new Index(index);
} }
} }

View File

@ -1,210 +1,208 @@
import {join, extname, relative, basename} from "path/mod.ts"; import { basename, extname, join, relative } from "path/mod.ts";
import { readMarkdownDoc } from "./readDoc.ts"; import { readMarkdownDoc } from "./readDoc.ts";
import { Index } from "./client_search.ts"; import { Index } from "./client_search.ts";
export interface Doc{ export interface Doc {
path: string; path: string;
content: string; content: string;
attributes: { attributes: {
title?: string; title?: string;
japanese_title?: string; japanese_title?: string;
tags?: string[]; tags?: string[];
rjcode?: string; rjcode?: string;
author?: string; author?: string;
}; };
} }
export async function loadDocuments(path: string): Promise<Doc[]>{ export async function loadDocuments(path: string): Promise<Doc[]> {
const doc_json = await Deno.readTextFile(path); const doc_json = await Deno.readTextFile(path);
return JSON.parse(doc_json) as Doc[]; return JSON.parse(doc_json) as Doc[];
} }
export interface DocCollectorOptions{ export interface DocCollectorOptions {
summaryOnly?: boolean; summaryOnly?: boolean;
dropContent?: boolean; dropContent?: boolean;
} }
export class DocCollector { export class DocCollector {
private doc_map: Map<string,Doc>; private doc_map: Map<string, Doc>;
private options: DocCollectorOptions; private options: DocCollectorOptions;
constructor(options: DocCollectorOptions = {}){ constructor(options: DocCollectorOptions = {}) {
this.doc_map = new Map(); this.doc_map = new Map();
this.options = options; this.options = options;
}
public getDocs(): Doc[] {
return [...this.doc_map.values()];
}
public setDoc(doc: Doc) {
if (this.options.dropContent) {
doc.content = "";
}
this.doc_map.set(doc.path, doc);
}
public setDocs(docs: Doc[]) {
for (const doc of docs) {
this.setDoc(doc);
}
}
public removeDoc(path: string) {
this.doc_map.delete(path);
}
public async walkDir(path: string) {
const dir = Deno.readDir(path);
const fileList = [];
for await (const entry of dir) {
fileList.push(entry);
} }
public getDocs(): Doc[]{ if (fileList.some((entry) => entry.name === "SUMMARY.md")) {
return [...this.doc_map.values()]; const { content, metadata } = await readMarkdownDoc(
} join(path, "SUMMARY.md"),
);
public setDoc(doc: Doc){ this.setDoc({
if (this.options.dropContent){ path: join(path, "SUMMARY.md"),
doc.content = ""; content: content,
attributes: metadata,
});
} else {
for (const entry of fileList) {
if (entry.isDirectory) {
await this.walkDir(join(path, entry.name));
} else if (entry.isFile && !this.options.summaryOnly) {
const doc = await this.readDoc(join(path, entry.name));
this.setDoc(doc);
} }
this.doc_map.set(doc.path,doc); }
} }
public setDocs(docs: Doc[]){ }
for (const doc of docs){
public async readDoc(path: string): Promise<Doc> {
const ext = extname(path);
if (ext === ".md") {
return await this.readMarkdown(path);
} else if (ext === ".html" || ext === ".htm" || ext === ".xhtml") {
return await this.readHTML(path);
} else if (ext === ".txt") {
return await this.readText(path);
} else {
return {
path: path,
content: "",
attributes: {},
};
}
}
public async readHTML(path: string): Promise<Doc> {
const content = await Deno.readTextFile(path);
return {
path: path,
content: content,
attributes: {},
};
}
public async readText(path: string): Promise<Doc> {
const content = await Deno.readTextFile(path);
return {
path: path,
content: content,
attributes: {},
};
}
public async readMarkdown(path: string): Promise<Doc> {
const { content, metadata } = await readMarkdownDoc(path);
return {
path: path,
content: content,
attributes: metadata,
};
}
async watchDir(path: string, {
onRemove = (_path: string) => {},
onAdd = (_doc: Doc) => {},
onChange = (_doc: Doc) => {},
abort = undefined,
}: {
onRemove?: (path: string) => void | Promise<void>;
onAdd?: (doc: Doc) => void | Promise<void>;
onChange?: (doc: Doc) => void | Promise<void>;
abort?: AbortSignal;
}) {
const watcher = Deno.watchFs(path);
if (abort) {
abort.addEventListener("abort", () => {
watcher.close();
});
}
for await (const event of watcher) {
if (
event.kind === "access" || event.kind === "other" ||
event.kind === "any"
) {
continue;
}
if (event.paths.length === 0) {
continue;
}
for (const path of event.paths) {
const relpath = relative(Deno.cwd(), path);
const filename = basename(relpath);
if (filename === "SUMMARY.md") {
if (event.kind === "remove") {
this.doc_map.delete(relpath);
await onRemove(relpath);
} else if (event.kind === "create" || event.kind === "modify") {
const { content, metadata } = await readMarkdownDoc(relpath);
const doc = {
path: relpath,
content: content,
attributes: metadata,
};
this.setDoc(doc); this.setDoc(doc);
} if (event.kind === "create") {
} await onAdd(doc);
public removeDoc(path: string){ } else if (event.kind === "modify") {
this.doc_map.delete(path); await onChange(doc);
}
public async walkDir(path: string){
const dir = Deno.readDir(path);
const fileList = [];
for await (const entry of dir){
fileList.push(entry);
}
if (fileList.some((entry) => entry.name === "SUMMARY.md")){
const {content, metadata} = await readMarkdownDoc(join(path, "SUMMARY.md"));
this.setDoc({
path: join(path, "SUMMARY.md"),
content: content,
attributes: metadata,
});
}
else {
for (const entry of fileList){
if (entry.isDirectory){
await this.walkDir(join(path, entry.name));
}
else if (entry.isFile && !this.options.summaryOnly){
const doc = await this.readDoc(join(path, entry.name));
this.setDoc(doc);
}
} }
}
} }
}
} }
}
public async readDoc(path: string): Promise<Doc>{ makeIndex(options?: {
const ext = extname(path); onUpdate?: (() => void) | (() => Promise<void>);
if (ext === ".md"){ abort?: AbortSignal;
return await this.readMarkdown(path); watch?: boolean;
} }) {
else if (ext === ".html" || ext === ".htm" || ext === ".xhtml"){ const opt = options ?? {};
return await this.readHTML(path); const index = Index.createIndex(this.getDocs());
} if (!opt.watch) {
else if (ext === ".txt"){ return index;
return await this.readText(path);
}
else {
return {
path: path,
content: "",
attributes: {}
}
}
} }
const update = async () => {
public async readHTML(path: string): Promise<Doc>{ index.setDocs(this.getDocs());
const content = await Deno.readTextFile(path); if (opt.onUpdate) {
return { await opt.onUpdate();
path: path, }
content: content, };
attributes: {}, this.watchDir(".", {
} async onAdd(_doc) {
} await update();
public async readText(path: string): Promise<Doc>{ },
const content = await Deno.readTextFile(path); async onRemove(_path) {
return { await update();
path: path, },
content: content, async onChange(_doc) {
attributes: {}, await update();
} },
} abort: opt.abort,
public async readMarkdown(path: string): Promise<Doc>{ });
const {content, metadata} = await readMarkdownDoc(path); return index;
}
return { }
path: path,
content: content,
attributes: metadata,
}
}
async watchDir(path: string, {
onRemove = (_path: string) => {},
onAdd = (_doc: Doc) => {},
onChange = (_doc: Doc) => {},
abort = undefined,
}:{
onRemove?: (path: string) => void | Promise<void>,
onAdd?: (doc: Doc) => void | Promise<void>,
onChange?: (doc: Doc) => void | Promise<void>,
abort?: AbortSignal,
}){
const watcher = Deno.watchFs(path);
if (abort){
abort.addEventListener("abort", () => {
watcher.close();
});
}
for await (const event of watcher){
if (event.kind === "access" || event.kind === "other" || event.kind === "any"){
continue;
}
if (event.paths.length === 0){
continue;
}
for (const path of event.paths){
const relpath = relative(Deno.cwd(), path);
const filename = basename(relpath);
if (filename === "SUMMARY.md"){
if( event.kind === "remove"){
this.doc_map.delete(relpath);
await onRemove(relpath);
}
else if (event.kind === "create" || event.kind === "modify"){
const {content, metadata} = await readMarkdownDoc(relpath);
const doc = {
path: relpath,
content: content,
attributes: metadata,
};
this.setDoc(doc);
if (event.kind === "create"){
await onAdd(doc);
}
else if (event.kind === "modify"){
await onChange(doc);
}
}
}
}
}
}
makeIndex(options? : {
onUpdate?: (() => void) | (() => Promise<void>),
abort?: AbortSignal,
watch?: boolean,
}){
const opt = options ?? {};
const index = Index.createIndex(this.getDocs());
if (!opt.watch){
return index;
}
const update = async () => {
index.setDocs(this.getDocs());
if (opt.onUpdate){
await opt.onUpdate();
}
}
this.watchDir(".", {
async onAdd(_doc){
await update();
},
async onRemove(_path){
await update();
},
async onChange(_doc){
await update();
},
abort: opt.abort,
});
return index;
}
}

View File

@ -1,60 +1,59 @@
const ICON_MAP: Record<string, string> = { const ICON_MAP: Record<string, string> = {
".pdf": "file-pdf", ".pdf": "file-pdf",
".zip": "file-zip", ".zip": "file-zip",
".rar": "file-zip", ".rar": "file-zip",
".7z": "file-zip", ".7z": "file-zip",
".tar": "file-zip", ".tar": "file-zip",
".gz": "file-zip", ".gz": "file-zip",
".bz2": "file-zip", ".bz2": "file-zip",
".xz": "file-zip", ".xz": "file-zip",
".doc": "file-word", ".doc": "file-word",
".docx": "file-word", ".docx": "file-word",
".xls": "file-excel", ".xls": "file-excel",
".xlsx": "file-excel", ".xlsx": "file-excel",
".ppt": "file-ppt", ".ppt": "file-ppt",
".pptx": "file-ppt", ".pptx": "file-ppt",
".txt": "file-text", ".txt": "file-text",
".md": "filetype-md", ".md": "filetype-md",
".html": "filetype-html", ".html": "filetype-html",
".ts": "file-code", ".ts": "file-code",
".tsx": "filetype-tsx", ".tsx": "filetype-tsx",
".js": "filetype-js", ".js": "filetype-js",
".json": "filetype-json", ".json": "filetype-json",
".jsx": "filetype-jsx", ".jsx": "filetype-jsx",
".css": "filetype-css", ".css": "filetype-css",
".scss": "filetype-scss", ".scss": "filetype-scss",
".csv": "filetype-csv", ".csv": "filetype-csv",
".xml": "filetype-xml", ".xml": "filetype-xml",
".svg": "filetype-svg", ".svg": "filetype-svg",
".mp3": "file-music", ".mp3": "file-music",
".wav": "file-music", ".wav": "file-music",
".ogg": "file-music", ".ogg": "file-music",
".flac": "file-music", ".flac": "file-music",
".mp4": "file-play", ".mp4": "file-play",
".mkv": "file-play", ".mkv": "file-play",
".avi": "file-play", ".avi": "file-play",
".mov": "file-play", ".mov": "file-play",
".wmv": "file-play", ".wmv": "file-play",
".webm": "file-play", ".webm": "file-play",
".mpg": "file-play", ".mpg": "file-play",
".mpeg": "file-play", ".mpeg": "file-play",
".flv": "file-play", ".flv": "file-play",
".m4v": "file-play", ".m4v": "file-play",
".m4a": "file-play", ".m4a": "file-play",
".aac": "file-play", ".aac": "file-play",
".jpg": "file-image", ".jpg": "file-image",
".jpeg": "file-image", ".jpeg": "file-image",
".png": "file-image", ".png": "file-image",
".gif": "file-image", ".gif": "file-image",
".bmp": "file-image", ".bmp": "file-image",
".ico": "file-image", ".ico": "file-image",
".tiff": "file-image", ".tiff": "file-image",
".tif": "file-image", ".tif": "file-image",
".webp": "file-image", ".webp": "file-image",
".psd": "file-image", ".psd": "file-image",
} };
export function extToIcon(s: string): string{ export function extToIcon(s: string): string {
return `/icon/${(ICON_MAP[s] ?? "file")}.svg`; return `/icon/${(ICON_MAP[s] ?? "file")}.svg`;
} }

View File

@ -1,36 +1,41 @@
import {parse as parseYaml, stringify} from "https://deno.land/std@0.170.0/encoding/yaml.ts"; import {
parse as parseYaml,
stringify,
} from "https://deno.land/std@0.170.0/encoding/yaml.ts";
function trimSubstring(str: string, start: number, end: number) { function trimSubstring(str: string, start: number, end: number) {
while (str[start] === ' ' || str[start] === '\t' || str[start] === '\r' || str[start] === '\n') { while (
start++; str[start] === " " || str[start] === "\t" || str[start] === "\r" ||
} str[start] === "\n"
return str.substring(start, end); ) {
start++;
}
return str.substring(start, end);
} }
interface Doc { interface Doc {
metadata: Record<string, string | string[]>; metadata: Record<string, string | string[]>;
content: string; content: string;
} }
export function parse(content: string): Doc { export function parse(content: string): Doc {
if(!content.startsWith('---')){ if (!content.startsWith("---")) {
return {
metadata: {},
content: content,
}
}
const index = content.indexOf('\n---', 3);
const meta = content.substring(3, index);
const c = trimSubstring(content, index + 4, content.length);
const metadata = parseYaml(meta) as Record<string, string | string[]>;
return { return {
metadata: metadata, metadata: {},
content: c, content: content,
} };
}
const index = content.indexOf("\n---", 3);
const meta = content.substring(3, index);
const c = trimSubstring(content, index + 4, content.length);
const metadata = parseYaml(meta) as Record<string, string | string[]>;
return {
metadata: metadata,
content: c,
};
} }
export async function readMarkdownDoc(path: string): Promise<Doc> { export async function readMarkdownDoc(path: string): Promise<Doc> {
const doc = await Deno.readTextFile(path); const doc = await Deno.readTextFile(path);
return parse(doc); return parse(doc);
} }

View File

@ -1,44 +1,43 @@
import { Index } from "../client_search.ts"; import { Index } from "../client_search.ts";
import { Doc, DocCollector } from "../collect.ts"; import { Doc, DocCollector } from "../collect.ts";
export const docCollector = new DocCollector( export const docCollector = new DocCollector(
{ {
dropContent: true, dropContent: true,
summaryOnly: true, summaryOnly: true,
}); },
);
export let docIndex: Index | undefined = undefined; export let docIndex: Index | undefined = undefined;
export async function prepareDocs() { export async function prepareDocs() {
const docPath = Deno.env.get("COLLECT_DOC_PATH"); const docPath = Deno.env.get("COLLECT_DOC_PATH");
if (!docPath) { if (!docPath) {
await docCollector.walkDir("."); await docCollector.walkDir(".");
docIndex = docCollector.makeIndex({ docIndex = docCollector.makeIndex({
watch: true watch: true,
});
return docIndex;
}
try {
const doc_dump = await Deno.readTextFile(docPath);
const docs = JSON.parse(doc_dump) as Doc[];
docCollector.setDocs(docs);
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
await docCollector.walkDir(".");
await Deno.writeTextFile(docPath, JSON.stringify(docCollector.getDocs()));
}
else {
throw error;
}
}
docIndex = docCollector.makeIndex({
watch: true,
onUpdate: async () => {
await Deno.writeTextFile(docPath, JSON.stringify(docCollector.getDocs()));
}
}); });
return docIndex; return docIndex;
}
try {
const doc_dump = await Deno.readTextFile(docPath);
const docs = JSON.parse(doc_dump) as Doc[];
docCollector.setDocs(docs);
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
await docCollector.walkDir(".");
await Deno.writeTextFile(docPath, JSON.stringify(docCollector.getDocs()));
} else {
throw error;
}
}
docIndex = docCollector.makeIndex({
watch: true,
onUpdate: async () => {
await Deno.writeTextFile(docPath, JSON.stringify(docCollector.getDocs()));
},
});
return docIndex;
} }

View File

@ -1,13 +1,13 @@
import {DB} from 'sqlite'; import { DB } from "sqlite";
import {createSchema} from './user.ts'; import { createSchema } from "./user.ts";
export function connectDB(): DB { export function connectDB(): DB {
let DB_path = Deno.env.get("DB_PATH"); let DB_path = Deno.env.get("DB_PATH");
if (DB_path === undefined){ if (DB_path === undefined) {
Deno.env.set("DB_PATH", "./db.sqlite"); Deno.env.set("DB_PATH", "./db.sqlite");
DB_path = "./db.sqlite"; DB_path = "./db.sqlite";
} }
let db = new DB(DB_path); let db = new DB(DB_path);
createSchema(db); createSchema(db);
return db; return db;
} }

View File

@ -1,65 +1,74 @@
import {genSalt, hash, compare} from "bcrypt"; import { compare, genSalt, hash } from "bcrypt";
import { DB } from "sqlite"; import { DB } from "sqlite";
interface User{ interface User {
name: string; name: string;
salted_password: string; salted_password: string;
salt: string; salt: string;
} }
export async function createUser(name: string, password: string){ export async function createUser(name: string, password: string) {
const salt = await genSalt(10); const salt = await genSalt(10);
const salted_password = await hash(password, salt); const salted_password = await hash(password, salt);
const user: User = { const user: User = {
name: name, name: name,
salted_password: salted_password, salted_password: salted_password,
salt: salt salt: salt,
} };
return user; return user;
} }
export async function verifyUser(user: User, password: string){ export async function verifyUser(user: User, password: string) {
return await compare(password, user.salted_password); return await compare(password, user.salted_password);
} }
export async function getAllUsers(db :DB): Promise<User[]>{ export async function getAllUsers(db: DB): Promise<User[]> {
const users = await db.query<[string, string, string]> const users = await db.query<[string, string, string]>(
("SELECT name, salted_password, salt FROM users"); "SELECT name, salted_password, salt FROM users",
return users.map(([name, salted_password, salt])=>({ );
name, return users.map(([name, salted_password, salt]) => ({
salted_password, name,
salt, salted_password,
})); salt,
}));
} }
export async function getUser(db: DB, name: string): Promise<User | undefined>{ export async function getUser(db: DB, name: string): Promise<User | undefined> {
const users = await db.query<[string, string, string]> const users = await db.query<[string, string, string]>(
("SELECT name, salted_password, salt FROM users WHERE name = ?", [name]); "SELECT name, salted_password, salt FROM users WHERE name = ?",
if (users === undefined || users.length === 0){ [name],
return undefined; );
} if (users === undefined || users.length === 0) {
const user = users[0]; return undefined;
return { }
name: user[0], const user = users[0];
salted_password: user[1], return {
salt: user[2], name: user[0],
} salted_password: user[1],
salt: user[2],
};
} }
export async function addUser(db: DB, user: User){ export async function addUser(db: DB, user: User) {
await db.query("INSERT INTO users (name, salted_password, salt) VALUES (?, ?, ?)", await db.query(
[user.name, user.salted_password, user.salt]); "INSERT INTO users (name, salted_password, salt) VALUES (?, ?, ?)",
[user.name, user.salted_password, user.salt],
);
} }
export async function updateUser(db: DB, user: User){ export async function updateUser(db: DB, user: User) {
await db.query("UPDATE users SET salted_password = ?, salt = ? WHERE name = ?", await db.query(
[user.salted_password, user.salt, user.name]); "UPDATE users SET salted_password = ?, salt = ? WHERE name = ?",
[user.salted_password, user.salt, user.name],
);
} }
export async function deleteUser(db: DB, name: string){ export async function deleteUser(db: DB, name: string) {
await db.query("DELETE FROM users WHERE name = ?", [name]); await db.query("DELETE FROM users WHERE name = ?", [name]);
} }
export async function createSchema(db: DB){ export async function createSchema(db: DB) {
await db.query("CREATE TABLE IF NOT EXISTS users (name TEXT PRIMARY KEY, salted_password TEXT, salt TEXT)"); await db.query(
} "CREATE TABLE IF NOT EXISTS users (name TEXT PRIMARY KEY, salted_password TEXT, salt TEXT)",
);
}

View File

@ -1995,4 +1995,4 @@
"sina-weibo": 63690, "sina-weibo": 63690,
"tencent-qq": 63691, "tencent-qq": 63691,
"wikipedia": 63692 "wikipedia": 63692
} }

View File

@ -5,4 +5,4 @@ module.exports = {
extend: {}, extend: {},
}, },
plugins: [], plugins: [],
} };

View File

@ -2,6 +2,7 @@
title: "hello" title: "hello"
tags: ["asdf","wer"] tags: ["asdf","wer"]
--- ---
# hello # hello
- [hello](hello.md) - [hello](hello.md)
@ -9,4 +10,4 @@ tags: ["asdf","wer"]
asdf asdf
File: test_data\d\hello.md File: test_data\d\hello.md

View File

@ -3,8 +3,12 @@ rjcode: RJ130512
title: Summary of the 13th Meeting of the Joint Committee on the Safety of Nuclear Installations title: Summary of the 13th Meeting of the Joint Committee on the Safety of Nuclear Installations
tags: ["summary", "meeting", "joint committee", "safety", "nuclear installations"] tags: ["summary", "meeting", "joint committee", "safety", "nuclear installations"]
--- ---
# Summary of the 13th Meeting of the Joint Committee on the Safety of Nuclear Installations # Summary of the 13th Meeting of the Joint Committee on the Safety of Nuclear Installations
## 1. Opening of the meeting ## 1. Opening of the meeting
The 13th meeting of the Joint Committee on the Safety of Nuclear Installations (JCSNI) was held in Vienna on 12 May 2013. The meeting was chaired by Mr. J. M. Sánchez, Director of the Nuclear Safety Department of the Spanish Ministry of Industry, Energy and Tourism, and the Vice-Chairman of the JCSNI. The 13th meeting of the Joint Committee on the Safety of Nuclear Installations
(JCSNI) was held in Vienna on 12 May 2013. The meeting was chaired by Mr. J. M.
Sánchez, Director of the Nuclear Safety Department of the Spanish Ministry of
Industry, Energy and Tourism, and the Vice-Chairman of the JCSNI.

View File

@ -3,4 +3,4 @@ title: "한글 테스트. 띄어쓰기없이도되나?"
tags: ["한글", "테스트"] tags: ["한글", "테스트"]
--- ---
# 한글 테스트. 띄어쓰기없이도되나? # 한글 테스트. 띄어쓰기없이도되나?

119
user.ts
View File

@ -1,63 +1,66 @@
import { Command, Input, Secret } from "https://deno.land/x/cliffy@v0.25.6/mod.ts"; import {
Command,
Input,
Secret,
} from "https://deno.land/x/cliffy@v0.25.6/mod.ts";
import { connectDB } from "./src/user/db.ts"; import { connectDB } from "./src/user/db.ts";
import * as users from "./src/user/user.ts"; import * as users from "./src/user/user.ts";
export const user_command = new Command() export const user_command = new Command()
.description("Manage users.") .description("Manage users.")
.command("add", "add a user") .command("add", "add a user")
.arguments("[username:string]") .arguments("[username:string]")
.option("-p, --password <password:string>", "The password for the user.") .option("-p, --password <password:string>", "The password for the user.")
.option("-q, --quiet", "quiet output.") .option("-q, --quiet", "quiet output.")
.action(async ({quiet, password} .action(async ({ quiet, password }, username) => {
, username) => { if (username === undefined) {
if(username === undefined){ username = await Input.prompt("Username: ");
username = await Input.prompt("Username: "); }
} if (password === undefined) {
if(password === undefined){ password = await Secret.prompt("Password: ");
password = await Secret.prompt("Password: "); const password2 = await Secret.prompt("Confirm password: ");
const password2 = await Secret.prompt("Confirm password: "); if (password !== password2) {
if (password !== password2){ console.error("Passwords do not match.");
console.error("Passwords do not match."); Deno.exit(1);
Deno.exit(1); }
} }
} const db = connectDB();
const db = connectDB(); const new_user = await users.createUser(username, password);
const new_user = await users.createUser( username, password); await users.addUser(db, new_user);
await users.addUser(db, new_user); if (!quiet) {
if (!quiet){ console.log(`Added user ${username}`);
console.log(`Added user ${username}`); }
} })
}) .command("delete", "delete a user")
.command("delete", "delete a user") .arguments("<username:string>")
.arguments("<username:string>") .option("-q, --quiet", "Quiet output.")
.option("-q, --quiet", "Quiet output.") .action(async ({ quiet }, username) => {
.action(async ({quiet}, username) => { const db = connectDB();
const db = connectDB(); await users.deleteUser(db, username);
await users.deleteUser(db, username); if (!quiet) {
if (!quiet){ console.log(`Deleting user ${username}`);
console.log(`Deleting user ${username}`); }
} })
}) .command("list", "list all users")
.command("list", "list all users") .action(async () => {
.action(async () => { const db = connectDB();
const db = connectDB(); const all_users = await users.getAllUsers(db);
const all_users = await users.getAllUsers(db); for (const user of all_users) {
for (const user of all_users){ console.log(`${user.name}`);
console.log(`${user.name}`); }
} })
}) .command("reset", "reset a user's password")
.command("reset", "reset a user's password") .arguments("<username:string> <password:string>")
.arguments("<username:string> <password:string>") .option("-q, --quiet", "quiet output.")
.option("-q, --quiet", "quiet output.") .action(async ({ quiet }, [username, password]) => {
.action(async ({quiet}, [username, password]) => { const db = connectDB();
const db = connectDB(); const new_user = await users.createUser(username, password);
const new_user = await users.createUser( username, password); await users.updateUser(db, new_user);
await users.updateUser(db, new_user); if (!quiet) {
if (!quiet){ console.log(`Resetting password for user ${username}`);
console.log(`Resetting password for user ${username}`); }
} });
});
if (import.meta.main){ if (import.meta.main) {
await user_command.parse(Deno.args); await user_command.parse(Deno.args);
} }

View File

@ -1,28 +1,31 @@
export async function generateSecretKey() { export async function generateSecretKey() {
const key = await crypto.subtle.generateKey( const key = await crypto.subtle.generateKey(
{ name: "HMAC", hash: "SHA-512" },
true,
["sign", "verify"],
);
return key;
}
export async function prepareSecretKey() {
const key = Deno.env.get("SECRET_KEY");
if (key) {
const jwk = JSON.parse(key) as JsonWebKey;
{
const key = await crypto.subtle.importKey(
"jwk",
jwk,
{ name: "HMAC", hash: "SHA-512" }, { name: "HMAC", hash: "SHA-512" },
true, true,
["sign", "verify"], ["sign", "verify"],
); );
return key;
}
} else {
const key = await generateSecretKey();
const out = await crypto.subtle.exportKey("jwk", key);
Deno.env.set("SECRET_KEY", JSON.stringify(out));
return key; return key;
}
} }
export async function prepareSecretKey(){
const key = Deno.env.get("SECRET_KEY");
if (key){
const jwk = JSON.parse(key) as JsonWebKey;
{
const key = await crypto.subtle.importKey("jwk", jwk,
{ name: "HMAC", hash: "SHA-512" }, true, ["sign", "verify"]);
return key;
}
}
else {
const key = await generateSecretKey();
const out = await crypto.subtle.exportKey("jwk", key);
Deno.env.set("SECRET_KEY", JSON.stringify(out));
return key;
}
}

View File

@ -1,15 +1,18 @@
export function removePrefixFromPathname(pathname: string, prefix: string): string { export function removePrefixFromPathname(
let ret = pathname; pathname: string,
ret = ret.slice(prefix.length); prefix: string,
if (ret.startsWith("/")) { ): string {
ret = ret.slice(1); let ret = pathname;
} ret = ret.slice(prefix.length);
if (ret === "") { if (ret.startsWith("/")) {
ret = "."; ret = ret.slice(1);
} }
return ret; if (ret === "") {
ret = ".";
}
return ret;
} }
export function encodePath(path: string): string { export function encodePath(path: string): string {
return path.split("/").map(encodeURIComponent).join("/"); return path.split("/").map(encodeURIComponent).join("/");
} }