Compare commits
2 Commits
d959076d28
...
312964f22c
Author | SHA1 | Date | |
---|---|---|---|
312964f22c | |||
8b3db1e2f1 |
@ -23,4 +23,4 @@ Start the project:
|
||||
deno task start
|
||||
```
|
||||
|
||||
This will watch the project directory and restart as necessary.
|
||||
This will watch the project directory and restart as necessary.
|
||||
|
@ -3,63 +3,67 @@ import MarkdownRenderer from "./MarkdownRenderer.tsx";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
const TypeToExt = {
|
||||
"image": new Set([".png", ".jpg", ".jpeg", ".gif", ".svg"]),
|
||||
"video": new Set([".mp4", ".webm", ".ogg"]),
|
||||
"audio": new Set([".mp3", ".wav", ".flac"]),
|
||||
"md": new Set([".md"]),
|
||||
"text": new Set([".txt"]),
|
||||
"code": new Set([".json", ".js", ".ts", ".css", ".tsx", ".jsx"]),
|
||||
}
|
||||
"image": new Set([".png", ".jpg", ".jpeg", ".gif", ".svg"]),
|
||||
"video": new Set([".mp4", ".webm", ".ogg"]),
|
||||
"audio": new Set([".mp3", ".wav", ".flac"]),
|
||||
"md": new Set([".md"]),
|
||||
"text": new Set([".txt"]),
|
||||
"code": new Set([".json", ".js", ".ts", ".css", ".tsx", ".jsx"]),
|
||||
};
|
||||
|
||||
function extToType(ext: string) {
|
||||
for (const [type, exts] of Object.entries(TypeToExt)) {
|
||||
if (exts.has(ext)) {
|
||||
return type;
|
||||
}
|
||||
for (const [type, exts] of Object.entries(TypeToExt)) {
|
||||
if (exts.has(ext)) {
|
||||
return type;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function FetchAndRender(props: {src: string, type: string}){
|
||||
const src = props.src;
|
||||
const ext = extname(src);
|
||||
const [content, setContent] = useState("");
|
||||
useEffect(() => {
|
||||
fetch(src).then(res => res.text()).then(setContent);
|
||||
}, [src]);
|
||||
switch (props.type) {
|
||||
case "text":
|
||||
return <div style={{ whiteSpace: "pre-wrap" }}>{content}</div>;
|
||||
case "md":
|
||||
return <MarkdownRenderer text={content} />;
|
||||
case "code":
|
||||
return <pre style={{ whiteSpace: "pre-wrap" }}><code lang={ext.slice(1)}>{content}</code></pre>;
|
||||
//case "csv":
|
||||
// return <CsvRenderer content={content} />;
|
||||
default:
|
||||
return <>error: invalid type: {props.type} content: {content}</>;
|
||||
}
|
||||
function FetchAndRender(props: { src: string; type: string }) {
|
||||
const src = props.src;
|
||||
const ext = extname(src);
|
||||
const [content, setContent] = useState("");
|
||||
useEffect(() => {
|
||||
fetch(src).then((res) => res.text()).then(setContent);
|
||||
}, [src]);
|
||||
switch (props.type) {
|
||||
case "text":
|
||||
return <div style={{ whiteSpace: "pre-wrap" }}>{content}</div>;
|
||||
case "md":
|
||||
return <MarkdownRenderer text={content} />;
|
||||
case "code":
|
||||
return (
|
||||
<pre
|
||||
style={{ whiteSpace: "pre-wrap" }}
|
||||
><code lang={ext.slice(1)}>{content}</code></pre>
|
||||
);
|
||||
//case "csv":
|
||||
// return <CsvRenderer content={content} />;
|
||||
default:
|
||||
return <>error: invalid type: {props.type} content: {content}</>;
|
||||
}
|
||||
}
|
||||
|
||||
export function RenderView(props: {src: string}) {
|
||||
const src = props.src;
|
||||
const type = extToType(extname(src));
|
||||
switch (type) {
|
||||
case "text":
|
||||
case "md":
|
||||
case "code":
|
||||
return <FetchAndRender src={src} type={type}></FetchAndRender>
|
||||
//case "csv":
|
||||
// return <CsvRenderer content={content} />;
|
||||
case "image":
|
||||
return <img style={{ width: "100%" }} src={src} />;
|
||||
case "video":
|
||||
return <video style={{ width: "100%" }} controls src={src} />;
|
||||
case "audio":
|
||||
return <audio style={{ width: "100%" }} controls src={src} />;
|
||||
default:
|
||||
return <>error: invalid type: {type} src: {src}</>;
|
||||
}
|
||||
export function RenderView(props: { src: string }) {
|
||||
const src = props.src;
|
||||
const type = extToType(extname(src));
|
||||
switch (type) {
|
||||
case "text":
|
||||
case "md":
|
||||
case "code":
|
||||
return <FetchAndRender src={src} type={type}></FetchAndRender>;
|
||||
//case "csv":
|
||||
// return <CsvRenderer content={content} />;
|
||||
case "image":
|
||||
return <img style={{ width: "100%" }} src={src} />;
|
||||
case "video":
|
||||
return <video style={{ width: "100%" }} controls src={src} />;
|
||||
case "audio":
|
||||
return <audio style={{ width: "100%" }} controls src={src} />;
|
||||
default:
|
||||
return <>error: invalid type: {type} src: {src}</>;
|
||||
}
|
||||
}
|
||||
|
||||
export default RenderView;
|
||||
|
@ -1,94 +1,108 @@
|
||||
import { Head, asset } from "$fresh/runtime.ts";
|
||||
import { asset, Head } from "$fresh/runtime.ts";
|
||||
import { useState } from "preact/hooks";
|
||||
import { extname, join } from "path/posix.ts";
|
||||
import { ComponentChild } from "preact";
|
||||
import UpList from "./UpList.tsx";
|
||||
import {extToIcon} from "../src/media.ts";
|
||||
import { extToIcon } from "../src/media.ts";
|
||||
import { encodePath } from "../util/util.ts";
|
||||
|
||||
function ListItem(props:{
|
||||
href: string,
|
||||
icon: string,
|
||||
children: ComponentChild
|
||||
}) {
|
||||
return (
|
||||
<li class="p-1 hover:bg-gray-400 transition-colors">
|
||||
<a class="flex gap-2" href={props.href}>
|
||||
<img src={asset(props.icon)}/><p class="">{props.children}</p></a>
|
||||
</li>
|
||||
);
|
||||
function ListItem(props: {
|
||||
href: string;
|
||||
icon: string;
|
||||
children: ComponentChild;
|
||||
}) {
|
||||
return (
|
||||
<li class="p-1 hover:bg-gray-400 transition-colors">
|
||||
<a class="flex gap-2" href={props.href}>
|
||||
<img src={asset(props.icon)} />
|
||||
<p class="">{props.children}</p>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export interface EntryInfo{
|
||||
name: string;
|
||||
isFile: boolean;
|
||||
isDirectory: boolean;
|
||||
isSymlink: boolean;
|
||||
size: number;
|
||||
lastModified?: Date;
|
||||
export interface EntryInfo {
|
||||
name: string;
|
||||
isFile: boolean;
|
||||
isDirectory: boolean;
|
||||
isSymlink: boolean;
|
||||
size: number;
|
||||
lastModified?: Date;
|
||||
}
|
||||
|
||||
interface DirListProps {
|
||||
path: string;
|
||||
files: EntryInfo[];
|
||||
path: string;
|
||||
files: EntryInfo[];
|
||||
}
|
||||
|
||||
export function DirList(props: DirListProps) {
|
||||
const data = props;
|
||||
const [files, setFiles] = useState(data.files);
|
||||
const data = props;
|
||||
const [files, setFiles] = useState(data.files);
|
||||
|
||||
return (<div>
|
||||
<UpList path={data.path}></UpList>
|
||||
<ul class="border-2 rounded-md">
|
||||
<li class="p-1 flex gap-2">
|
||||
<button class="flex" onClick={sortDir}>
|
||||
<img src={asset("/icon/sort-down.svg")}/> Sort Directory
|
||||
</button>
|
||||
<button class="flex" onClick={sortAlpha}>
|
||||
<img src={asset("/icon/sort-alpha-down.svg")} /> Sort Alphabet
|
||||
</button>
|
||||
</li>
|
||||
<ListItem key=".." href={`/dir/${encodePath(join(data.path,".."))}`}
|
||||
icon="/icon/back.svg"
|
||||
>...</ListItem>
|
||||
{files.map((file) => (
|
||||
<ListItem key={file.name} href={`/dir/${encodePath(join(data.path,file.name))}`}
|
||||
icon={file.isDirectory ? "/icon/folder.svg": extToIcon(extname(file.name))}
|
||||
>{file.name}</ListItem>
|
||||
))}
|
||||
</ul>
|
||||
</div>)
|
||||
function sortDir() {
|
||||
// sort by directory first then by index
|
||||
const sorted_files = files.map((x,i)=>
|
||||
([x,i] as [EntryInfo, number]))
|
||||
.sort(
|
||||
([a, ai],[b,bi]) => {
|
||||
if (a.isDirectory && !b.isDirectory) {
|
||||
return -1;
|
||||
} else if (!a.isDirectory && b.isDirectory) {
|
||||
return 1;
|
||||
} else {
|
||||
return ai - bi;
|
||||
}
|
||||
});
|
||||
setFiles(sorted_files.map(([x,_])=>x));
|
||||
}
|
||||
function sortAlpha() {
|
||||
// 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));
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<UpList path={data.path}></UpList>
|
||||
<ul class="border-2 rounded-md">
|
||||
<li class="p-1 flex gap-2">
|
||||
<button class="flex" onClick={sortDir}>
|
||||
<img src={asset("/icon/sort-down.svg")} /> Sort Directory
|
||||
</button>
|
||||
<button class="flex" onClick={sortAlpha}>
|
||||
<img src={asset("/icon/sort-alpha-down.svg")} /> Sort Alphabet
|
||||
</button>
|
||||
</li>
|
||||
<ListItem
|
||||
key=".."
|
||||
href={`/dir/${encodePath(join(data.path, ".."))}`}
|
||||
icon="/icon/back.svg"
|
||||
>
|
||||
...
|
||||
</ListItem>
|
||||
{files.map((file) => (
|
||||
<ListItem
|
||||
key={file.name}
|
||||
href={`/dir/${encodePath(join(data.path, file.name))}`}
|
||||
icon={file.isDirectory
|
||||
? "/icon/folder.svg"
|
||||
: extToIcon(extname(file.name))}
|
||||
>
|
||||
{file.name}
|
||||
</ListItem>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
function sortDir() {
|
||||
// sort by directory first then by index
|
||||
const sorted_files = files.map((x, i) => ([x, i] as [EntryInfo, number]))
|
||||
.sort(
|
||||
([a, ai], [b, bi]) => {
|
||||
if (a.isDirectory && !b.isDirectory) {
|
||||
return -1;
|
||||
} else if (!a.isDirectory && b.isDirectory) {
|
||||
return 1;
|
||||
} else {
|
||||
return ai - bi;
|
||||
}
|
||||
},
|
||||
);
|
||||
setFiles(sorted_files.map(([x, _]) => x));
|
||||
}
|
||||
function sortAlpha() {
|
||||
// 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;
|
||||
|
@ -3,43 +3,52 @@ import { useEffect, useState } from "preact/hooks";
|
||||
import { Index } from "../src/client_search.ts";
|
||||
import { encodePath } from "../util/util.ts";
|
||||
|
||||
function SearchBar(props:{
|
||||
search?: string;
|
||||
onSearch?: (search: string) => void;
|
||||
}){
|
||||
const [search, setSearch] = useState(props.search ?? "");
|
||||
return (
|
||||
<div class="flex items-center justify-center">
|
||||
<input type="text" class="w-1/2 px-4 py-2 border-2 rounded-lg" placeholder="Search..."
|
||||
//onChange={(event)=>{}}
|
||||
onKeyUp={(event)=>{
|
||||
if (event.currentTarget.value === search) return;
|
||||
setSearch(event.currentTarget.value);
|
||||
props.onSearch?.(event.currentTarget.value);
|
||||
}}>{search}</input>
|
||||
</div>
|
||||
)
|
||||
function SearchBar(props: {
|
||||
search?: string;
|
||||
onSearch?: (search: string) => void;
|
||||
}) {
|
||||
const [search, setSearch] = useState(props.search ?? "");
|
||||
return (
|
||||
<div class="flex items-center justify-center">
|
||||
<input
|
||||
type="text"
|
||||
class="w-1/2 px-4 py-2 border-2 rounded-lg"
|
||||
placeholder="Search..."
|
||||
//onChange={(event)=>{}}
|
||||
onKeyUp={(event) => {
|
||||
if (event.currentTarget.value === search) return;
|
||||
setSearch(event.currentTarget.value);
|
||||
props.onSearch?.(event.currentTarget.value);
|
||||
}}
|
||||
>
|
||||
{search}
|
||||
</input>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DocSearch(props: {
|
||||
docs: Doc[]
|
||||
}){
|
||||
const [docs, setDocs] = useState(props.docs);
|
||||
const index = Index.createIndex(props.docs);
|
||||
docs: Doc[];
|
||||
}) {
|
||||
const [docs, setDocs] = useState(props.docs);
|
||||
const index = Index.createIndex(props.docs);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchBar onSearch={(s)=>{
|
||||
setDocs(index.search(s));
|
||||
}}></SearchBar>
|
||||
<h1 class="text-2xl font-bold">Doc</h1>
|
||||
<ul class="mt-4">
|
||||
{docs.map((doc) => (
|
||||
<li class="mt-2">
|
||||
<a href={`/dir/${encodePath(doc.path)}`}> {doc.path}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<SearchBar
|
||||
onSearch={(s) => {
|
||||
setDocs(index.search(s));
|
||||
}}
|
||||
>
|
||||
</SearchBar>
|
||||
<h1 class="text-2xl font-bold">Doc</h1>
|
||||
<ul class="mt-4">
|
||||
{docs.map((doc) => (
|
||||
<li class="mt-2">
|
||||
<a href={`/dir/${encodePath(doc.path)}`}>{doc.path}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -4,15 +4,15 @@ import { extname } from "path/mod.ts";
|
||||
import { encodePath } from "../util/util.ts";
|
||||
|
||||
export default function FileViewer(props: { path: string }) {
|
||||
const { path } = props;
|
||||
const srcPath = `/fs/${encodePath(path)}`;
|
||||
return (
|
||||
<div class="p-4 mx-auto max-w-screen-md">
|
||||
<UpList path={path} />
|
||||
<a href={srcPath}>Direct link</a>
|
||||
<div class="p-2 border-2 rounded-lg">
|
||||
<RenderView src={srcPath}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const { path } = props;
|
||||
const srcPath = `/fs/${encodePath(path)}`;
|
||||
return (
|
||||
<div class="p-4 mx-auto max-w-screen-md">
|
||||
<UpList path={path} />
|
||||
<a href={srcPath}>Direct link</a>
|
||||
<div class="p-2 border-2 rounded-lg">
|
||||
<RenderView src={srcPath} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,13 +1,18 @@
|
||||
import { marked } from "https://deno.land/x/marked@1.0.1/mod.ts";
|
||||
|
||||
export function MarkdownRenderer(props: { text: string | undefined }) {
|
||||
let text = props.text;
|
||||
if (text === undefined) {
|
||||
text = "";
|
||||
}
|
||||
const index = text.indexOf('\n---', 3);
|
||||
const c = text.slice(index + 4, text.length);
|
||||
return <div class="markdown-body" dangerouslySetInnerHTML={{ __html: marked.parse(c) }} />;
|
||||
let text = props.text;
|
||||
if (text === undefined) {
|
||||
text = "";
|
||||
}
|
||||
const index = text.indexOf("\n---", 3);
|
||||
const c = text.slice(index + 4, text.length);
|
||||
return (
|
||||
<div
|
||||
class="markdown-body"
|
||||
dangerouslySetInnerHTML={{ __html: marked.parse(c) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default MarkdownRenderer;
|
||||
|
@ -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 { ComponentChild } from "preact";
|
||||
import { encodePath } from "../util/util.ts";
|
||||
|
||||
|
||||
function stairs(path: string){
|
||||
if (path === ".") return [];
|
||||
const uplist = path.split("/");
|
||||
let current = ".";
|
||||
const stairs = [];
|
||||
for (const up of uplist){
|
||||
current = join(current, up);
|
||||
stairs.push([current, up]);
|
||||
}
|
||||
return stairs;
|
||||
function stairs(path: string) {
|
||||
if (path === ".") return [];
|
||||
const uplist = path.split("/");
|
||||
let current = ".";
|
||||
const stairs = [];
|
||||
for (const up of uplist) {
|
||||
current = join(current, up);
|
||||
stairs.push([current, up]);
|
||||
}
|
||||
return stairs;
|
||||
}
|
||||
|
||||
export default function UpList(props:{path: string}) {
|
||||
const data = props;
|
||||
const uplist = stairs(data.path);
|
||||
|
||||
return (<div>
|
||||
<h1 class={"text-2xl flex flex-wrap"}>
|
||||
<a href="/dir/" class="flex flex-wrap p-2 hover:bg-gray-400 rounded-sm">
|
||||
<img src={asset("/icon/house.svg")}/>
|
||||
<span class="ml-1">Home</span></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)}`}>
|
||||
<img src={asset("/icon/folder.svg")} />
|
||||
<span class="ml-1">{up}</span>
|
||||
</a>
|
||||
</>
|
||||
))
|
||||
}</h1>
|
||||
</div>)
|
||||
}
|
||||
export default function UpList(props: { path: string }) {
|
||||
const data = props;
|
||||
const uplist = stairs(data.path);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 class={"text-2xl flex flex-wrap"}>
|
||||
<a href="/dir/" class="flex flex-wrap p-2 hover:bg-gray-400 rounded-sm">
|
||||
<img src={asset("/icon/house.svg")} />
|
||||
<span class="ml-1">Home</span>
|
||||
</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)}`}
|
||||
>
|
||||
<img src={asset("/icon/folder.svg")} />
|
||||
<span class="ml-1">{up}</span>
|
||||
</a>
|
||||
</>
|
||||
))}
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
29
keyout.ts
29
keyout.ts
@ -4,24 +4,27 @@ import { prepareSecretKey } from "./util/secret.ts";
|
||||
|
||||
export const key_out_cmd = new Command();
|
||||
key_out_cmd.name("keyout")
|
||||
.description("Output the secret key.")
|
||||
.option("-f, --file <file:string>", "The file to output the key to. (default: stdout)")
|
||||
.option("--dotenv", "Output the key in dotenv format.")
|
||||
.action(async ({file, dotenv}) => {
|
||||
.description("Output the secret key.")
|
||||
.option(
|
||||
"-f, --file <file:string>",
|
||||
"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 keyout = await crypto.subtle.exportKey("jwk", key);
|
||||
let out = JSON.stringify(keyout);
|
||||
if (dotenv){
|
||||
out = [`SECRET_KEY='${out}'`].join("\n");
|
||||
if (dotenv) {
|
||||
out = [`SECRET_KEY='${out}'`].join("\n");
|
||||
}
|
||||
|
||||
if (file){
|
||||
await Deno.writeTextFile(file, out);
|
||||
if (file) {
|
||||
await Deno.writeTextFile(file, out);
|
||||
} else {
|
||||
console.log(out);
|
||||
console.log(out);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if ( import.meta.main ){
|
||||
await key_out_cmd.parse(Deno.args);
|
||||
}
|
||||
if (import.meta.main) {
|
||||
await key_out_cmd.parse(Deno.args);
|
||||
}
|
||||
|
121
main.ts
121
main.ts
@ -4,14 +4,19 @@
|
||||
/// <reference lib="dom.asynciterable" />
|
||||
/// <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 twindPlugin from "$fresh/plugins/twind.ts";
|
||||
import twindConfig from "./twind.config.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 { fromFileUrl, join } from "path/mod.ts";
|
||||
import { prepareSecretKey } from "./util/secret.ts";
|
||||
@ -19,72 +24,84 @@ import { serve } from "http/server.ts";
|
||||
|
||||
import { user_command } from "./user.ts";
|
||||
import { key_out_cmd } from "./keyout.ts";
|
||||
import { prepareDocs } from "./src/store/doc.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 = {
|
||||
name:"css plugin",
|
||||
// deno-lint-ignore require-await
|
||||
render(ctx): PluginRenderResult{
|
||||
ctx.render();
|
||||
return {
|
||||
styles: [{
|
||||
cssText: github_markdown,
|
||||
}]
|
||||
}
|
||||
}
|
||||
name: "css plugin",
|
||||
// deno-lint-ignore require-await
|
||||
render(ctx): PluginRenderResult {
|
||||
ctx.render();
|
||||
return {
|
||||
styles: [{
|
||||
cssText: github_markdown,
|
||||
}],
|
||||
};
|
||||
},
|
||||
};
|
||||
await prepareSecretKey();
|
||||
|
||||
async function startServer(manifest: Manifest, options: StartOptions = {}){
|
||||
const ctx = await ServerContext.fromManifest(manifest, options);
|
||||
|
||||
options.port ??= 8000;
|
||||
if (options.experimentalDenoServe === true) {
|
||||
// @ts-ignore as `Deno.serve` is still unstable.
|
||||
await Deno.serve(ctx.handler() as Deno.ServeHandler, options);
|
||||
} else {
|
||||
await serve(ctx.handler(), options);
|
||||
}
|
||||
async function startServer(manifest: Manifest, options: StartOptions = {}) {
|
||||
const ctx = await ServerContext.fromManifest(manifest, options);
|
||||
|
||||
options.port ??= 8000;
|
||||
if (options.experimentalDenoServe === true) {
|
||||
// @ts-ignore as `Deno.serve` is still unstable.
|
||||
await Deno.serve(ctx.handler() as Deno.ServeHandler, options);
|
||||
} else {
|
||||
await serve(ctx.handler(), options);
|
||||
}
|
||||
}
|
||||
|
||||
async function start({port = 8000, hostname = "localhost"}: {port?: number, hostname?: string} = {}){
|
||||
await startServer(manifest, { plugins: [twindPlugin(twindConfig), CSSPlugin],
|
||||
port: port,
|
||||
hostname: hostname,
|
||||
});
|
||||
async function start(
|
||||
{ port = 8000, hostname = "localhost" }: {
|
||||
port?: number;
|
||||
hostname?: string;
|
||||
} = {},
|
||||
) {
|
||||
await startServer(manifest, {
|
||||
plugins: [twindPlugin(twindConfig), CSSPlugin],
|
||||
port: port,
|
||||
hostname: hostname,
|
||||
});
|
||||
}
|
||||
|
||||
if (import.meta.main){
|
||||
const cmd = new Command();
|
||||
cmd.name("fs-server")
|
||||
.description("A simple file server that supports search, upload, and download.")
|
||||
if (import.meta.main) {
|
||||
const cmd = new Command();
|
||||
cmd.name("fs-server")
|
||||
.description(
|
||||
"A simple file server that supports search, upload, and download.",
|
||||
)
|
||||
.version("0.0.1")
|
||||
.globalOption("-d, --debug", "Enable debug mode.")
|
||||
.command("start", "Start the server.")
|
||||
.option("-p, --port <port:number>", "The port to listen on.",
|
||||
{ default: 8000 })
|
||||
.option("-p, --port <port:number>", "The port to listen on.", {
|
||||
default: 8000,
|
||||
})
|
||||
.option("--auth", "Enable authentication.")
|
||||
.arguments("[hostname:string]")
|
||||
.action(async ({debug, port, auth }, hostname) => {
|
||||
hostname ??= "localhost";
|
||||
if (auth){
|
||||
Deno.env.set("AUTH_REQUIRED", "true");
|
||||
}
|
||||
if (debug){
|
||||
console.log("Debug mode enabled.");
|
||||
}
|
||||
await start({
|
||||
port: port,
|
||||
hostname: hostname,
|
||||
});
|
||||
}
|
||||
)
|
||||
.action(async ({ debug, port, auth }, hostname) => {
|
||||
hostname ??= "localhost";
|
||||
if (auth) {
|
||||
Deno.env.set("AUTH_REQUIRED", "true");
|
||||
}
|
||||
if (debug) {
|
||||
console.log("Debug mode enabled.");
|
||||
}
|
||||
await prepareDocs();
|
||||
await start({
|
||||
port: port,
|
||||
hostname: hostname,
|
||||
});
|
||||
})
|
||||
.command("user", user_command)
|
||||
.command("keyout", key_out_cmd);
|
||||
|
||||
await cmd.parse(Deno.args);
|
||||
await cmd.parse(Deno.args);
|
||||
} else {
|
||||
await prepareDocs();
|
||||
await start();
|
||||
}
|
||||
else {
|
||||
await start();
|
||||
}
|
@ -1,20 +1,21 @@
|
||||
import { MiddlewareHandlerContext } from "$fresh/server.ts";
|
||||
import { getCookies } from "http/cookie.ts";
|
||||
import { decode, verify } from "djwt";
|
||||
import { verify } from "djwt";
|
||||
import { prepareSecretKey } from "../util/secret.ts";
|
||||
|
||||
const secret_key = await prepareSecretKey();
|
||||
|
||||
export const handler =
|
||||
async (req: Request , ctx: MiddlewareHandlerContext<Record<string, unknown>>) => {
|
||||
const cookies = getCookies(req.headers);
|
||||
const jwt = cookies["auth"];
|
||||
try{
|
||||
const payload = await verify(jwt, secret_key);
|
||||
ctx.state["login"] = payload;
|
||||
}
|
||||
catch (e){
|
||||
ctx.state["login"] = null;
|
||||
}
|
||||
return await ctx.next();
|
||||
}
|
||||
export const handler = async (
|
||||
req: Request,
|
||||
ctx: MiddlewareHandlerContext<Record<string, unknown>>,
|
||||
) => {
|
||||
const cookies = getCookies(req.headers);
|
||||
const jwt = cookies["auth"];
|
||||
try {
|
||||
const payload = await verify(jwt, secret_key);
|
||||
ctx.state["login"] = payload;
|
||||
} catch (e) {
|
||||
ctx.state["login"] = null;
|
||||
}
|
||||
return await ctx.next();
|
||||
};
|
||||
|
@ -6,44 +6,44 @@ import { getUser, verifyUser } from "../../src/user/user.ts";
|
||||
import { create as createJWT } from "djwt";
|
||||
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> {
|
||||
const url = new URL(req.url);
|
||||
const form = await req.formData();
|
||||
const username = form.get("username");
|
||||
const password = form.get("password");
|
||||
if (username && password){
|
||||
const DB = connectDB();
|
||||
const user = await getUser(DB, username.toString());
|
||||
if (user){
|
||||
if (await verifyUser(user, password.toString())){
|
||||
const headers = new Headers();
|
||||
const jwt = await createJWT({alg:"HS512", typ: "JWT"},{
|
||||
username: user.name
|
||||
}, SECRET_KEY);
|
||||
setCookie(headers, {
|
||||
name: "auth",
|
||||
value: jwt,
|
||||
httpOnly: true,
|
||||
sameSite: "Strict",
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
domain: url.hostname,
|
||||
path: "/",
|
||||
secure: url.protocol === "https:"
|
||||
});
|
||||
const url = new URL(req.url);
|
||||
const form = await req.formData();
|
||||
const username = form.get("username");
|
||||
const password = form.get("password");
|
||||
if (username && password) {
|
||||
const DB = connectDB();
|
||||
const user = await getUser(DB, username.toString());
|
||||
if (user) {
|
||||
if (await verifyUser(user, password.toString())) {
|
||||
const headers = new Headers();
|
||||
const jwt = await createJWT({ alg: "HS512", typ: "JWT" }, {
|
||||
username: user.name,
|
||||
}, SECRET_KEY);
|
||||
setCookie(headers, {
|
||||
name: "auth",
|
||||
value: jwt,
|
||||
httpOnly: true,
|
||||
sameSite: "Strict",
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
domain: url.hostname,
|
||||
path: "/",
|
||||
secure: url.protocol === "https:",
|
||||
});
|
||||
|
||||
headers.set("Location", "/");
|
||||
return new Response(null,{
|
||||
status: Status.SeeOther, // See Other
|
||||
headers: headers
|
||||
});
|
||||
}
|
||||
}
|
||||
headers.set("Location", "/");
|
||||
return new Response(null, {
|
||||
status: Status.SeeOther, // See Other
|
||||
headers: headers,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(`<!DOCTYPE html><html>
|
||||
return new Response(
|
||||
`<!DOCTYPE html><html>
|
||||
<head> <title> Login Failed </title> </head>
|
||||
<body>
|
||||
<h1> Login Failed </h1>
|
||||
@ -52,14 +52,16 @@ async function POST(req: Request, ctx: HandlerContext): Promise<Response> {
|
||||
document.location.href = "/login";
|
||||
</script>
|
||||
</body>
|
||||
</html>`, {
|
||||
headers:{
|
||||
"Content-Type": "text/html"
|
||||
},
|
||||
status: Status.Forbidden,
|
||||
});
|
||||
</html>`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "text/html",
|
||||
},
|
||||
status: Status.Forbidden,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export const handler = {
|
||||
POST
|
||||
};
|
||||
POST,
|
||||
};
|
||||
|
@ -14,4 +14,4 @@ export const handler: Handlers = {
|
||||
headers,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { PageProps, Handlers, HandlerContext } from "$fresh/server.ts";
|
||||
import { Head, asset } from "$fresh/runtime.ts";
|
||||
import {removePrefixFromPathname} from "../../util/util.ts";
|
||||
import { HandlerContext, Handlers, PageProps } from "$fresh/server.ts";
|
||||
import { asset, Head } from "$fresh/runtime.ts";
|
||||
import { removePrefixFromPathname } from "../../util/util.ts";
|
||||
import { join } from "path/posix.ts";
|
||||
import DirList, { EntryInfo } from "../../islands/DirList.tsx";
|
||||
import FileViewer from "../../islands/FileViewer.tsx";
|
||||
@ -10,74 +10,78 @@ type DirProps = {
|
||||
path: string;
|
||||
stat: Deno.FileInfo;
|
||||
files: EntryInfo[];
|
||||
}
|
||||
};
|
||||
type FileProps = {
|
||||
type: "file";
|
||||
path: string;
|
||||
stat: Deno.FileInfo;
|
||||
}
|
||||
};
|
||||
|
||||
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";
|
||||
if (authRequired) {
|
||||
const login = ctx.state["login"];
|
||||
if (!login) {
|
||||
return new Response(null, {
|
||||
status: 401,
|
||||
headers: {
|
||||
"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"
|
||||
}
|
||||
});
|
||||
}
|
||||
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 url = new URL(req.url);
|
||||
const path = removePrefixFromPathname(decodeURI(url.pathname), "/dir");
|
||||
const stat = await Deno.stat(path);
|
||||
if (stat.isDirectory){
|
||||
if (stat.isDirectory) {
|
||||
const filesIter = await Deno.readDir(path);
|
||||
const files: EntryInfo[] = []
|
||||
for await (const file of filesIter){
|
||||
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
|
||||
size: fileStat.size,
|
||||
});
|
||||
}
|
||||
return await ctx.render({
|
||||
type: "dir",
|
||||
stat,
|
||||
files
|
||||
, path
|
||||
})
|
||||
}
|
||||
else{
|
||||
stat,
|
||||
files,
|
||||
path,
|
||||
});
|
||||
} else {
|
||||
return await ctx.render({
|
||||
type: "file",
|
||||
stat, path
|
||||
stat,
|
||||
path,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const handler: Handlers = {
|
||||
GET
|
||||
}
|
||||
GET,
|
||||
};
|
||||
|
||||
export default function DirLists(props: PageProps<DirOrFileProps>) {
|
||||
const data = props.data;
|
||||
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>)}
|
||||
</div>
|
||||
</>
|
||||
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>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,20 +1,37 @@
|
||||
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 { Doc } from "../../src/collect.ts";
|
||||
import { docCollector } from "../../src/store/doc.ts";
|
||||
|
||||
async function GET(req: Request, ctx: HandlerContext): Promise<Response> {
|
||||
const docs = docCollector.getDocs();
|
||||
return await ctx.render({docs});
|
||||
const authRequired = Deno.env.get("AUTH_REQUIRED") === "true";
|
||||
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 = {
|
||||
GET,
|
||||
}
|
||||
GET,
|
||||
};
|
||||
|
||||
export default function Docs(props: PageProps<{docs:Doc[]}>) {
|
||||
const {docs} = props.data;
|
||||
export default function Docs(props: PageProps<{ docs: Doc[] }>) {
|
||||
const { docs } = props.data;
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@ -25,4 +42,4 @@ export default function Docs(props: PageProps<{docs:Doc[]}>) {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,74 +1,82 @@
|
||||
import { HandlerContext, Handlers } from "$fresh/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> {
|
||||
const url = new URL(req.url);
|
||||
const path = removePrefixFromPathname(decodeURI(url.pathname), "/fs");
|
||||
// if auth is required, check if the user is logged in.
|
||||
// if not, return a 401.
|
||||
const authRequired = Deno.env.get("AUTH_REQUIRED") === "true";
|
||||
if (authRequired) {
|
||||
const login = ctx.state["login"];
|
||||
if (!login) {
|
||||
return new Response(null, {
|
||||
status: 401,
|
||||
headers: {
|
||||
"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"
|
||||
}
|
||||
});
|
||||
}
|
||||
export async function GET(
|
||||
req: Request,
|
||||
ctx: HandlerContext,
|
||||
): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
const path = removePrefixFromPathname(decodeURI(url.pathname), "/fs");
|
||||
// if auth is required, check if the user is logged in.
|
||||
// if not, return a 401.
|
||||
const authRequired = Deno.env.get("AUTH_REQUIRED") === "true";
|
||||
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",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try{
|
||||
|
||||
const fileInfo = await Deno.stat(path);
|
||||
if (fileInfo.isDirectory) {
|
||||
// if index.html exists, serve it.
|
||||
// otherwise, serve a directory listing.
|
||||
const indexPath = 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
|
||||
});
|
||||
try {
|
||||
const fileInfo = await Deno.stat(path);
|
||||
if (fileInfo.isDirectory) {
|
||||
// if index.html exists, serve it.
|
||||
// otherwise, serve a directory listing.
|
||||
const indexPath = path + "/index.html";
|
||||
try {
|
||||
await Deno.stat(indexPath);
|
||||
const res = await serveFile(req, indexPath);
|
||||
return res;
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
if (e instanceof Deno.errors.NotFound) {
|
||||
return new Response("Not Found", {
|
||||
status: 404,
|
||||
});
|
||||
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,
|
||||
},
|
||||
);
|
||||
}
|
||||
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 = {
|
||||
GET
|
||||
}
|
||||
GET,
|
||||
};
|
||||
|
@ -13,7 +13,8 @@ export default function Home() {
|
||||
alt="the fresh logo: a sliced lemon dripping with juice"
|
||||
/>
|
||||
<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>
|
||||
<a href="/dir/">Go To CWD</a> | <a href="/doc/">Doc</a>
|
||||
<hr></hr>
|
||||
|
@ -1,38 +1,55 @@
|
||||
import { Head } from "$fresh/runtime.ts";
|
||||
|
||||
export default function Login() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Simple file server - Login</title>
|
||||
</Head>
|
||||
<div class="">
|
||||
<div class="p-4 absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Simple file server - Login</title>
|
||||
</Head>
|
||||
<div class="">
|
||||
<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
|
||||
sm:max-w-screen-sm max-w-screen-md">
|
||||
<img
|
||||
src="/logo.svg"
|
||||
class="w-32 h-32"
|
||||
alt="the fresh logo: a sliced lemon dripping with juice"
|
||||
/>
|
||||
<form action="/api/login" method="POST" class="flex flex-col gap-2 items-stretch">
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<div class="basis-40 flex items-center flex-1">
|
||||
<label for="username" class="w-20">Username</label>
|
||||
<input type="text" name="username" id="username"
|
||||
class="border-b-2 focus:border-green-500 transition-colors flex-1" />
|
||||
</div>
|
||||
<div class="flex items-center flex-1">
|
||||
<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>
|
||||
<button type="submit" class="bg-gray-400 p-2 rounded
|
||||
m-auto">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
<img
|
||||
src="/logo.svg"
|
||||
class="w-32 h-32"
|
||||
alt="the fresh logo: a sliced lemon dripping with juice"
|
||||
/>
|
||||
<form
|
||||
action="/api/login"
|
||||
method="POST"
|
||||
class="flex flex-col gap-2 items-stretch"
|
||||
>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<div class="basis-40 flex items-center flex-1">
|
||||
<label for="username" class="w-20">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
id="username"
|
||||
class="border-b-2 focus:border-green-500 transition-colors flex-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center flex-1">
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-gray-400 p-2 rounded
|
||||
m-auto"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
152
search.ts
152
search.ts
@ -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 { Doc, DocCollector, loadDocuments } from "./src/collect.ts";
|
||||
|
||||
export async function collectDocuments(path: string, out: string){
|
||||
const collector = new DocCollector({ summaryOnly: true, dropContent: true });
|
||||
await collector.walkDir(path);
|
||||
const docs = collector.getDocs();
|
||||
await Deno.writeTextFile(out, JSON.stringify(docs));
|
||||
export async function collectDocuments(path: string, out: string) {
|
||||
const collector = new DocCollector({ summaryOnly: true, dropContent: true });
|
||||
await collector.walkDir(path);
|
||||
const docs = collector.getDocs();
|
||||
await Deno.writeTextFile(out, JSON.stringify(docs));
|
||||
}
|
||||
|
||||
export async function watchDocuments(doc: string , options?:{
|
||||
abort?: AbortSignal,
|
||||
}){
|
||||
const doc_dump = await loadDocuments(doc);
|
||||
const collector = new DocCollector({ summaryOnly: true, dropContent: true });
|
||||
collector.setDocs(doc_dump);
|
||||
const index = Index.createIndex(doc_dump);
|
||||
async function update(){
|
||||
index.setDocs(collector.getDocs());
|
||||
await Deno.writeTextFile(doc, JSON.stringify(collector.getDocs()));
|
||||
}
|
||||
collector.watchDir(".", {
|
||||
async onAdd(doc){
|
||||
console.log("onAdd :", doc.path);
|
||||
await update();
|
||||
},
|
||||
async onRemove(path){
|
||||
console.log("onRemove :", path);
|
||||
await update();
|
||||
},
|
||||
async onChange(doc){
|
||||
console.log("onModify :", doc.path);
|
||||
await update();
|
||||
},
|
||||
abort: options?.abort,
|
||||
});
|
||||
return index;
|
||||
export async function watchDocuments(doc: string, options?: {
|
||||
abort?: AbortSignal;
|
||||
}) {
|
||||
const doc_dump = await loadDocuments(doc);
|
||||
const collector = new DocCollector({ summaryOnly: true, dropContent: true });
|
||||
collector.setDocs(doc_dump);
|
||||
const index = Index.createIndex(doc_dump);
|
||||
async function update() {
|
||||
index.setDocs(collector.getDocs());
|
||||
await Deno.writeTextFile(doc, JSON.stringify(collector.getDocs()));
|
||||
}
|
||||
collector.watchDir(".", {
|
||||
async onAdd(doc) {
|
||||
console.log("onAdd :", doc.path);
|
||||
await update();
|
||||
},
|
||||
async onRemove(path) {
|
||||
console.log("onRemove :", path);
|
||||
await update();
|
||||
},
|
||||
async onChange(doc) {
|
||||
console.log("onModify :", doc.path);
|
||||
await update();
|
||||
},
|
||||
abort: options?.abort,
|
||||
});
|
||||
return index;
|
||||
}
|
||||
|
||||
export const search = new Command();
|
||||
search.name("search")
|
||||
.description("Search for a document.")
|
||||
.command("collect", "Collect a document.")
|
||||
.description("Collect documents and index documents.")
|
||||
.arguments("<path:string>")
|
||||
.option("-o, --out <path:string>", "out file to write the index to.", {default: "collected_docs.json"})
|
||||
.action(async ({out} ,path: string) => {
|
||||
console.log("collecting", path);
|
||||
await collectDocuments(path, out);
|
||||
})
|
||||
.command("search", "Search for a document.")
|
||||
.description("Search for a document.")
|
||||
.arguments("<query:string>")
|
||||
.option("-d, --docs <path:string>", "collected document file to search", {default: "collected_docs.json"})
|
||||
.action(async ({docs}, query: string) => {
|
||||
console.log("searching", query);
|
||||
const doc_dump = await loadDocuments(docs);
|
||||
const results = Index.createIndex(doc_dump).search(query);
|
||||
console.log(results);
|
||||
})
|
||||
.command("interact", "watch the index and search for a document.")
|
||||
.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);
|
||||
.description("Search for a document.")
|
||||
.command("collect", "Collect a document.")
|
||||
.description("Collect documents and index documents.")
|
||||
.arguments("<path:string>")
|
||||
.option("-o, --out <path:string>", "out file to write the index to.", {
|
||||
default: "collected_docs.json",
|
||||
})
|
||||
.action(async ({ out }, path: string) => {
|
||||
console.log("collecting", path);
|
||||
await collectDocuments(path, out);
|
||||
})
|
||||
.command("search", "Search for a document.")
|
||||
.description("Search for a document.")
|
||||
.arguments("<query:string>")
|
||||
.option("-d, --docs <path:string>", "collected document file to search", {
|
||||
default: "collected_docs.json",
|
||||
})
|
||||
.action(async ({ docs }, query: string) => {
|
||||
console.log("searching", query);
|
||||
const doc_dump = await loadDocuments(docs);
|
||||
const results = Index.createIndex(doc_dump).search(query);
|
||||
console.log(results);
|
||||
})
|
||||
.command("interact", "watch the index and search for a document.")
|
||||
.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
|
||||
while (true){
|
||||
const query = await prompt("query: ");
|
||||
if (query === null){
|
||||
continue;
|
||||
}
|
||||
if (query === ":exit"){
|
||||
break;
|
||||
}
|
||||
const results = index.search(query);
|
||||
console.log(results);
|
||||
}
|
||||
});
|
||||
// interact
|
||||
while (true) {
|
||||
const query = await prompt("query: ");
|
||||
if (query === null) {
|
||||
continue;
|
||||
}
|
||||
if (query === ":exit") {
|
||||
break;
|
||||
}
|
||||
const results = index.search(query);
|
||||
console.log(results);
|
||||
}
|
||||
});
|
||||
|
||||
if ( import.meta.main ){
|
||||
await search.parse(Deno.args);
|
||||
}
|
||||
if (import.meta.main) {
|
||||
await search.parse(Deno.args);
|
||||
}
|
||||
|
@ -1,31 +1,38 @@
|
||||
//// @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";
|
||||
|
||||
export class Index{
|
||||
private index: Fuse;
|
||||
export class Index {
|
||||
private index: Fuse;
|
||||
|
||||
private constructor(index: Fuse){
|
||||
this.index = index;
|
||||
}
|
||||
private constructor(index: Fuse) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public setDocs(docs: Doc[]){
|
||||
this.index.setCollection(docs);
|
||||
}
|
||||
public setDocs(docs: Doc[]) {
|
||||
this.index.setCollection(docs);
|
||||
}
|
||||
|
||||
public search(query: string): Doc[]{
|
||||
return this.index.search(query, {limit: 10}).map((
|
||||
result) =>
|
||||
result.item as Doc);
|
||||
}
|
||||
public search(query: string): Doc[] {
|
||||
return this.index.search(query, { limit: 10 }).map((
|
||||
result,
|
||||
) => result.item as Doc);
|
||||
}
|
||||
|
||||
public static createIndex(docs: Doc[]){
|
||||
const index = new Fuse(docs,{
|
||||
keys: ["attributes.title", "attributes.japanese_title", "attributes.tags", "attributes.rjcode", "attributes.author", "path"],
|
||||
includeScore: true,
|
||||
includeMatches: true,
|
||||
})
|
||||
public static createIndex(docs: Doc[]) {
|
||||
const index = new Fuse(docs, {
|
||||
keys: [
|
||||
"attributes.title",
|
||||
"attributes.japanese_title",
|
||||
"attributes.tags",
|
||||
"attributes.rjcode",
|
||||
"attributes.author",
|
||||
"path",
|
||||
],
|
||||
includeScore: true,
|
||||
includeMatches: true,
|
||||
});
|
||||
|
||||
return new Index(index);
|
||||
}
|
||||
return new Index(index);
|
||||
}
|
||||
}
|
||||
|
376
src/collect.ts
376
src/collect.ts
@ -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 { Index } from "./client_search.ts";
|
||||
|
||||
export interface Doc{
|
||||
path: string;
|
||||
content: string;
|
||||
attributes: {
|
||||
title?: string;
|
||||
japanese_title?: string;
|
||||
tags?: string[];
|
||||
rjcode?: string;
|
||||
author?: string;
|
||||
};
|
||||
export interface Doc {
|
||||
path: string;
|
||||
content: string;
|
||||
attributes: {
|
||||
title?: string;
|
||||
japanese_title?: string;
|
||||
tags?: string[];
|
||||
rjcode?: string;
|
||||
author?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadDocuments(path: string): Promise<Doc[]>{
|
||||
const doc_json = await Deno.readTextFile(path);
|
||||
return JSON.parse(doc_json) as Doc[];
|
||||
export async function loadDocuments(path: string): Promise<Doc[]> {
|
||||
const doc_json = await Deno.readTextFile(path);
|
||||
return JSON.parse(doc_json) as Doc[];
|
||||
}
|
||||
|
||||
export interface DocCollectorOptions{
|
||||
summaryOnly?: boolean;
|
||||
dropContent?: boolean;
|
||||
export interface DocCollectorOptions {
|
||||
summaryOnly?: boolean;
|
||||
dropContent?: boolean;
|
||||
}
|
||||
|
||||
export class DocCollector {
|
||||
private doc_map: Map<string,Doc>;
|
||||
private options: DocCollectorOptions;
|
||||
private doc_map: Map<string, Doc>;
|
||||
private options: DocCollectorOptions;
|
||||
|
||||
constructor(options: DocCollectorOptions = {}){
|
||||
this.doc_map = new Map();
|
||||
this.options = options;
|
||||
constructor(options: DocCollectorOptions = {}) {
|
||||
this.doc_map = new Map();
|
||||
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[]{
|
||||
return [...this.doc_map.values()];
|
||||
}
|
||||
|
||||
public setDoc(doc: Doc){
|
||||
if (this.options.dropContent){
|
||||
doc.content = "";
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
if (event.kind === "create") {
|
||||
await onAdd(doc);
|
||||
} else if (event.kind === "modify") {
|
||||
await onChange(doc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: {}
|
||||
}
|
||||
}
|
||||
makeIndex(options?: {
|
||||
onUpdate?: (() => void) | (() => Promise<void>);
|
||||
abort?: AbortSignal;
|
||||
watch?: boolean;
|
||||
}) {
|
||||
const opt = options ?? {};
|
||||
const index = Index.createIndex(this.getDocs());
|
||||
if (!opt.watch) {
|
||||
return index;
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
115
src/media.ts
115
src/media.ts
@ -1,60 +1,59 @@
|
||||
|
||||
const ICON_MAP: Record<string, string> = {
|
||||
".pdf": "file-pdf",
|
||||
".zip": "file-zip",
|
||||
".rar": "file-zip",
|
||||
".7z": "file-zip",
|
||||
".tar": "file-zip",
|
||||
".gz": "file-zip",
|
||||
".bz2": "file-zip",
|
||||
".xz": "file-zip",
|
||||
".doc": "file-word",
|
||||
".docx": "file-word",
|
||||
".xls": "file-excel",
|
||||
".xlsx": "file-excel",
|
||||
".ppt": "file-ppt",
|
||||
".pptx": "file-ppt",
|
||||
".txt": "file-text",
|
||||
".md": "filetype-md",
|
||||
".html": "filetype-html",
|
||||
".ts": "file-code",
|
||||
".tsx": "filetype-tsx",
|
||||
".js": "filetype-js",
|
||||
".json": "filetype-json",
|
||||
".jsx": "filetype-jsx",
|
||||
".css": "filetype-css",
|
||||
".scss": "filetype-scss",
|
||||
".csv": "filetype-csv",
|
||||
".xml": "filetype-xml",
|
||||
".svg": "filetype-svg",
|
||||
".mp3": "file-music",
|
||||
".wav": "file-music",
|
||||
".ogg": "file-music",
|
||||
".flac": "file-music",
|
||||
".mp4": "file-play",
|
||||
".mkv": "file-play",
|
||||
".avi": "file-play",
|
||||
".mov": "file-play",
|
||||
".wmv": "file-play",
|
||||
".webm": "file-play",
|
||||
".mpg": "file-play",
|
||||
".mpeg": "file-play",
|
||||
".flv": "file-play",
|
||||
".m4v": "file-play",
|
||||
".m4a": "file-play",
|
||||
".aac": "file-play",
|
||||
".jpg": "file-image",
|
||||
".jpeg": "file-image",
|
||||
".png": "file-image",
|
||||
".gif": "file-image",
|
||||
".bmp": "file-image",
|
||||
".ico": "file-image",
|
||||
".tiff": "file-image",
|
||||
".tif": "file-image",
|
||||
".webp": "file-image",
|
||||
".psd": "file-image",
|
||||
}
|
||||
".pdf": "file-pdf",
|
||||
".zip": "file-zip",
|
||||
".rar": "file-zip",
|
||||
".7z": "file-zip",
|
||||
".tar": "file-zip",
|
||||
".gz": "file-zip",
|
||||
".bz2": "file-zip",
|
||||
".xz": "file-zip",
|
||||
".doc": "file-word",
|
||||
".docx": "file-word",
|
||||
".xls": "file-excel",
|
||||
".xlsx": "file-excel",
|
||||
".ppt": "file-ppt",
|
||||
".pptx": "file-ppt",
|
||||
".txt": "file-text",
|
||||
".md": "filetype-md",
|
||||
".html": "filetype-html",
|
||||
".ts": "file-code",
|
||||
".tsx": "filetype-tsx",
|
||||
".js": "filetype-js",
|
||||
".json": "filetype-json",
|
||||
".jsx": "filetype-jsx",
|
||||
".css": "filetype-css",
|
||||
".scss": "filetype-scss",
|
||||
".csv": "filetype-csv",
|
||||
".xml": "filetype-xml",
|
||||
".svg": "filetype-svg",
|
||||
".mp3": "file-music",
|
||||
".wav": "file-music",
|
||||
".ogg": "file-music",
|
||||
".flac": "file-music",
|
||||
".mp4": "file-play",
|
||||
".mkv": "file-play",
|
||||
".avi": "file-play",
|
||||
".mov": "file-play",
|
||||
".wmv": "file-play",
|
||||
".webm": "file-play",
|
||||
".mpg": "file-play",
|
||||
".mpeg": "file-play",
|
||||
".flv": "file-play",
|
||||
".m4v": "file-play",
|
||||
".m4a": "file-play",
|
||||
".aac": "file-play",
|
||||
".jpg": "file-image",
|
||||
".jpeg": "file-image",
|
||||
".png": "file-image",
|
||||
".gif": "file-image",
|
||||
".bmp": "file-image",
|
||||
".ico": "file-image",
|
||||
".tiff": "file-image",
|
||||
".tif": "file-image",
|
||||
".webp": "file-image",
|
||||
".psd": "file-image",
|
||||
};
|
||||
|
||||
export function extToIcon(s: string): string{
|
||||
return `/icon/${(ICON_MAP[s] ?? "file")}.svg`;
|
||||
}
|
||||
export function extToIcon(s: string): string {
|
||||
return `/icon/${(ICON_MAP[s] ?? "file")}.svg`;
|
||||
}
|
||||
|
@ -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) {
|
||||
while (str[start] === ' ' || str[start] === '\t' || str[start] === '\r' || str[start] === '\n') {
|
||||
start++;
|
||||
}
|
||||
return str.substring(start, end);
|
||||
while (
|
||||
str[start] === " " || str[start] === "\t" || str[start] === "\r" ||
|
||||
str[start] === "\n"
|
||||
) {
|
||||
start++;
|
||||
}
|
||||
return str.substring(start, end);
|
||||
}
|
||||
|
||||
interface Doc {
|
||||
metadata: Record<string, string | string[]>;
|
||||
content: string;
|
||||
metadata: Record<string, string | string[]>;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export function parse(content: string): Doc {
|
||||
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[]>;
|
||||
if (!content.startsWith("---")) {
|
||||
return {
|
||||
metadata: metadata,
|
||||
content: c,
|
||||
}
|
||||
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 {
|
||||
metadata: metadata,
|
||||
content: c,
|
||||
};
|
||||
}
|
||||
|
||||
export async function readMarkdownDoc(path: string): Promise<Doc> {
|
||||
const doc = await Deno.readTextFile(path);
|
||||
return parse(doc);
|
||||
const doc = await Deno.readTextFile(path);
|
||||
return parse(doc);
|
||||
}
|
||||
|
||||
|
@ -1,44 +1,43 @@
|
||||
import { Index } from "../client_search.ts";
|
||||
import { Doc, DocCollector } from "../collect.ts";
|
||||
|
||||
|
||||
export const docCollector = new DocCollector(
|
||||
{
|
||||
dropContent: true,
|
||||
summaryOnly: true,
|
||||
});
|
||||
{
|
||||
dropContent: true,
|
||||
summaryOnly: true,
|
||||
},
|
||||
);
|
||||
|
||||
export let docIndex: Index | undefined = undefined;
|
||||
|
||||
export async function prepareDocs() {
|
||||
const docPath = Deno.env.get("COLLECT_DOC_PATH");
|
||||
if (!docPath) {
|
||||
await docCollector.walkDir(".");
|
||||
docIndex = docCollector.makeIndex({
|
||||
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()));
|
||||
}
|
||||
const docPath = Deno.env.get("COLLECT_DOC_PATH");
|
||||
if (!docPath) {
|
||||
await docCollector.walkDir(".");
|
||||
docIndex = docCollector.makeIndex({
|
||||
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;
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
import {DB} from 'sqlite';
|
||||
import {createSchema} from './user.ts';
|
||||
import { DB } from "sqlite";
|
||||
import { createSchema } from "./user.ts";
|
||||
|
||||
export function connectDB(): DB {
|
||||
let DB_path = Deno.env.get("DB_PATH");
|
||||
if (DB_path === undefined){
|
||||
Deno.env.set("DB_PATH", "./db.sqlite");
|
||||
DB_path = "./db.sqlite";
|
||||
}
|
||||
let db = new DB(DB_path);
|
||||
createSchema(db);
|
||||
return db;
|
||||
}
|
||||
let DB_path = Deno.env.get("DB_PATH");
|
||||
if (DB_path === undefined) {
|
||||
Deno.env.set("DB_PATH", "./db.sqlite");
|
||||
DB_path = "./db.sqlite";
|
||||
}
|
||||
let db = new DB(DB_path);
|
||||
createSchema(db);
|
||||
return db;
|
||||
}
|
||||
|
103
src/user/user.ts
103
src/user/user.ts
@ -1,65 +1,74 @@
|
||||
import {genSalt, hash, compare} from "bcrypt";
|
||||
import { compare, genSalt, hash } from "bcrypt";
|
||||
import { DB } from "sqlite";
|
||||
|
||||
interface User{
|
||||
name: string;
|
||||
salted_password: string;
|
||||
salt: string;
|
||||
interface User {
|
||||
name: string;
|
||||
salted_password: string;
|
||||
salt: string;
|
||||
}
|
||||
|
||||
export async function createUser(name: string, password: string){
|
||||
const salt = await genSalt(10);
|
||||
const salted_password = await hash(password, salt);
|
||||
const user: User = {
|
||||
name: name,
|
||||
salted_password: salted_password,
|
||||
salt: salt
|
||||
}
|
||||
return user;
|
||||
export async function createUser(name: string, password: string) {
|
||||
const salt = await genSalt(10);
|
||||
const salted_password = await hash(password, salt);
|
||||
const user: User = {
|
||||
name: name,
|
||||
salted_password: salted_password,
|
||||
salt: salt,
|
||||
};
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function verifyUser(user: User, password: string){
|
||||
return await compare(password, user.salted_password);
|
||||
export async function verifyUser(user: User, password: string) {
|
||||
return await compare(password, user.salted_password);
|
||||
}
|
||||
|
||||
export async function getAllUsers(db :DB): Promise<User[]>{
|
||||
const users = await db.query<[string, string, string]>
|
||||
("SELECT name, salted_password, salt FROM users");
|
||||
return users.map(([name, salted_password, salt])=>({
|
||||
name,
|
||||
salted_password,
|
||||
salt,
|
||||
}));
|
||||
export async function getAllUsers(db: DB): Promise<User[]> {
|
||||
const users = await db.query<[string, string, string]>(
|
||||
"SELECT name, salted_password, salt FROM users",
|
||||
);
|
||||
return users.map(([name, salted_password, salt]) => ({
|
||||
name,
|
||||
salted_password,
|
||||
salt,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getUser(db: DB, name: string): Promise<User | undefined>{
|
||||
const users = await db.query<[string, string, string]>
|
||||
("SELECT name, salted_password, salt FROM users WHERE name = ?", [name]);
|
||||
if (users === undefined || users.length === 0){
|
||||
return undefined;
|
||||
}
|
||||
const user = users[0];
|
||||
return {
|
||||
name: user[0],
|
||||
salted_password: user[1],
|
||||
salt: user[2],
|
||||
}
|
||||
export async function getUser(db: DB, name: string): Promise<User | undefined> {
|
||||
const users = await db.query<[string, string, string]>(
|
||||
"SELECT name, salted_password, salt FROM users WHERE name = ?",
|
||||
[name],
|
||||
);
|
||||
if (users === undefined || users.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const user = users[0];
|
||||
return {
|
||||
name: user[0],
|
||||
salted_password: user[1],
|
||||
salt: user[2],
|
||||
};
|
||||
}
|
||||
|
||||
export async function addUser(db: DB, user: User){
|
||||
await db.query("INSERT INTO users (name, salted_password, salt) VALUES (?, ?, ?)",
|
||||
[user.name, user.salted_password, user.salt]);
|
||||
export async function addUser(db: DB, user: User) {
|
||||
await db.query(
|
||||
"INSERT INTO users (name, salted_password, salt) VALUES (?, ?, ?)",
|
||||
[user.name, user.salted_password, user.salt],
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateUser(db: DB, user: User){
|
||||
await db.query("UPDATE users SET salted_password = ?, salt = ? WHERE name = ?",
|
||||
[user.salted_password, user.salt, user.name]);
|
||||
export async function updateUser(db: DB, user: User) {
|
||||
await db.query(
|
||||
"UPDATE users SET salted_password = ?, salt = ? WHERE name = ?",
|
||||
[user.salted_password, user.salt, user.name],
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteUser(db: DB, name: string){
|
||||
await db.query("DELETE FROM users WHERE name = ?", [name]);
|
||||
export async function deleteUser(db: DB, name: string) {
|
||||
await db.query("DELETE FROM users WHERE name = ?", [name]);
|
||||
}
|
||||
|
||||
export async function createSchema(db: DB){
|
||||
await db.query("CREATE TABLE IF NOT EXISTS users (name TEXT PRIMARY KEY, salted_password TEXT, salt TEXT)");
|
||||
}
|
||||
export async function createSchema(db: DB) {
|
||||
await db.query(
|
||||
"CREATE TABLE IF NOT EXISTS users (name TEXT PRIMARY KEY, salted_password TEXT, salt TEXT)",
|
||||
);
|
||||
}
|
||||
|
@ -1995,4 +1995,4 @@
|
||||
"sina-weibo": 63690,
|
||||
"tencent-qq": 63691,
|
||||
"wikipedia": 63692
|
||||
}
|
||||
}
|
||||
|
@ -5,4 +5,4 @@ module.exports = {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
};
|
||||
|
@ -2,6 +2,7 @@
|
||||
title: "hello"
|
||||
tags: ["asdf","wer"]
|
||||
---
|
||||
|
||||
# hello
|
||||
|
||||
- [hello](hello.md)
|
||||
@ -9,4 +10,4 @@ tags: ["asdf","wer"]
|
||||
|
||||
asdf
|
||||
|
||||
File: test_data\d\hello.md
|
||||
File: test_data\d\hello.md
|
||||
|
@ -3,8 +3,12 @@ rjcode: RJ130512
|
||||
title: Summary of the 13th Meeting of the Joint Committee on the Safety of 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
|
||||
|
||||
## 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.
|
||||
|
@ -3,4 +3,4 @@ title: "한글 테스트. 띄어쓰기없이도되나?"
|
||||
tags: ["한글", "테스트"]
|
||||
---
|
||||
|
||||
# 한글 테스트. 띄어쓰기없이도되나?
|
||||
# 한글 테스트. 띄어쓰기없이도되나?
|
||||
|
4
test_data/한글/SUMMARY.md
Normal file
4
test_data/한글/SUMMARY.md
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "SUMMARY"
|
||||
tags: ["SUMMARY"]
|
||||
---
|
119
user.ts
119
user.ts
@ -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 * as users from "./src/user/user.ts";
|
||||
|
||||
export const user_command = new Command()
|
||||
.description("Manage users.")
|
||||
.command("add", "add a user")
|
||||
.arguments("[username:string]")
|
||||
.option("-p, --password <password:string>", "The password for the user.")
|
||||
.option("-q, --quiet", "quiet output.")
|
||||
.action(async ({quiet, password}
|
||||
, username) => {
|
||||
if(username === undefined){
|
||||
username = await Input.prompt("Username: ");
|
||||
}
|
||||
if(password === undefined){
|
||||
password = await Secret.prompt("Password: ");
|
||||
const password2 = await Secret.prompt("Confirm password: ");
|
||||
if (password !== password2){
|
||||
console.error("Passwords do not match.");
|
||||
Deno.exit(1);
|
||||
}
|
||||
}
|
||||
const db = connectDB();
|
||||
const new_user = await users.createUser( username, password);
|
||||
await users.addUser(db, new_user);
|
||||
if (!quiet){
|
||||
console.log(`Added user ${username}`);
|
||||
}
|
||||
})
|
||||
.command("delete", "delete a user")
|
||||
.arguments("<username:string>")
|
||||
.option("-q, --quiet", "Quiet output.")
|
||||
.action(async ({quiet}, username) => {
|
||||
const db = connectDB();
|
||||
await users.deleteUser(db, username);
|
||||
if (!quiet){
|
||||
console.log(`Deleting user ${username}`);
|
||||
}
|
||||
})
|
||||
.command("list", "list all users")
|
||||
.action(async () => {
|
||||
const db = connectDB();
|
||||
const all_users = await users.getAllUsers(db);
|
||||
for (const user of all_users){
|
||||
console.log(`${user.name}`);
|
||||
}
|
||||
})
|
||||
.command("reset", "reset a user's password")
|
||||
.arguments("<username:string> <password:string>")
|
||||
.option("-q, --quiet", "quiet output.")
|
||||
.action(async ({quiet}, [username, password]) => {
|
||||
const db = connectDB();
|
||||
const new_user = await users.createUser( username, password);
|
||||
await users.updateUser(db, new_user);
|
||||
if (!quiet){
|
||||
console.log(`Resetting password for user ${username}`);
|
||||
}
|
||||
});
|
||||
.description("Manage users.")
|
||||
.command("add", "add a user")
|
||||
.arguments("[username:string]")
|
||||
.option("-p, --password <password:string>", "The password for the user.")
|
||||
.option("-q, --quiet", "quiet output.")
|
||||
.action(async ({ quiet, password }, username) => {
|
||||
if (username === undefined) {
|
||||
username = await Input.prompt("Username: ");
|
||||
}
|
||||
if (password === undefined) {
|
||||
password = await Secret.prompt("Password: ");
|
||||
const password2 = await Secret.prompt("Confirm password: ");
|
||||
if (password !== password2) {
|
||||
console.error("Passwords do not match.");
|
||||
Deno.exit(1);
|
||||
}
|
||||
}
|
||||
const db = connectDB();
|
||||
const new_user = await users.createUser(username, password);
|
||||
await users.addUser(db, new_user);
|
||||
if (!quiet) {
|
||||
console.log(`Added user ${username}`);
|
||||
}
|
||||
})
|
||||
.command("delete", "delete a user")
|
||||
.arguments("<username:string>")
|
||||
.option("-q, --quiet", "Quiet output.")
|
||||
.action(async ({ quiet }, username) => {
|
||||
const db = connectDB();
|
||||
await users.deleteUser(db, username);
|
||||
if (!quiet) {
|
||||
console.log(`Deleting user ${username}`);
|
||||
}
|
||||
})
|
||||
.command("list", "list all users")
|
||||
.action(async () => {
|
||||
const db = connectDB();
|
||||
const all_users = await users.getAllUsers(db);
|
||||
for (const user of all_users) {
|
||||
console.log(`${user.name}`);
|
||||
}
|
||||
})
|
||||
.command("reset", "reset a user's password")
|
||||
.arguments("<username:string> <password:string>")
|
||||
.option("-q, --quiet", "quiet output.")
|
||||
.action(async ({ quiet }, [username, password]) => {
|
||||
const db = connectDB();
|
||||
const new_user = await users.createUser(username, password);
|
||||
await users.updateUser(db, new_user);
|
||||
if (!quiet) {
|
||||
console.log(`Resetting password for user ${username}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (import.meta.main){
|
||||
await user_command.parse(Deno.args);
|
||||
}
|
||||
if (import.meta.main) {
|
||||
await user_command.parse(Deno.args);
|
||||
}
|
||||
|
@ -1,28 +1,31 @@
|
||||
|
||||
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" },
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
27
util/util.ts
27
util/util.ts
@ -1,15 +1,18 @@
|
||||
export function removePrefixFromPathname(pathname: string, prefix: string): string {
|
||||
let ret = pathname;
|
||||
ret = ret.slice(prefix.length);
|
||||
if (ret.startsWith("/")) {
|
||||
ret = ret.slice(1);
|
||||
}
|
||||
if (ret === "") {
|
||||
ret = ".";
|
||||
}
|
||||
return ret;
|
||||
export function removePrefixFromPathname(
|
||||
pathname: string,
|
||||
prefix: string,
|
||||
): string {
|
||||
let ret = pathname;
|
||||
ret = ret.slice(prefix.length);
|
||||
if (ret.startsWith("/")) {
|
||||
ret = ret.slice(1);
|
||||
}
|
||||
if (ret === "") {
|
||||
ret = ".";
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function encodePath(path: string): string {
|
||||
return path.split("/").map(encodeURIComponent).join("/");
|
||||
}
|
||||
return path.split("/").map(encodeURIComponent).join("/");
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user