diff --git a/cli/plug_run.test.ts b/cli/plug_run.test.ts index 07f4d941..e8c1039f 100644 --- a/cli/plug_run.test.ts +++ b/cli/plug_run.test.ts @@ -10,6 +10,7 @@ import { path } from "../common/deps.ts"; Deno.test("Test plug run", async () => { // const tempDir = await Deno.makeTempDir(); + const tempDbFile = await Deno.makeTempFile({ suffix: ".db" }); const assetBundle = new AssetBundle(assets); @@ -26,6 +27,7 @@ Deno.test("Test plug run", async () => { assertEquals( await runPlug( testSpaceFolder, + tempDbFile, "test.run", [], assetBundle, @@ -35,4 +37,5 @@ Deno.test("Test plug run", async () => { // await Deno.remove(tempDir, { recursive: true }); esbuild.stop(); + await Deno.remove(tempDbFile); }); diff --git a/cli/plug_run.ts b/cli/plug_run.ts index cd81bb42..099f3110 100644 --- a/cli/plug_run.ts +++ b/cli/plug_run.ts @@ -9,16 +9,13 @@ import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_sp export async function runPlug( spacePath: string, + dbPath: string, functionName: string | undefined, args: string[] = [], builtinAssetBundle: AssetBundle, - indexFirst = false, httpServerPort = 3123, httpHostname = "127.0.0.1", ) { - spacePath = path.resolve(spacePath); - const tempFile = Deno.makeTempFileSync({ suffix: ".db" }); - console.log("Tempt db file", tempFile); const serverController = new AbortController(); const app = new Application(); @@ -27,7 +24,7 @@ export async function runPlug( new DiskSpacePrimitives(spacePath), builtinAssetBundle, ), - tempFile, + dbPath, app, ); await serverSystem.init(); @@ -37,13 +34,6 @@ export async function runPlug( signal: serverController.signal, }); - if (indexFirst) { - await serverSystem.system.loadedPlugs.get("index")!.invoke( - "reindexSpace", - [], - ); - } - if (functionName) { const [plugName, funcName] = functionName.split("."); @@ -54,7 +44,6 @@ export async function runPlug( const result = await plug.invoke(funcName, args); await serverSystem.close(); serverSystem.denoKv.close(); - await Deno.remove(tempFile); serverController.abort(); return result; } else { diff --git a/cmd/constants.ts b/cmd/constants.ts new file mode 100644 index 00000000..07a2607e --- /dev/null +++ b/cmd/constants.ts @@ -0,0 +1 @@ +export const silverBulletDbFile = ".silverbullet.db"; diff --git a/cmd/plug_run.ts b/cmd/plug_run.ts index 3a0cfeb8..96d0090a 100644 --- a/cmd/plug_run.ts +++ b/cmd/plug_run.ts @@ -4,14 +4,15 @@ import assets from "../dist/plug_asset_bundle.json" assert { type: "json", }; import { AssetBundle } from "../plugos/asset_bundle/bundle.ts"; +import { silverBulletDbFile } from "./constants.ts"; export async function plugRunCommand( { - noIndex, + db, hostname, port, }: { - noIndex: boolean; + db?: string; hostname?: string; port?: number; }, @@ -21,14 +22,18 @@ export async function plugRunCommand( ) { spacePath = path.resolve(spacePath); console.log("Space path", spacePath); + let dbPath = path.resolve(spacePath, silverBulletDbFile); + if (db) { + dbPath = path.resolve(db); + } console.log("Function to run:", functionName, "with arguments", args); try { const result = await runPlug( spacePath, + dbPath, functionName, args, new AssetBundle(assets), - !noIndex, port, hostname, ); diff --git a/cmd/server.ts b/cmd/server.ts index c5c57f70..545b26ff 100644 --- a/cmd/server.ts +++ b/cmd/server.ts @@ -17,6 +17,7 @@ import { ServerSystem } from "../server/server_system.ts"; import { sleep } from "$sb/lib/async.ts"; import { SilverBulletHooks } from "../common/manifest.ts"; import { System } from "../plugos/system.ts"; +import { silverBulletDbFile } from "./constants.ts"; export async function serveCommand( options: { @@ -28,7 +29,7 @@ export async function serveCommand( key?: string; reindex?: boolean; db?: string; - serverProcessing?: boolean; + storeOnly?: boolean; }, folder?: string, ) { @@ -36,11 +37,13 @@ export async function serveCommand( "127.0.0.1"; const port = options.port || (Deno.env.get("SB_PORT") && +Deno.env.get("SB_PORT")!) || 3000; - - let dbFile = options.db || Deno.env.get("SB_DB_FILE") || ".silverbullet.db"; + const storeOnly = options.storeOnly || Deno.env.get("SB_STORE_ONLY"); + let dbFile = options.db || Deno.env.get("SB_DB_FILE") || silverBulletDbFile; const app = new Application(); + console.log(options); + if (!folder) { folder = Deno.env.get("SB_FOLDER"); if (!folder) { @@ -84,7 +87,8 @@ To allow outside connections, pass -L 0.0.0.0 as a flag, and put a TLS terminato ); let system: System | undefined; - if (options.serverProcessing) { + // system = undefined in storeOnly mode (no PlugOS instance on the server) + if (!storeOnly) { // Enable server-side processing dbFile = path.resolve(folder, dbFile); console.log( diff --git a/plug-api/plugos-syscall/fetch.ts b/plug-api/plugos-syscall/fetch.ts deleted file mode 100644 index a53c9368..00000000 --- a/plug-api/plugos-syscall/fetch.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { - ProxyFetchRequest, - ProxyFetchResponse, -} from "../../common/proxy_fetch.ts"; -import { - base64Decode, - base64Encode, -} from "../../plugos/asset_bundle/base64.ts"; - -async function readStream( - stream: ReadableStream, -): Promise { - const arrays: Uint8Array[] = []; - let totalRead = 0; - const reader = stream.getReader(); - while (true) { - // The `read()` method returns a promise that - // resolves when a value has been received. - const { done, value } = await reader.read(); - // Result objects contain two properties: - // `done` - `true` if the stream has already given you all its data. - // `value` - Some data. Always `undefined` when `done` is `true`. - if (done) { - const resultArray = new Uint8Array(totalRead); - let offset = 0; - for (const array of arrays) { - resultArray.set(array, offset); - offset += array.length; - } - return resultArray; - } - arrays.push(value); - totalRead += value.length; - } -} - -export async function sandboxFetch( - reqInfo: RequestInfo, - options?: ProxyFetchRequest, -): Promise { - if (typeof reqInfo !== "string") { - const body = new Uint8Array(await reqInfo.arrayBuffer()); - const encodedBody = body.length > 0 ? base64Encode(body) : undefined; - options = { - method: reqInfo.method, - headers: Object.fromEntries(reqInfo.headers.entries()), - base64Body: encodedBody, - }; - reqInfo = reqInfo.url; - } - // @ts-ignore: monkey patching fetch - return syscall("sandboxFetch.fetch", reqInfo, options); -} - -async function bodyInitToUint8Array(init: BodyInit): Promise { - if (init instanceof Blob) { - const buffer = await init.arrayBuffer(); - return new Uint8Array(buffer); - } else if (init instanceof ArrayBuffer) { - return new Uint8Array(init); - } else if (init instanceof ReadableStream) { - return readStream(init); - } else if (typeof init === "string") { - return new TextEncoder().encode(init); - } else { - throw new Error("Unknown body init type"); - } -} - -export function monkeyPatchFetch() { - // @ts-ignore: monkey patching fetch - globalThis.nativeFetch = globalThis.fetch; - // @ts-ignore: monkey patching fetch - globalThis.fetch = async function ( - reqInfo: RequestInfo, - init?: RequestInit, - ): Promise { - const encodedBody = init && init.body - ? base64Encode( - new Uint8Array(await (new Response(init.body)).arrayBuffer()), - ) - : undefined; - // console.log("Encoded this body", encodedBody); - const r = await sandboxFetch( - reqInfo, - init && { - method: init.method, - headers: init.headers as Record, - base64Body: encodedBody, - }, - ); - return new Response(r.base64Body ? base64Decode(r.base64Body) : null, { - status: r.status, - headers: r.headers, - }); - }; -} diff --git a/plugos/worker_runtime.ts b/plugos/worker_runtime.ts index e3dde1be..5b41f9ff 100644 --- a/plugos/worker_runtime.ts +++ b/plugos/worker_runtime.ts @@ -1,7 +1,9 @@ // This is the runtime imported from the compiled plug worker code - import type { ControllerMessage, WorkerMessage } from "./protocol.ts"; -import type { Manifest } from "../common/manifest.ts"; +import type { + ProxyFetchRequest, + ProxyFetchResponse, +} from "../common/proxy_fetch.ts"; declare global { function syscall(name: string, ...args: any[]): Promise; @@ -53,7 +55,7 @@ self.syscall = async (name: string, ...args: any[]) => { export function setupMessageListener( // deno-lint-ignore ban-types functionMapping: Record, - manifest: Manifest, + manifest: any, ) { self.addEventListener("message", (event: { data: WorkerMessage }) => { (async () => { @@ -81,7 +83,6 @@ export function setupMessageListener( }); } } - break; case "sysr": { @@ -109,6 +110,72 @@ export function setupMessageListener( }); } +export function base64Decode(s: string): Uint8Array { + const binString = atob(s); + const len = binString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binString.charCodeAt(i); + } + return bytes; +} + +export function base64Encode(buffer: Uint8Array | string): string { + if (typeof buffer === "string") { + buffer = new TextEncoder().encode(buffer); + } + let binary = ""; + const len = buffer.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(buffer[i]); + } + return btoa(binary); +} + +export async function sandboxFetch( + reqInfo: RequestInfo, + options?: ProxyFetchRequest, +): Promise { + if (typeof reqInfo !== "string") { + const body = new Uint8Array(await reqInfo.arrayBuffer()); + const encodedBody = body.length > 0 ? base64Encode(body) : undefined; + options = { + method: reqInfo.method, + headers: Object.fromEntries(reqInfo.headers.entries()), + base64Body: encodedBody, + }; + reqInfo = reqInfo.url; + } + return syscall("sandboxFetch.fetch", reqInfo, options); +} + // Monkey patch fetch() -import { monkeyPatchFetch } from "../plug-api/plugos-syscall/fetch.ts"; + +export function monkeyPatchFetch() { + globalThis.nativeFetch = globalThis.fetch; + // @ts-ignore: monkey patching fetch + globalThis.fetch = async function ( + reqInfo: RequestInfo, + init?: RequestInit, + ): Promise { + const encodedBody = init && init.body + ? base64Encode( + new Uint8Array(await (new Response(init.body)).arrayBuffer()), + ) + : undefined; + const r = await sandboxFetch( + reqInfo, + init && { + method: init.method, + headers: init.headers as Record, + base64Body: encodedBody, + }, + ); + return new Response(r.base64Body ? base64Decode(r.base64Body) : null, { + status: r.status, + headers: r.headers, + }); + }; +} + monkeyPatchFetch(); diff --git a/plugs/editor/editor.plug.yaml b/plugs/editor/editor.plug.yaml index c76699f5..1308fc07 100644 --- a/plugs/editor/editor.plug.yaml +++ b/plugs/editor/editor.plug.yaml @@ -104,7 +104,6 @@ functions: command: name: "Text: Strikethrough" key: "Ctrl-Shift-s" - mac: "Cmd-Shift-s" wrapper: "~~" marker: path: ./text.ts:wrapSelection diff --git a/plugs/index/refactor.ts b/plugs/index/refactor.ts index 289594db..cddeff3d 100644 --- a/plugs/index/refactor.ts +++ b/plugs/index/refactor.ts @@ -45,9 +45,7 @@ export async function renamePageCommand(cmdDef: any) { throw e; } } - const updatedReferences = await renamePage(oldName, newName); - console.log("Navigating to new page"); - await editor.navigate(newName, 0, true); + const updatedReferences = await renamePage(oldName, newName, true); await editor.flashNotification( `Renamed page, and updated ${updatedReferences} references`, @@ -57,12 +55,21 @@ export async function renamePageCommand(cmdDef: any) { } } -async function renamePage(oldName: string, newName: string): Promise { +async function renamePage( + oldName: string, + newName: string, + navigateThere = false, +): Promise { const text = await space.readPage(oldName); console.log("Writing new page to space"); const newPageMeta = await space.writePage(newName, text); + if (navigateThere) { + console.log("Navigating to new page"); + await editor.navigate(newName, 0, true); + } + const pagesToUpdate = await getBackLinks(oldName); console.log("All pages containing backlinks", pagesToUpdate); diff --git a/server/http_server.ts b/server/http_server.ts index 9ec3588c..21175f1c 100644 --- a/server/http_server.ts +++ b/server/http_server.ts @@ -70,8 +70,8 @@ export class HttpServer { this.options.pagesPath.replaceAll("\\", "\\\\"), // ); ).replaceAll( - "{{SUPPORT_ONLINE_MODE}}", - this.system ? "true" : "false", + "{{SYNC_ONLY}}", + this.system ? "false" : "true", ); } diff --git a/silverbullet.ts b/silverbullet.ts index 4301e3a5..e1e12c49 100755 --- a/silverbullet.ts +++ b/silverbullet.ts @@ -45,11 +45,11 @@ await new Command() "Path to TLS key", ) .option( - "--no-server-processing [type:boolean]", - "Disable online mode (no server-side processing)", + "--store-only", + "Run the server as a pure space (file) store only without any backend processing (this disables 'server mode' in the client)", ) .option( - "--reindex [type:boolean]", + "--reindex", "Reindex space on startup", ) .option( @@ -60,8 +60,8 @@ await new Command() // plug:compile .command("plug:compile", "Bundle (compile) one or more plug manifests") .arguments("<...name.plug.yaml:string>") - .option("--debug [type:boolean]", "Do not minifiy code", { default: false }) - .option("--info [type:boolean]", "Print out size info per function", { + .option("--debug", "Do not minifiy code", { default: false }) + .option("--info", "Print out size info per function", { default: false, }) .option("--watch, -w [type:boolean]", "Watch for changes and rebuild", { @@ -78,14 +78,15 @@ await new Command() // plug:run .command("plug:run", "Run a PlugOS function from the CLI") .arguments(" [function] [...args:string]") - .option("--noIndex [type:boolean]", "Do not run a full space index first", { - default: false, - }) .option( "--hostname, -L ", "Hostname or address to listen on", ) .option("-p, --port ", "Port to listen on") + .option( + "--db ", + "Path to database file", + ) .action(plugRunCommand) .command("user:add", "Add a new user to an authentication file") .arguments("[username:string]") diff --git a/web/boot.ts b/web/boot.ts index b0d5dad6..e15f684e 100644 --- a/web/boot.ts +++ b/web/boot.ts @@ -1,7 +1,7 @@ import { safeRun } from "../common/util.ts"; import { Client } from "./client.ts"; -const syncMode = window.silverBulletConfig.supportOnlineMode !== "true" || +const syncMode = window.silverBulletConfig.syncOnly || !!localStorage.getItem("syncMode"); safeRun(async () => { diff --git a/web/client.ts b/web/client.ts index fd00e4e4..2fef70a0 100644 --- a/web/client.ts +++ b/web/client.ts @@ -52,7 +52,7 @@ declare global { // Injected via index.html silverBulletConfig: { spaceFolderPath: string; - supportOnlineMode: string; + syncOnly: boolean; }; client: Client; } diff --git a/web/components/top_bar.tsx b/web/components/top_bar.tsx index 635c2aae..174a6c15 100644 --- a/web/components/top_bar.tsx +++ b/web/components/top_bar.tsx @@ -46,8 +46,6 @@ export function TopBar({ lhs?: ComponentChildren; rhs?: ComponentChildren; }) { - const inputRef = useRef(null); - // Another one of my less proud moments: // Somehow I cannot seem to proerply limit the width of the page name, so I'm doing // it this way. If you have a better way to do this, please let me know! @@ -66,7 +64,7 @@ export function TopBar({ // Then calculate a new width currentPageElement.style.width = `${ - Math.min(editorWidth - 150, innerDiv.clientWidth - 150) + Math.min(editorWidth - 170, innerDiv.clientWidth - 170) }px`; } } diff --git a/web/editor_ui.tsx b/web/editor_ui.tsx index f271c076..95e56fb7 100644 --- a/web/editor_ui.tsx +++ b/web/editor_ui.tsx @@ -204,7 +204,8 @@ export class MainUI { editor.focus(); }} actionButtons={[ - ...window.silverBulletConfig.supportOnlineMode === "true" + ...!window.silverBulletConfig.syncOnly + // If we support syncOnly, don't show this toggle button ? [{ icon: RefreshCwIcon, description: this.editor.syncMode diff --git a/web/index.html b/web/index.html index 9591e439..9c09fcf1 100644 --- a/web/index.html +++ b/web/index.html @@ -35,13 +35,13 @@ window.silverBulletConfig = { // These {{VARIABLES}} are replaced by http_server.ts spaceFolderPath: "{{SPACE_PATH}}", - supportOnlineMode: "{{SUPPORT_ONLINE_MODE}}", + syncOnly: "{{SYNC_ONLY}}" === "true", }; - // But in case these variables aren't replaced by the server, fall back fully static mode (no sync) + // But in case these variables aren't replaced by the server, fall back sync only mode if (window.silverBulletConfig.spaceFolderPath.includes("{{")) { window.silverBulletConfig = { spaceFolderPath: "", - supportOnlineMode: false, + syncOnly: true, }; } diff --git a/web/styles/main.scss b/web/styles/main.scss index d2d7eea5..5ed6cbea 100644 --- a/web/styles/main.scss +++ b/web/styles/main.scss @@ -121,7 +121,9 @@ body { display: inline-block; position: relative; top: -6px; - padding-right: 6px; + padding: 4px; + background-color: var(--top-background-color); + margin-right: -2px; } .progress-bar { diff --git a/website/Install.md b/website/Install.md index 4996240a..1feb20fd 100644 --- a/website/Install.md +++ b/website/Install.md @@ -130,6 +130,7 @@ You can configure SB with environment variables instead of flags as well. The fo * `SB_PORT`: Sets the port to listen to, e.g. `SB_PORT=1234` * `SB_FOLDER`: Sets the folder to expose, e.g. `SB_FOLDER=/space` * `SB_AUTH`: Loads an [[Authentication]] database from a (JSON encoded) string, e.g. `SB_AUTH=$(cat /path/to/.auth.json)` +* `SB_STORE_ONLY`: Runs the server in a "dumb" space store only mode (not indexing content or keeping other state), e.g. `SB_STORE_ONLY=1` ## Using Authelia You need to adjust a few configuration options in [[Authelia]] in order for SilverBullet to work as intended.