From cce5be43e1205889faad7014621a138b35521cd3 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Wed, 6 Jul 2022 17:48:43 +0200 Subject: [PATCH] Adding https/file support for mounts --- docs/Mounts.md | 6 +- docs/index.md | 3 + packages/plugs/core/mount.ts | 180 ++++++++++++++++++++++++++++------- 3 files changed, 151 insertions(+), 38 deletions(-) diff --git a/docs/Mounts.md b/docs/Mounts.md index 2f27b2c8..3c194556 100644 --- a/docs/Mounts.md +++ b/docs/Mounts.md @@ -2,8 +2,10 @@ Space mounting in `MOUNTS` ```yaml - path: file:/Users/zef/git/blog - prefix: 📖 + prefix: blog/ perm: ro,rw #default rw +- path: http://someIP:3000 + prefix: prod/ ``` Features @@ -13,6 +15,8 @@ To do: * [ ] Handle queries * `page` and `link` query needs to dynamically add/remove a `and name =~ /^🚪 PREFIX/` clause) * `task` same but with `page` check +* [x] Add `file:` support +* [x] Add `http:`/`https:` support * Due to namespacing, the mounted space needs to be namespaced somehow * Could be an emoji, could be a page prefix (now using `name`) diff --git a/docs/index.md b/docs/index.md index ac642871..9ecc4212 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,6 +26,9 @@ I know, right? [[🔨 Development]] [[🗺 Roadmap]] +## Proposals +[[Mounts]] + ## Installing and running Silver Bullet To run a release version, you need to have a recent version of npm (8+) and node.js (16+) installed as well as some basic build infrastructure (make, cpp). Silver Bullet has only been tested on MacOS and Linux thus far. diff --git a/packages/plugs/core/mount.ts b/packages/plugs/core/mount.ts index 0569ac09..04b74a87 100644 --- a/packages/plugs/core/mount.ts +++ b/packages/plugs/core/mount.ts @@ -20,6 +20,8 @@ const globalMountPrefix = "🚪 "; type MountPoint = { prefix: string; path: string; + password?: string; + protocol: "file" | "http"; perm: "rw" | "ro"; }; @@ -44,11 +46,12 @@ async function updateMountPoints() { return; } let mountsYaml = codeTextNode.children![0].text; - let mountList = YAML.parse(mountsYaml!); + let mountList: Partial[] = YAML.parse(mountsYaml!); if (!Array.isArray(mountList)) { console.error("Invalid MOUNTS file, should have array of mount points"); return; } + // Let's validate this and fill in some of the blanks for (let mountPoint of mountList) { if (!mountPoint.prefix) { console.error("Invalid mount point, no prefix specified", mountPoint); @@ -61,9 +64,16 @@ async function updateMountPoints() { if (!mountPoint.perm) { mountPoint.perm = "rw"; } + if (mountPoint.path.startsWith("file:")) { + mountPoint.protocol = "file"; + mountPoint.path = mountPoint.path.substring("file:".length); + } else { + mountPoint.protocol = "http"; + mountPoint.path += "/fs"; // Add the /fs suffix to the path + } } - mountPointCache = mountList; + mountPointCache = mountList as MountPoint[]; } async function translateLinksWithPrefix( @@ -120,15 +130,39 @@ export async function readPageMounted( ): Promise<{ text: string; meta: PageMeta }> { await updateMountPoints(); let { resolvedPath, mountPoint } = lookupMountPoint(name); - let { text, meta } = await readFile(`${resolvedPath}.md`); - return { - text: await translateLinksWithPrefix(text, mountPoint.prefix), - meta: { - name: name, - lastModified: meta.lastModified, - perm: mountPoint.perm, - }, - }; + if (mountPoint.protocol === "file") { + let { text, meta } = await readFile(`${resolvedPath}.md`); + return { + text: await translateLinksWithPrefix(text, mountPoint.prefix), + meta: { + name: name, + lastModified: meta.lastModified, + perm: mountPoint.perm, + }, + }; + } else { + let res = await fetch(resolvedPath, { + method: "GET", + headers: authHeaders(mountPoint), + }); + if (res.status !== 200) { + throw new Error( + `Failed to read page: ${res.status}: ${await res.text()}` + ); + } + if (res.headers.get("X-Status") === "404") { + throw new Error(`Page not found`); + } + let text = await res.text(); + return { + text: await translateLinksWithPrefix(text, mountPoint.prefix), + meta: { + name: name, + lastModified: +(res.headers.get("Last-Modified") || "0"), + perm: (res.headers.get("X-Permission") as "rw" | "ro") || "rw", + }, + }; + } } export async function writePageMounted( @@ -138,52 +172,124 @@ export async function writePageMounted( await updateMountPoints(); let { resolvedPath, mountPoint } = lookupMountPoint(name); text = await translateLinksWithoutPrefix(text, mountPoint.prefix); - let meta = await writeFile(`${resolvedPath}.md`, text); - return { - name: name, - lastModified: meta.lastModified, - perm: mountPoint.perm, - }; + if (mountPoint.protocol === "file") { + let meta = await writeFile(`${resolvedPath}.md`, text); + return { + name: name, + lastModified: meta.lastModified, + perm: mountPoint.perm, + }; + } else { + let res = await fetch(resolvedPath, { + method: "PUT", + headers: authHeaders(mountPoint), + body: text, + }); + if (res.status !== 200) { + throw new Error( + `Failed to write page: ${res.status}: ${await res.text()}` + ); + } + return { + name: name, + lastModified: +(res.headers.get("Last-Modified") || "0"), + perm: (res.headers.get("X-Permission") as "rw" | "ro") || "rw", + }; + } } export async function deletePageMounted(name: string): Promise { await updateMountPoints(); let { resolvedPath, mountPoint } = lookupMountPoint(name); - if (mountPoint.perm === "rw") { - await deleteFile(`${resolvedPath}.md`); + if (mountPoint.protocol === "file") { + if (mountPoint.perm === "rw") { + await deleteFile(`${resolvedPath}.md`); + } else { + throw new Error("Deleting read-only page"); + } } else { - throw new Error("Deleting read-only page"); + throw new Error("Not yet implemented"); } } +function authHeaders(mountPoint: MountPoint): HeadersInit { + return { + Authorization: mountPoint.password ? `Bearer ${mountPoint.password}` : "", + }; +} + export async function getPageMetaMounted(name: string): Promise { await updateMountPoints(); let { resolvedPath, mountPoint } = lookupMountPoint(name); - let meta = await getFileMeta(`${resolvedPath}.md`); - return { - name, - lastModified: meta.lastModified, - perm: mountPoint.perm, - }; + if (mountPoint.protocol === "file") { + let meta = await getFileMeta(`${resolvedPath}.md`); + return { + name, + lastModified: meta.lastModified, + perm: mountPoint.perm, + }; + } else { + // http/https + let res = await fetch(resolvedPath, { + method: "OPTIONS", + headers: authHeaders(mountPoint), + }); + if (res.status !== 200) { + throw new Error( + `Failed to list pages: ${res.status}: ${await res.text()}` + ); + } + if (res.headers.get("X-Status") === "404") { + throw new Error(`Page not found`); + } + return { + name: name, + lastModified: +(res.headers.get("Last-Modified") || "0"), + perm: (res.headers.get("X-Permission") as "rw" | "ro") || "rw", + }; + } } export async function listPagesMounted(): Promise { await updateMountPoints(); let allPages: PageMeta[] = []; for (let mp of mountPointCache) { - let files = await listFiles(mp.path, true); - for (let file of files) { - if (!file.name.endsWith(".md")) { - continue; + if (mp.protocol === "file") { + let files = await listFiles(mp.path, true); + for (let file of files) { + if (!file.name.endsWith(".md")) { + continue; + } + allPages.push({ + name: `${globalMountPrefix}${mp.prefix}${file.name.substring( + 0, + file.name.length - 3 + )}`, + lastModified: file.lastModified, + perm: mp.perm, + }); } - allPages.push({ - name: `${globalMountPrefix}${mp.prefix}${file.name.substring( - 0, - file.name.length - 3 - )}`, - lastModified: file.lastModified, - perm: mp.perm, + } else { + let res = await fetch(mp.path, { + headers: authHeaders(mp), }); + if (res.status !== 200) { + throw new Error( + `Failed to list pages: ${res.status}: ${await res.text()}` + ); + } + let remotePages: PageMeta[] = await res.json(); + // console.log("Remote pages", remotePages); + for (let pageMeta of remotePages) { + if (pageMeta.name.startsWith("_")) { + continue; + } + allPages.push({ + name: `${globalMountPrefix}${mp.prefix}${pageMeta.name}`, + lastModified: pageMeta.lastModified, + perm: mp.perm, + }); + } } } return allPages;