Fixes #161 by implementing read-only mode, first iteration

pull/662/head
Zef Hemel 2024-01-26 17:05:10 +01:00
parent 1d3bc9cf44
commit 5bc7193fb0
43 changed files with 416 additions and 357 deletions

View File

@ -42,9 +42,6 @@ export async function copyAssets(dist: string) {
await copy("web/auth.html", `${dist}/auth.html`, { await copy("web/auth.html", `${dist}/auth.html`, {
overwrite: true, overwrite: true,
}); });
await copy("web/logout.html", `${dist}/logout.html`, {
overwrite: true,
});
await copy("web/images/favicon.png", `${dist}/favicon.png`, { await copy("web/images/favicon.png", `${dist}/favicon.png`, {
overwrite: true, overwrite: true,
}); });
@ -84,7 +81,7 @@ async function buildCopyBundleAssets() {
console.log("Now ESBuilding the client and service workers..."); console.log("Now ESBuilding the client and service workers...");
await esbuild.build({ const result = await esbuild.build({
entryPoints: [ entryPoints: [
{ {
in: "web/boot.ts", in: "web/boot.ts",
@ -102,6 +99,7 @@ async function buildCopyBundleAssets() {
sourcemap: "linked", sourcemap: "linked",
minify: true, minify: true,
jsxFactory: "h", jsxFactory: "h",
// metafile: true,
jsx: "automatic", jsx: "automatic",
jsxFragment: "Fragment", jsxFragment: "Fragment",
jsxImportSource: "https://esm.sh/preact@10.11.1", jsxImportSource: "https://esm.sh/preact@10.11.1",
@ -111,6 +109,11 @@ async function buildCopyBundleAssets() {
}), }),
}); });
if (result.metafile) {
const text = await esbuild.analyzeMetafile(result.metafile!);
console.log("Bundle info", text);
}
// Patch the service_worker {{CACHE_NAME}} // Patch the service_worker {{CACHE_NAME}}
let swCode = await Deno.readTextFile("dist_client_bundle/service_worker.js"); let swCode = await Deno.readTextFile("dist_client_bundle/service_worker.js");
swCode = swCode.replaceAll("{{CACHE_NAME}}", `cache-${Date.now()}`); swCode = swCode.replaceAll("{{CACHE_NAME}}", `cache-${Date.now()}`);

View File

@ -5,14 +5,12 @@ import { runPlug } from "./plug_run.ts";
import assets from "../dist/plug_asset_bundle.json" assert { type: "json" }; import assets from "../dist/plug_asset_bundle.json" assert { type: "json" };
import { assertEquals } from "../test_deps.ts"; import { assertEquals } from "../test_deps.ts";
import { path } from "../common/deps.ts"; import { path } from "../common/deps.ts";
import { MemoryKvPrimitives } from "../plugos/lib/memory_kv_primitives.ts";
Deno.test("Test plug run", { Deno.test("Test plug run", {
sanitizeResources: false, sanitizeResources: false,
sanitizeOps: false, sanitizeOps: false,
}, async () => { }, async () => {
// const tempDir = await Deno.makeTempDir();
const tempDbFile = await Deno.makeTempFile({ suffix: ".db" });
const assetBundle = new AssetBundle(assets); const assetBundle = new AssetBundle(assets);
const testFolder = path.dirname(new URL(import.meta.url).pathname); const testFolder = path.dirname(new URL(import.meta.url).pathname);
@ -31,11 +29,11 @@ Deno.test("Test plug run", {
"test.run", "test.run",
[], [],
assetBundle, assetBundle,
new MemoryKvPrimitives(),
), ),
"Hello", "Hello",
); );
// await Deno.remove(tempDir, { recursive: true }); // await Deno.remove(tempDir, { recursive: true });
esbuild.stop(); esbuild.stop();
await Deno.remove(tempDbFile);
}); });

View File

@ -4,29 +4,23 @@ import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
import { sleep } from "$sb/lib/async.ts"; import { sleep } from "$sb/lib/async.ts";
import { ServerSystem } from "../server/server_system.ts"; import { ServerSystem } from "../server/server_system.ts";
import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts"; import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts";
import { determineDatabaseBackend } from "../server/db_backend.ts";
import { EndpointHook } from "../plugos/hooks/endpoint.ts"; import { EndpointHook } from "../plugos/hooks/endpoint.ts";
import { determineShellBackend } from "../server/shell_backend.ts"; import { LocalShell } from "../server/shell_backend.ts";
import { Hono } from "../server/deps.ts"; import { Hono } from "../server/deps.ts";
import { KvPrimitives } from "../plugos/lib/kv_primitives.ts";
export async function runPlug( export async function runPlug(
spacePath: string, spacePath: string,
functionName: string | undefined, functionName: string | undefined,
args: string[] = [], args: string[] = [],
builtinAssetBundle: AssetBundle, builtinAssetBundle: AssetBundle,
httpServerPort = 3123, kvPrimitives: KvPrimitives,
httpHostname = "127.0.0.1", httpServerPort?: number,
httpHostname?: string,
) { ) {
const serverController = new AbortController(); const serverController = new AbortController();
const app = new Hono(); const app = new Hono();
const dbBackend = await determineDatabaseBackend(spacePath);
if (!dbBackend) {
console.error("Cannot run plugs in databaseless mode.");
return;
}
const endpointHook = new EndpointHook("/_/"); const endpointHook = new EndpointHook("/_/");
const serverSystem = new ServerSystem( const serverSystem = new ServerSystem(
@ -34,23 +28,25 @@ export async function runPlug(
new DiskSpacePrimitives(spacePath), new DiskSpacePrimitives(spacePath),
builtinAssetBundle, builtinAssetBundle,
), ),
dbBackend, kvPrimitives,
determineShellBackend(spacePath), new LocalShell(spacePath),
false,
); );
await serverSystem.init(true); await serverSystem.init(true);
app.use((context, next) => { app.use((context, next) => {
return endpointHook.handleRequest(serverSystem.system!, context, next); return endpointHook.handleRequest(serverSystem.system!, context, next);
}); });
Deno.serve({ if (httpHostname && httpServerPort) {
hostname: httpHostname, Deno.serve({
port: httpServerPort, hostname: httpHostname,
signal: serverController.signal, port: httpServerPort,
}, app.fetch); signal: serverController.signal,
}, app.fetch);
}
if (functionName) { if (functionName) {
const result = await serverSystem.system.invokeFunction(functionName, args); const result = await serverSystem.system.invokeFunction(functionName, args);
await serverSystem.close(); await serverSystem.close();
serverSystem.kvPrimitives.close();
serverController.abort(); serverController.abort();
return result; return result;
} else { } else {

View File

@ -4,6 +4,7 @@ import assets from "../dist/plug_asset_bundle.json" assert {
type: "json", type: "json",
}; };
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts"; import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
import { determineDatabaseBackend } from "../server/db_backend.ts";
export async function plugRunCommand( export async function plugRunCommand(
{ {
@ -20,18 +21,28 @@ export async function plugRunCommand(
spacePath = path.resolve(spacePath); spacePath = path.resolve(spacePath);
console.log("Space path", spacePath); console.log("Space path", spacePath);
console.log("Function to run:", functionName, "with arguments", args); console.log("Function to run:", functionName, "with arguments", args);
const kvPrimitives = await determineDatabaseBackend(spacePath);
if (!kvPrimitives) {
console.error("Cannot run plugs in databaseless mode.");
return;
}
try { try {
const result = await runPlug( const result = await runPlug(
spacePath, spacePath,
functionName, functionName,
args, args,
new AssetBundle(assets), new AssetBundle(assets),
kvPrimitives,
port, port,
hostname, hostname,
); );
if (result) { if (result) {
console.log("Output", result); console.log("Output", result);
} }
kvPrimitives.close();
Deno.exit(0); Deno.exit(0);
} catch (e: any) { } catch (e: any) {
console.error(e.message); console.error(e.message);

View File

@ -10,6 +10,8 @@ import { sleep } from "$sb/lib/async.ts";
import { determineDatabaseBackend } from "../server/db_backend.ts"; import { determineDatabaseBackend } from "../server/db_backend.ts";
import { SpaceServerConfig } from "../server/instance.ts"; import { SpaceServerConfig } from "../server/instance.ts";
import { runPlug } from "../cli/plug_run.ts";
import { PrefixedKvPrimitives } from "../plugos/lib/prefixed_kv_primitives.ts";
export async function serveCommand( export async function serveCommand(
options: { options: {
@ -41,6 +43,8 @@ export async function serveCommand(
const syncOnly = options.syncOnly || !!Deno.env.get("SB_SYNC_ONLY"); const syncOnly = options.syncOnly || !!Deno.env.get("SB_SYNC_ONLY");
const readOnly = !!Deno.env.get("SB_READ_ONLY");
if (syncOnly) { if (syncOnly) {
console.log("Running in sync-only mode (no backend processing)"); console.log("Running in sync-only mode (no backend processing)");
} }
@ -75,6 +79,9 @@ export async function serveCommand(
const [user, pass] = userAuth.split(":"); const [user, pass] = userAuth.split(":");
userCredentials = { user, pass }; userCredentials = { user, pass };
} }
const backendConfig = Deno.env.get("SB_SHELL_BACKEND") || "local";
const configs = new Map<string, SpaceServerConfig>(); const configs = new Map<string, SpaceServerConfig>();
configs.set("*", { configs.set("*", {
hostname, hostname,
@ -82,15 +89,29 @@ export async function serveCommand(
auth: userCredentials, auth: userCredentials,
authToken: Deno.env.get("SB_AUTH_TOKEN"), authToken: Deno.env.get("SB_AUTH_TOKEN"),
syncOnly, syncOnly,
readOnly,
shellBackend: backendConfig,
clientEncryption, clientEncryption,
pagesPath: folder, pagesPath: folder,
}); });
const plugAssets = new AssetBundle(plugAssetBundle as AssetJson);
if (readOnly) {
console.log("Indexing the space first. Hang on...");
await runPlug(
folder,
"index.reindexSpace",
[],
plugAssets,
new PrefixedKvPrimitives(baseKvPrimitives, ["*"]),
);
}
const httpServer = new HttpServer({ const httpServer = new HttpServer({
hostname, hostname,
port, port,
clientAssetBundle: new AssetBundle(clientAssetBundle as AssetJson), clientAssetBundle: new AssetBundle(clientAssetBundle as AssetJson),
plugAssetBundle: new AssetBundle(plugAssetBundle as AssetJson), plugAssetBundle: plugAssets,
baseKvPrimitives, baseKvPrimitives,
keyFile: options.key, keyFile: options.key,
certFile: options.cert, certFile: options.cert,

View File

@ -0,0 +1,38 @@
import { FileMeta } from "$sb/types.ts";
import { SpacePrimitives } from "./space_primitives.ts";
export class ReadOnlySpacePrimitives implements SpacePrimitives {
wrapped: SpacePrimitives;
constructor(wrapped: SpacePrimitives) {
this.wrapped = wrapped;
}
async fetchFileList(): Promise<FileMeta[]> {
return (await this.wrapped.fetchFileList()).map((f: FileMeta) => ({
...f,
perm: "ro",
}));
}
async readFile(name: string): Promise<{ meta: FileMeta; data: Uint8Array }> {
const { meta, data } = await this.wrapped.readFile(name);
return {
meta: {
...meta,
perm: "ro",
},
data,
};
}
async getFileMeta(name: string): Promise<FileMeta> {
const meta = await this.wrapped.getFileMeta(name);
return {
...meta,
perm: "ro",
};
}
writeFile(): Promise<FileMeta> {
throw new Error("Read only space, not allowed to write");
}
deleteFile(): Promise<void> {
throw new Error("Read only space, not allowed to delete");
}
}

View File

@ -1,10 +1,11 @@
import { SysCallMapping, System } from "../../plugos/system.ts"; import { SysCallMapping, System } from "../../plugos/system.ts";
import type { Client } from "../client.ts"; import type { Client } from "../../web/client.ts";
import { CommandDef } from "../hooks/command.ts"; import { CommandDef } from "../../web/hooks/command.ts";
import { proxySyscall } from "./util.ts"; import { proxySyscall } from "../../web/syscalls/util.ts";
export function systemSyscalls( export function systemSyscalls(
system: System<any>, system: System<any>,
readOnlyMode: boolean,
client?: Client, client?: Client,
): SysCallMapping { ): SysCallMapping {
const api: SysCallMapping = { const api: SysCallMapping = {
@ -64,6 +65,9 @@ export function systemSyscalls(
"system.getEnv": () => { "system.getEnv": () => {
return system.env; return system.env;
}, },
"system.getMode": () => {
return readOnlyMode ? "ro" : "rw";
},
}; };
return api; return api;
} }

View File

@ -3,3 +3,10 @@ import { syscall } from "./syscall.ts";
export function resetClient() { export function resetClient() {
return syscall("debug.resetClient"); return syscall("debug.resetClient");
} }
/**
* Wipes the entire state KV store and the entire space KV store.
*/
export function cleanup() {
return syscall("debug.cleanup");
}

View File

@ -26,3 +26,7 @@ export function reloadPlugs() {
export function getEnv(): Promise<string | undefined> { export function getEnv(): Promise<string | undefined> {
return syscall("system.getEnv"); return syscall("system.getEnv");
} }
export function getMode(): Promise<"ro" | "rw"> {
return syscall("system.getMode");
}

View File

@ -101,11 +101,6 @@ setupMessageListener(functionMapping, manifest);
metafile: options.info, metafile: options.info,
treeShaking: true, treeShaking: true,
plugins: [ plugins: [
// {
// name: "json",
// setup: (build) =>
// build.onLoad({ filter: /\.json$/ }, () => ({ loader: "json" })),
// },
...denoPlugins({ ...denoPlugins({
// TODO do this differently // TODO do this differently
importMapURL: options.importMap || importMapURL: options.importMap ||

View File

@ -7,7 +7,26 @@ import type { SysCallMapping } from "../system.ts";
* @param ds the datastore to wrap * @param ds the datastore to wrap
* @param prefix prefix to scope all keys to to which the plug name will be appended * @param prefix prefix to scope all keys to to which the plug name will be appended
*/ */
export function dataStoreSyscalls(ds: DataStore): SysCallMapping { export function dataStoreReadSyscalls(ds: DataStore): SysCallMapping {
return {
"datastore.batchGet": (
_ctx,
keys: KvKey[],
): Promise<(any | undefined)[]> => {
return ds.batchGet(keys);
},
"datastore.get": (_ctx, key: KvKey): Promise<any | null> => {
return ds.get(key);
},
"datastore.query": async (_ctx, query: KvQuery): Promise<KV[]> => {
return (await ds.query(query));
},
};
}
export function dataStoreWriteSyscalls(ds: DataStore): SysCallMapping {
return { return {
"datastore.delete": (_ctx, key: KvKey) => { "datastore.delete": (_ctx, key: KvKey) => {
return ds.delete(key); return ds.delete(key);
@ -25,21 +44,6 @@ export function dataStoreSyscalls(ds: DataStore): SysCallMapping {
return ds.batchDelete(keys); return ds.batchDelete(keys);
}, },
"datastore.batchGet": (
_ctx,
keys: KvKey[],
): Promise<(any | undefined)[]> => {
return ds.batchGet(keys);
},
"datastore.get": (_ctx, key: KvKey): Promise<any | null> => {
return ds.get(key);
},
"datastore.query": async (_ctx, query: KvQuery): Promise<KV[]> => {
return (await ds.query(query));
},
"datastore.queryDelete": (_ctx, query: KvQuery): Promise<void> => { "datastore.queryDelete": (_ctx, query: KvQuery): Promise<void> => {
return ds.queryDelete(query); return ds.queryDelete(query);
}, },

View File

@ -1,5 +0,0 @@
import { editor } from "$sb/syscalls.ts";
export async function accountLogoutCommand() {
await editor.openUrl("/.client/logout.html", true);
}

View File

@ -1,58 +0,0 @@
import { traverseTree } from "../../plug-api/lib/tree.ts";
import { editor, markdown, space } from "$sb/syscalls.ts";
import { parsePageRef } from "$sb/lib/page.ts";
export async function brokenLinksCommand() {
const pageName = "BROKEN LINKS";
await editor.flashNotification("Scanning your space...");
const allPages = await space.listPages();
const allPagesMap = new Map(allPages.map((p) => [p.name, true]));
const brokenLinks: { page: string; link: string; pos: number }[] = [];
for (const pageMeta of allPages) {
const text = await space.readPage(pageMeta.name);
const tree = await markdown.parseMarkdown(text);
traverseTree(tree, (tree) => {
if (tree.type === "WikiLinkPage") {
// Add the prefix in the link text
const { page: pageName } = parsePageRef(tree.children![0].text!);
if (pageName.startsWith("!")) {
return true;
}
if (
pageName && !pageName.startsWith("{{")
) {
if (!allPagesMap.has(pageName)) {
brokenLinks.push({
page: pageMeta.name,
link: pageName,
pos: tree.from!,
});
}
}
}
if (tree.type === "PageRef") {
const pageName = tree.children![0].text!.slice(2, -2);
if (pageName.startsWith("!")) {
return true;
}
if (!allPagesMap.has(pageName)) {
brokenLinks.push({
page: pageMeta.name,
link: pageName,
pos: tree.from!,
});
}
}
return false;
});
}
const lines: string[] = [];
for (const brokenLink of brokenLinks) {
lines.push(
`* [[${brokenLink.page}@${brokenLink.pos}]]: ${brokenLink.link}`,
);
}
await space.writePage(pageName, lines.join("\n"));
await editor.navigate({ page: pageName });
}

14
plugs/editor/clean.ts Normal file
View File

@ -0,0 +1,14 @@
import { debug, editor } from "$sb/syscalls.ts";
export async function cleanCommand() {
if (
!await editor.confirm(
"This will remove all your locally cached data and authentication cookies. Are you sure?",
)
) {
return;
}
await editor.flashNotification("Now wiping all state and logging out...");
await debug.cleanup();
await editor.openUrl("/.auth?logout", true);
}

View File

@ -35,10 +35,12 @@ functions:
path: "./page.ts:deletePage" path: "./page.ts:deletePage"
command: command:
name: "Page: Delete" name: "Page: Delete"
requireMode: rw
copyPage: copyPage:
path: "./page.ts:copyPage" path: "./page.ts:copyPage"
command: command:
name: "Page: Copy" name: "Page: Copy"
requireMode: rw
# Completion # Completion
pageComplete: pageComplete:
@ -54,6 +56,7 @@ functions:
path: editor.ts:reloadSettingsAndCommands path: editor.ts:reloadSettingsAndCommands
command: command:
name: "System: Reload Settings and Commands" name: "System: Reload Settings and Commands"
requireMode: rw
# Navigation # Navigation
linkNavigate: linkNavigate:
@ -89,22 +92,26 @@ functions:
name: "Text: Quote Selection" name: "Text: Quote Selection"
key: "Ctrl-Shift-." key: "Ctrl-Shift-."
mac: "Cmd-Shift-." mac: "Cmd-Shift-."
requireMode: rw
listifySelection: listifySelection:
path: ./text.ts:listifySelection path: ./text.ts:listifySelection
command: command:
name: "Text: Listify Selection" name: "Text: Listify Selection"
key: "Ctrl-Shift-8" key: "Ctrl-Shift-8"
mac: "Cmd-Shift-8" mac: "Cmd-Shift-8"
requireMode: rw
numberListifySelection: numberListifySelection:
path: ./text.ts:numberListifySelection path: ./text.ts:numberListifySelection
command: command:
name: "Text: Number Listify Selection" name: "Text: Number Listify Selection"
requireMode: rw
linkSelection: linkSelection:
path: ./text.ts:linkSelection path: ./text.ts:linkSelection
command: command:
name: "Text: Link Selection" name: "Text: Link Selection"
key: "Ctrl-Shift-k" key: "Ctrl-Shift-k"
mac: "Cmd-Shift-k" mac: "Cmd-Shift-k"
requireMode: rw
bold: bold:
path: ./text.ts:wrapSelection path: ./text.ts:wrapSelection
command: command:
@ -112,6 +119,7 @@ functions:
key: "Ctrl-b" key: "Ctrl-b"
mac: "Cmd-b" mac: "Cmd-b"
wrapper: "**" wrapper: "**"
requireMode: rw
italic: italic:
path: ./text.ts:wrapSelection path: ./text.ts:wrapSelection
command: command:
@ -119,23 +127,27 @@ functions:
key: "Ctrl-i" key: "Ctrl-i"
mac: "Cmd-i" mac: "Cmd-i"
wrapper: "_" wrapper: "_"
requireMode: rw
strikethrough: strikethrough:
path: ./text.ts:wrapSelection path: ./text.ts:wrapSelection
command: command:
name: "Text: Strikethrough" name: "Text: Strikethrough"
key: "Ctrl-Shift-s" key: "Ctrl-Shift-s"
wrapper: "~~" wrapper: "~~"
requireMode: rw
marker: marker:
path: ./text.ts:wrapSelection path: ./text.ts:wrapSelection
command: command:
name: "Text: Marker" name: "Text: Marker"
key: "Alt-m" key: "Alt-m"
wrapper: "==" wrapper: "=="
requireMode: rw
centerCursor: centerCursor:
path: "./editor.ts:centerCursorCommand" path: "./editor.ts:centerCursorCommand"
command: command:
name: "Navigate: Center Cursor" name: "Navigate: Center Cursor"
key: "Ctrl-Alt-l" key: "Ctrl-Alt-l"
requireMode: rw
# Debug commands # Debug commands
parseCommand: parseCommand:
@ -150,6 +162,7 @@ functions:
name: "Link: Unfurl" name: "Link: Unfurl"
key: "Ctrl-Shift-u" key: "Ctrl-Shift-u"
mac: "Cmd-Shift-u" mac: "Cmd-Shift-u"
requireMode: rw
contexts: contexts:
- NakedURL - NakedURL
@ -180,11 +193,6 @@ functions:
events: events:
- editor:modeswitch - editor:modeswitch
brokenLinksCommand:
path: ./broken_links.ts:brokenLinksCommand
command:
name: "Broken Links: Show"
# Random stuff # Random stuff
statsCommand: statsCommand:
path: ./stats.ts:statsCommand path: ./stats.ts:statsCommand
@ -210,14 +218,15 @@ functions:
name: "Help: Getting Started" name: "Help: Getting Started"
accountLogoutCommand: accountLogoutCommand:
path: ./account.ts:accountLogoutCommand path: clean.ts:cleanCommand
command: command:
name: "Account: Logout" name: "Clear Local Storage & Logout"
uploadFileCommand: uploadFileCommand:
path: ./upload.ts:uploadFile path: ./upload.ts:uploadFile
command: command:
name: "Upload: File" name: "Upload: File"
requireMode: rw
# Outline commands # Outline commands
outlineMoveUp: outlineMoveUp:
@ -225,24 +234,28 @@ functions:
command: command:
name: "Outline: Move Up" name: "Outline: Move Up"
key: "Alt-ArrowUp" key: "Alt-ArrowUp"
requireMode: rw
outlineMoveDown: outlineMoveDown:
path: ./outline.ts:moveItemDown path: ./outline.ts:moveItemDown
command: command:
name: "Outline: Move Down" name: "Outline: Move Down"
key: "Alt-ArrowDown" key: "Alt-ArrowDown"
requireMode: rw
outlineIndent: outlineIndent:
path: ./outline.ts:indentItem path: ./outline.ts:indentItem
command: command:
name: "Outline: Move Right" name: "Outline: Move Right"
key: "Alt->" key: "Alt->"
requireMode: rw
outlineOutdent: outlineOutdent:
path: ./outline.ts:outdentItem path: ./outline.ts:outdentItem
command: command:
name: "Outline: Move Left" name: "Outline: Move Left"
key: "Alt-<" key: "Alt-<"
requireMode: rw
# Outline folding commands # Outline folding commands
foldCommand: foldCommand:

View File

@ -33,4 +33,5 @@ functions:
importLibraryCommand: importLibraryCommand:
path: library.ts:importLibraryCommand path: library.ts:importLibraryCommand
command: command:
name: "Library: Import" name: "Library: Import"
requireMode: rw

View File

@ -2,7 +2,7 @@ import { datastore } from "$sb/syscalls.ts";
import { KV, KvKey, KvQuery, ObjectQuery, ObjectValue } from "$sb/types.ts"; import { KV, KvKey, KvQuery, ObjectQuery, ObjectValue } from "$sb/types.ts";
import { QueryProviderEvent } from "$sb/app_event.ts"; import { QueryProviderEvent } from "$sb/app_event.ts";
import { builtins } from "./builtins.ts"; import { builtins } from "./builtins.ts";
import { AttributeObject, determineType } from "./attributes.ts"; import { determineType } from "./attributes.ts";
import { ttlCache } from "$sb/lib/memory_cache.ts"; import { ttlCache } from "$sb/lib/memory_cache.ts";
const indexKey = "idx"; const indexKey = "idx";
@ -47,12 +47,17 @@ export async function clearPageIndex(page: string): Promise<void> {
} }
/** /**
* Clears the entire datastore for this indexKey plug * Clears the entire page index
*/ */
export async function clearIndex(): Promise<void> { export async function clearIndex(): Promise<void> {
const allKeys: KvKey[] = []; const allKeys: KvKey[] = [];
for ( for (
const { key } of await datastore.query({ prefix: [] }) const { key } of await datastore.query({ prefix: [indexKey] })
) {
allKeys.push(key);
}
for (
const { key } of await datastore.query({ prefix: [pageKey] })
) { ) {
allKeys.push(key); allKeys.push(key);
} }

View File

@ -1,7 +1,6 @@
import { ObjectValue } from "$sb/types.ts"; import { ObjectValue } from "$sb/types.ts";
import { system } from "$sb/syscalls.ts";
import { indexObjects } from "./api.ts"; import { indexObjects } from "./api.ts";
import { AttributeObject } from "./attributes.ts";
import { TagObject } from "./tags.ts";
export const builtinPseudoPage = ":builtin:"; export const builtinPseudoPage = ":builtin:";
@ -92,6 +91,10 @@ export const builtins: Record<string, Record<string, string>> = {
}; };
export async function loadBuiltinsIntoIndex() { export async function loadBuiltinsIntoIndex() {
if (await system.getMode() === "ro") {
console.log("Running in read-only mode, not loading builtins");
return;
}
console.log("Loading builtins attributes into index"); console.log("Loading builtins attributes into index");
const allObjects: ObjectValue<any>[] = []; const allObjects: ObjectValue<any>[] = [];
for (const [tagName, attributes] of Object.entries(builtins)) { for (const [tagName, attributes] of Object.entries(builtins)) {

View File

@ -11,6 +11,10 @@ export async function reindexCommand() {
} }
export async function reindexSpace() { export async function reindexSpace() {
if (await system.getMode() === "ro") {
console.info("Not reindexing because we're in read-only mode");
return;
}
console.log("Clearing page index..."); console.log("Clearing page index...");
// Executed this way to not have to embed the search plug code here // Executed this way to not have to embed the search plug code here
await system.invokeFunction("index.clearIndex"); await system.invokeFunction("index.clearIndex");
@ -55,6 +59,10 @@ export async function processIndexQueue(messages: MQMessage[]) {
} }
export async function parseIndexTextRepublish({ name, text }: IndexEvent) { export async function parseIndexTextRepublish({ name, text }: IndexEvent) {
if (await system.getMode() === "ro") {
console.info("Not reindexing", name, "because we're in read-only mode");
return;
}
const parsed = await markdown.parseMarkdown(text); const parsed = await markdown.parseMarkdown(text);
if (isTemplate(text)) { if (isTemplate(text)) {

View File

@ -51,6 +51,7 @@ functions:
path: "./command.ts:reindexCommand" path: "./command.ts:reindexCommand"
command: command:
name: "Space: Reindex" name: "Space: Reindex"
requireMode: rw
processIndexQueue: processIndexQueue:
path: ./command.ts:processIndexQueue path: ./command.ts:processIndexQueue
mqSubscriptions: mqSubscriptions:
@ -142,16 +143,19 @@ functions:
mac: Cmd-Alt-r mac: Cmd-Alt-r
key: Ctrl-Alt-r key: Ctrl-Alt-r
page: "" page: ""
requireMode: rw
renamePrefixCommand: renamePrefixCommand:
path: "./refactor.ts:renamePrefixCommand" path: "./refactor.ts:renamePrefixCommand"
command: command:
name: "Page: Batch Rename Prefix" name: "Page: Batch Rename Prefix"
requireMode: rw
# Refactoring Commands # Refactoring Commands
extractToPageCommand: extractToPageCommand:
path: ./refactor.ts:extractToPageCommand path: ./refactor.ts:extractToPageCommand
command: command:
name: "Page: Extract" name: "Page: Extract"
requireMode: rw
# TOC # TOC
tocWidget: tocWidget:

View File

@ -8,6 +8,7 @@ functions:
name: "Plugs: Update" name: "Plugs: Update"
key: "Ctrl-Shift-p" key: "Ctrl-Shift-p"
mac: "Cmd-Shift-p" mac: "Cmd-Shift-p"
requireMode: rw
getPlugHTTPS: getPlugHTTPS:
path: "./plugmanager.ts:getPlugHTTPS" path: "./plugmanager.ts:getPlugHTTPS"
events: events:
@ -24,3 +25,4 @@ functions:
path: ./plugmanager.ts:addPlugCommand path: ./plugmanager.ts:addPlugCommand
command: command:
name: "Plugs: Add" name: "Plugs: Add"
requireMode: rw

View File

@ -5,4 +5,5 @@ functions:
command: command:
name: "Share: Publish" name: "Share: Publish"
key: "Ctrl-s" key: "Ctrl-s"
mac: "Cmd-s" mac: "Cmd-s"
requireMode: rw

View File

@ -17,11 +17,13 @@ functions:
command: command:
name: "Task: Cycle State" name: "Task: Cycle State"
key: Alt-t key: Alt-t
requireMode: rw
taskPostponeCommand: taskPostponeCommand:
path: ./task.ts:postponeCommand path: ./task.ts:postponeCommand
command: command:
name: "Task: Postpone" name: "Task: Postpone"
key: Alt-+ key: Alt-+
requireMode: rw
contexts: contexts:
- DeadlineDate - DeadlineDate
previewTaskToggle: previewTaskToggle:
@ -37,4 +39,5 @@ functions:
removeCompletedTasksCommand: removeCompletedTasksCommand:
path: task.ts:removeCompletedTasksCommand path: task.ts:removeCompletedTasksCommand
command: command:
name: "Task: Remove Completed" name: "Task: Remove Completed"
requireMode: rw

View File

@ -44,6 +44,7 @@ functions:
command: command:
name: "Page: From Template" name: "Page: From Template"
key: "Alt-Shift-t" key: "Alt-Shift-t"
requireMode: rw
# Lint # Lint

View File

@ -9,7 +9,6 @@ import {
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts"; import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
import { FileMeta } from "$sb/types.ts"; import { FileMeta } from "$sb/types.ts";
import { ShellRequest } from "./rpc.ts"; import { ShellRequest } from "./rpc.ts";
import { determineShellBackend } from "./shell_backend.ts";
import { SpaceServer, SpaceServerConfig } from "./instance.ts"; import { SpaceServer, SpaceServerConfig } from "./instance.ts";
import { KvPrimitives } from "../plugos/lib/kv_primitives.ts"; import { KvPrimitives } from "../plugos/lib/kv_primitives.ts";
import { PrefixedKvPrimitives } from "../plugos/lib/prefixed_kv_primitives.ts"; import { PrefixedKvPrimitives } from "../plugos/lib/prefixed_kv_primitives.ts";
@ -58,7 +57,6 @@ export class HttpServer {
async bootSpaceServer(config: SpaceServerConfig): Promise<SpaceServer> { async bootSpaceServer(config: SpaceServerConfig): Promise<SpaceServer> {
const spaceServer = new SpaceServer( const spaceServer = new SpaceServer(
config, config,
determineShellBackend(config.pagesPath),
this.plugAssetBundle, this.plugAssetBundle,
new PrefixedKvPrimitives(this.baseKvPrimitives, [ new PrefixedKvPrimitives(this.baseKvPrimitives, [
config.namespace, config.namespace,
@ -119,6 +117,9 @@ export class HttpServer {
).replaceAll( ).replaceAll(
"{{SYNC_ONLY}}", "{{SYNC_ONLY}}",
spaceServer.syncOnly ? "true" : "false", spaceServer.syncOnly ? "true" : "false",
).replaceAll(
"{{READ_ONLY}}",
spaceServer.readOnly ? "true" : "false",
).replaceAll( ).replaceAll(
"{{CLIENT_ENCRYPTION}}", "{{CLIENT_ENCRYPTION}}",
spaceServer.clientEncryption ? "true" : "false", spaceServer.clientEncryption ? "true" : "false",
@ -217,6 +218,7 @@ export class HttpServer {
JSON.stringify([ JSON.stringify([
spaceServer.clientEncryption, spaceServer.clientEncryption,
spaceServer.syncOnly, spaceServer.syncOnly,
spaceServer.readOnly,
]), ]),
), ),
); );
@ -510,7 +512,10 @@ export class HttpServer {
const req = c.req; const req = c.req;
const name = req.param("path")!; const name = req.param("path")!;
const spaceServer = await this.ensureSpaceServer(req); const spaceServer = await this.ensureSpaceServer(req);
console.log("Saving file", name); if (spaceServer.readOnly) {
return c.text("Read only mode, no writes allowed", 405);
}
console.log("Writing file", name);
if (name.startsWith(".")) { if (name.startsWith(".")) {
// Don't expose hidden files // Don't expose hidden files
return c.text("Forbidden", 403); return c.text("Forbidden", 403);
@ -533,6 +538,9 @@ export class HttpServer {
const req = c.req; const req = c.req;
const name = req.param("path")!; const name = req.param("path")!;
const spaceServer = await this.ensureSpaceServer(req); const spaceServer = await this.ensureSpaceServer(req);
if (spaceServer.readOnly) {
return c.text("Read only mode, no writes allowed", 405);
}
console.log("Deleting file", name); console.log("Deleting file", name);
if (name.startsWith(".")) { if (name.startsWith(".")) {
// Don't expose hidden files // Don't expose hidden files

View File

@ -1,6 +1,7 @@
import { SilverBulletHooks } from "../common/manifest.ts"; import { SilverBulletHooks } from "../common/manifest.ts";
import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts"; import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts";
import { FilteredSpacePrimitives } from "../common/spaces/filtered_space_primitives.ts"; import { FilteredSpacePrimitives } from "../common/spaces/filtered_space_primitives.ts";
import { ReadOnlySpacePrimitives } from "../common/spaces/ro_space_primitives.ts";
import { SpacePrimitives } from "../common/spaces/space_primitives.ts"; import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
import { ensureAndLoadSettingsAndIndex } from "../common/util.ts"; import { ensureAndLoadSettingsAndIndex } from "../common/util.ts";
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts"; import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
@ -10,6 +11,7 @@ import { BuiltinSettings } from "../web/types.ts";
import { JWTIssuer } from "./crypto.ts"; import { JWTIssuer } from "./crypto.ts";
import { gitIgnoreCompiler } from "./deps.ts"; import { gitIgnoreCompiler } from "./deps.ts";
import { ServerSystem } from "./server_system.ts"; import { ServerSystem } from "./server_system.ts";
import { determineShellBackend, NotSupportedShell } from "./shell_backend.ts";
import { ShellBackend } from "./shell_backend.ts"; import { ShellBackend } from "./shell_backend.ts";
import { determineStorageBackend } from "./storage_backend.ts"; import { determineStorageBackend } from "./storage_backend.ts";
@ -21,8 +23,10 @@ export type SpaceServerConfig = {
// Additional API auth token // Additional API auth token
authToken?: string; authToken?: string;
pagesPath: string; pagesPath: string;
syncOnly?: boolean; shellBackend: string;
clientEncryption?: boolean; syncOnly: boolean;
readOnly: boolean;
clientEncryption: boolean;
}; };
export class SpaceServer { export class SpaceServer {
@ -41,10 +45,11 @@ export class SpaceServer {
system?: System<SilverBulletHooks>; system?: System<SilverBulletHooks>;
clientEncryption: boolean; clientEncryption: boolean;
syncOnly: boolean; syncOnly: boolean;
readOnly: boolean;
shellBackend: ShellBackend;
constructor( constructor(
config: SpaceServerConfig, config: SpaceServerConfig,
public shellBackend: ShellBackend,
private plugAssetBundle: AssetBundle, private plugAssetBundle: AssetBundle,
private kvPrimitives: KvPrimitives, private kvPrimitives: KvPrimitives,
) { ) {
@ -53,13 +58,18 @@ export class SpaceServer {
this.auth = config.auth; this.auth = config.auth;
this.authToken = config.authToken; this.authToken = config.authToken;
this.clientEncryption = !!config.clientEncryption; this.clientEncryption = !!config.clientEncryption;
this.syncOnly = !!config.syncOnly; this.syncOnly = config.syncOnly;
this.readOnly = config.readOnly;
if (this.clientEncryption) { if (this.clientEncryption) {
// Sync only will forced on when encryption is enabled // Sync only will forced on when encryption is enabled
this.syncOnly = true; this.syncOnly = true;
} }
this.jwtIssuer = new JWTIssuer(kvPrimitives); this.jwtIssuer = new JWTIssuer(kvPrimitives);
this.shellBackend = config.readOnly
? new NotSupportedShell() // No shell for read only mode
: determineShellBackend(config);
} }
async init() { async init() {
@ -81,6 +91,10 @@ export class SpaceServer {
}, },
); );
if (this.readOnly) {
this.spacePrimitives = new ReadOnlySpacePrimitives(this.spacePrimitives);
}
// system = undefined in databaseless mode (no PlugOS instance on the server and no DB) // system = undefined in databaseless mode (no PlugOS instance on the server and no DB)
if (!this.syncOnly) { if (!this.syncOnly) {
// Enable server-side processing // Enable server-side processing
@ -88,6 +102,7 @@ export class SpaceServer {
this.spacePrimitives, this.spacePrimitives,
this.kvPrimitives, this.kvPrimitives,
this.shellBackend, this.shellBackend,
this.readOnly,
); );
this.serverSystem = serverSystem; this.serverSystem = serverSystem;
} }

View File

@ -11,10 +11,9 @@ import { eventSyscalls } from "../plugos/syscalls/event.ts";
import { mqSyscalls } from "../plugos/syscalls/mq.ts"; import { mqSyscalls } from "../plugos/syscalls/mq.ts";
import { System } from "../plugos/system.ts"; import { System } from "../plugos/system.ts";
import { Space } from "../web/space.ts"; import { Space } from "../web/space.ts";
import { debugSyscalls } from "../web/syscalls/debug.ts";
import { markdownSyscalls } from "../common/syscalls/markdown.ts"; import { markdownSyscalls } from "../common/syscalls/markdown.ts";
import { spaceSyscalls } from "./syscalls/space.ts"; import { spaceReadSyscalls, spaceWriteSyscalls } from "./syscalls/space.ts";
import { systemSyscalls } from "../web/syscalls/system.ts"; import { systemSyscalls } from "../common/syscalls/system.ts";
import { yamlSyscalls } from "../common/syscalls/yaml.ts"; import { yamlSyscalls } from "../common/syscalls/yaml.ts";
import { sandboxFetchSyscalls } from "../plugos/syscalls/fetch.ts"; import { sandboxFetchSyscalls } from "../plugos/syscalls/fetch.ts";
import { shellSyscalls } from "./syscalls/shell.ts"; import { shellSyscalls } from "./syscalls/shell.ts";
@ -22,7 +21,10 @@ import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
import { base64EncodedDataUrl } from "../plugos/asset_bundle/base64.ts"; import { base64EncodedDataUrl } from "../plugos/asset_bundle/base64.ts";
import { Plug } from "../plugos/plug.ts"; import { Plug } from "../plugos/plug.ts";
import { DataStore } from "../plugos/lib/datastore.ts"; import { DataStore } from "../plugos/lib/datastore.ts";
import { dataStoreSyscalls } from "../plugos/syscalls/datastore.ts"; import {
dataStoreReadSyscalls,
dataStoreWriteSyscalls,
} from "../plugos/syscalls/datastore.ts";
import { DataStoreMQ } from "../plugos/lib/mq.datastore.ts"; import { DataStoreMQ } from "../plugos/lib/mq.datastore.ts";
import { languageSyscalls } from "../common/syscalls/language.ts"; import { languageSyscalls } from "../common/syscalls/language.ts";
import { handlebarsSyscalls } from "../common/syscalls/handlebars.ts"; import { handlebarsSyscalls } from "../common/syscalls/handlebars.ts";
@ -65,6 +67,7 @@ export class ServerSystem {
private baseSpacePrimitives: SpacePrimitives, private baseSpacePrimitives: SpacePrimitives,
readonly kvPrimitives: KvPrimitives, readonly kvPrimitives: KvPrimitives,
private shellBackend: ShellBackend, private shellBackend: ShellBackend,
private readOnlyMode: boolean,
) { ) {
} }
@ -123,29 +126,37 @@ export class ServerSystem {
this.system.registerSyscalls( this.system.registerSyscalls(
[], [],
eventSyscalls(eventHook), eventSyscalls(eventHook),
spaceSyscalls(space), spaceReadSyscalls(space),
assetSyscalls(this.system), assetSyscalls(this.system),
yamlSyscalls(), yamlSyscalls(),
systemSyscalls(this.system), systemSyscalls(this.system, this.readOnlyMode),
mqSyscalls(mq), mqSyscalls(mq),
languageSyscalls(), languageSyscalls(),
handlebarsSyscalls(), handlebarsSyscalls(),
dataStoreSyscalls(this.ds), dataStoreReadSyscalls(this.ds),
debugSyscalls(),
codeWidgetSyscalls(codeWidgetHook), codeWidgetSyscalls(codeWidgetHook),
markdownSyscalls(), markdownSyscalls(),
); );
// Syscalls that require some additional permissions if (!this.readOnlyMode) {
this.system.registerSyscalls( // Write mode only
["fetch"], this.system.registerSyscalls(
sandboxFetchSyscalls(), [],
); spaceWriteSyscalls(space),
dataStoreWriteSyscalls(this.ds),
);
this.system.registerSyscalls( // Syscalls that require some additional permissions
["shell"], this.system.registerSyscalls(
shellSyscalls(this.shellBackend), ["fetch"],
); sandboxFetchSyscalls(),
);
this.system.registerSyscalls(
["shell"],
shellSyscalls(this.shellBackend),
);
}
await this.loadPlugs(); await this.loadPlugs();

View File

@ -1,3 +1,4 @@
import type { SpaceServerConfig } from "./instance.ts";
import { ShellRequest, ShellResponse } from "./rpc.ts"; import { ShellRequest, ShellResponse } from "./rpc.ts";
/** /**
@ -5,11 +6,13 @@ import { ShellRequest, ShellResponse } from "./rpc.ts";
* - SB_SHELL_BACKEND: "local" or "off" * - SB_SHELL_BACKEND: "local" or "off"
*/ */
export function determineShellBackend(path: string): ShellBackend { export function determineShellBackend(
spaceServerConfig: SpaceServerConfig,
): ShellBackend {
const backendConfig = Deno.env.get("SB_SHELL_BACKEND") || "local"; const backendConfig = Deno.env.get("SB_SHELL_BACKEND") || "local";
switch (backendConfig) { switch (backendConfig) {
case "local": case "local":
return new LocalShell(path); return new LocalShell(spaceServerConfig.pagesPath);
default: default:
console.info( console.info(
"Running in shellless mode, meaning shell commands are disabled", "Running in shellless mode, meaning shell commands are disabled",
@ -22,7 +25,7 @@ export interface ShellBackend {
handle(shellRequest: ShellRequest): Promise<ShellResponse>; handle(shellRequest: ShellRequest): Promise<ShellResponse>;
} }
class NotSupportedShell implements ShellBackend { export class NotSupportedShell implements ShellBackend {
handle(): Promise<ShellResponse> { handle(): Promise<ShellResponse> {
return Promise.resolve({ return Promise.resolve({
stdout: "", stdout: "",
@ -32,7 +35,7 @@ class NotSupportedShell implements ShellBackend {
} }
} }
class LocalShell implements ShellBackend { export class LocalShell implements ShellBackend {
constructor(private cwd: string) { constructor(private cwd: string) {
} }

View File

@ -5,7 +5,7 @@ import type { Space } from "../../web/space.ts";
/** /**
* Almost the same as web/syscalls/space.ts except leaving out client-specific stuff * Almost the same as web/syscalls/space.ts except leaving out client-specific stuff
*/ */
export function spaceSyscalls(space: Space): SysCallMapping { export function spaceReadSyscalls(space: Space): SysCallMapping {
return { return {
"space.listPages": (): Promise<PageMeta[]> => { "space.listPages": (): Promise<PageMeta[]> => {
return space.fetchPageList(); return space.fetchPageList();
@ -16,16 +16,6 @@ export function spaceSyscalls(space: Space): SysCallMapping {
"space.getPageMeta": (_ctx, name: string): Promise<PageMeta> => { "space.getPageMeta": (_ctx, name: string): Promise<PageMeta> => {
return space.getPageMeta(name); return space.getPageMeta(name);
}, },
"space.writePage": (
_ctx,
name: string,
text: string,
): Promise<PageMeta> => {
return space.writePage(name, text);
},
"space.deletePage": async (_ctx, name: string) => {
await space.deletePage(name);
},
"space.listPlugs": (): Promise<FileMeta[]> => { "space.listPlugs": (): Promise<FileMeta[]> => {
return space.listPlugs(); return space.listPlugs();
}, },
@ -41,16 +31,6 @@ export function spaceSyscalls(space: Space): SysCallMapping {
): Promise<AttachmentMeta> => { ): Promise<AttachmentMeta> => {
return await space.getAttachmentMeta(name); return await space.getAttachmentMeta(name);
}, },
"space.writeAttachment": (
_ctx,
name: string,
data: Uint8Array,
): Promise<AttachmentMeta> => {
return space.writeAttachment(name, data);
},
"space.deleteAttachment": async (_ctx, name: string) => {
await space.deleteAttachment(name);
},
// FS // FS
"space.listFiles": (): Promise<FileMeta[]> => { "space.listFiles": (): Promise<FileMeta[]> => {
@ -62,6 +42,31 @@ export function spaceSyscalls(space: Space): SysCallMapping {
"space.readFile": async (_ctx, name: string): Promise<Uint8Array> => { "space.readFile": async (_ctx, name: string): Promise<Uint8Array> => {
return (await space.spacePrimitives.readFile(name)).data; return (await space.spacePrimitives.readFile(name)).data;
}, },
};
}
export function spaceWriteSyscalls(space: Space): SysCallMapping {
return {
"space.writePage": (
_ctx,
name: string,
text: string,
): Promise<PageMeta> => {
return space.writePage(name, text);
},
"space.deletePage": async (_ctx, name: string) => {
await space.deletePage(name);
},
"space.writeAttachment": (
_ctx,
name: string,
data: Uint8Array,
): Promise<AttachmentMeta> => {
return space.writeAttachment(name, data);
},
"space.deleteAttachment": async (_ctx, name: string) => {
await space.deleteAttachment(name);
},
"space.writeFile": ( "space.writeFile": (
_ctx, _ctx,
name: string, name: string,

View File

@ -10,9 +10,14 @@ safeRun(async () => {
syncMode ? "in Sync Mode" : "in Online Mode", syncMode ? "in Sync Mode" : "in Online Mode",
); );
if (window.silverBulletConfig.readOnly) {
console.log("Running in read-only mode");
}
const client = new Client( const client = new Client(
document.getElementById("sb-root")!, document.getElementById("sb-root")!,
syncMode, syncMode,
window.silverBulletConfig.readOnly,
); );
window.client = client; window.client = client;
await client.init(); await client.init();

View File

@ -59,6 +59,8 @@ import { LimitedMap } from "$sb/lib/limited_map.ts";
import { renderHandlebarsTemplate } from "../common/syscalls/handlebars.ts"; import { renderHandlebarsTemplate } from "../common/syscalls/handlebars.ts";
import { buildQueryFunctions } from "../common/query_functions.ts"; import { buildQueryFunctions } from "../common/query_functions.ts";
import { PageRef } from "$sb/lib/page.ts"; import { PageRef } from "$sb/lib/page.ts";
import { ReadOnlySpacePrimitives } from "../common/spaces/ro_space_primitives.ts";
import { KvPrimitives } from "../plugos/lib/kv_primitives.ts";
const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/; const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/;
const autoSaveInterval = 1000; const autoSaveInterval = 1000;
@ -69,6 +71,7 @@ declare global {
silverBulletConfig: { silverBulletConfig: {
spaceFolderPath: string; spaceFolderPath: string;
syncOnly: boolean; syncOnly: boolean;
readOnly: boolean;
clientEncryption: boolean; clientEncryption: boolean;
}; };
client: Client; client: Client;
@ -87,7 +90,6 @@ export class Client {
private dbPrefix: string; private dbPrefix: string;
plugSpaceRemotePrimitives!: PlugSpacePrimitives; plugSpaceRemotePrimitives!: PlugSpacePrimitives;
// localSpacePrimitives!: FilteredSpacePrimitives;
httpSpacePrimitives!: HttpSpacePrimitives; httpSpacePrimitives!: HttpSpacePrimitives;
space!: Space; space!: Space;
@ -109,7 +111,7 @@ export class Client {
ui!: MainUI; ui!: MainUI;
stateDataStore!: DataStore; stateDataStore!: DataStore;
spaceDataStore!: DataStore; spaceKV?: KvPrimitives;
mq!: DataStoreMQ; mq!: DataStoreMQ;
// Used by the "wiki link" highlighter to check if a page exists // Used by the "wiki link" highlighter to check if a page exists
@ -118,7 +120,8 @@ export class Client {
constructor( constructor(
private parent: Element, private parent: Element,
public syncMode = false, public syncMode: boolean,
private readOnlyMode: boolean,
) { ) {
if (!syncMode) { if (!syncMode) {
this.fullSyncCompleted = true; this.fullSyncCompleted = true;
@ -159,6 +162,7 @@ export class Client {
this.mq, this.mq,
this.stateDataStore, this.stateDataStore,
this.eventHook, this.eventHook,
window.silverBulletConfig.readOnly,
); );
const localSpacePrimitives = await this.initSpace(); const localSpacePrimitives = await this.initSpace();
@ -518,6 +522,12 @@ export class Client {
} }
} }
if (this.readOnlyMode) {
remoteSpacePrimitives = new ReadOnlySpacePrimitives(
remoteSpacePrimitives,
);
}
this.plugSpaceRemotePrimitives = new PlugSpacePrimitives( this.plugSpaceRemotePrimitives = new PlugSpacePrimitives(
remoteSpacePrimitives, remoteSpacePrimitives,
this.system.namespaceHook, this.system.namespaceHook,
@ -535,6 +545,8 @@ export class Client {
); );
await spaceKvPrimitives.init(); await spaceKvPrimitives.init();
this.spaceKV = spaceKvPrimitives;
localSpacePrimitives = new FilteredSpacePrimitives( localSpacePrimitives = new FilteredSpacePrimitives(
new EventedSpacePrimitives( new EventedSpacePrimitives(
// Using fallback space primitives here to allow (by default) local reads to "fall through" to HTTP when files aren't synced yet // Using fallback space primitives here to allow (by default) local reads to "fall through" to HTTP when files aren't synced yet

View File

@ -17,16 +17,19 @@ import { editorSyscalls } from "./syscalls/editor.ts";
import { sandboxFetchSyscalls } from "./syscalls/fetch.ts"; import { sandboxFetchSyscalls } from "./syscalls/fetch.ts";
import { markdownSyscalls } from "../common/syscalls/markdown.ts"; import { markdownSyscalls } from "../common/syscalls/markdown.ts";
import { shellSyscalls } from "./syscalls/shell.ts"; import { shellSyscalls } from "./syscalls/shell.ts";
import { spaceSyscalls } from "./syscalls/space.ts"; import { spaceReadSyscalls, spaceWriteSyscalls } from "./syscalls/space.ts";
import { syncSyscalls } from "./syscalls/sync.ts"; import { syncSyscalls } from "./syscalls/sync.ts";
import { systemSyscalls } from "./syscalls/system.ts"; import { systemSyscalls } from "../common/syscalls/system.ts";
import { yamlSyscalls } from "../common/syscalls/yaml.ts"; import { yamlSyscalls } from "../common/syscalls/yaml.ts";
import { Space } from "./space.ts"; import { Space } from "./space.ts";
import { MQHook } from "../plugos/hooks/mq.ts"; import { MQHook } from "../plugos/hooks/mq.ts";
import { mqSyscalls } from "../plugos/syscalls/mq.ts"; import { mqSyscalls } from "../plugos/syscalls/mq.ts";
import { mqProxySyscalls } from "./syscalls/mq.proxy.ts"; import { mqProxySyscalls } from "./syscalls/mq.proxy.ts";
import { dataStoreProxySyscalls } from "./syscalls/datastore.proxy.ts"; import { dataStoreProxySyscalls } from "./syscalls/datastore.proxy.ts";
import { dataStoreSyscalls } from "../plugos/syscalls/datastore.ts"; import {
dataStoreReadSyscalls,
dataStoreWriteSyscalls,
} from "../plugos/syscalls/datastore.ts";
import { DataStore } from "../plugos/lib/datastore.ts"; import { DataStore } from "../plugos/lib/datastore.ts";
import { MessageQueue } from "../plugos/lib/mq.ts"; import { MessageQueue } from "../plugos/lib/mq.ts";
import { languageSyscalls } from "../common/syscalls/language.ts"; import { languageSyscalls } from "../common/syscalls/language.ts";
@ -54,6 +57,7 @@ export class ClientSystem {
private mq: MessageQueue, private mq: MessageQueue,
private ds: DataStore, private ds: DataStore,
private eventHook: EventHook, private eventHook: EventHook,
private readOnlyMode: boolean,
) { ) {
// Only set environment to "client" when running in thin client mode, otherwise we run everything locally (hybrid) // Only set environment to "client" when running in thin client mode, otherwise we run everything locally (hybrid)
this.system = new System( this.system = new System(
@ -153,8 +157,8 @@ export class ClientSystem {
[], [],
eventSyscalls(this.eventHook), eventSyscalls(this.eventHook),
editorSyscalls(this.client), editorSyscalls(this.client),
spaceSyscalls(this.client), spaceReadSyscalls(this.client),
systemSyscalls(this.system, this.client), systemSyscalls(this.system, false, this.client),
markdownSyscalls(), markdownSyscalls(),
assetSyscalls(this.system), assetSyscalls(this.system),
yamlSyscalls(), yamlSyscalls(),
@ -167,24 +171,31 @@ export class ClientSystem {
? mqSyscalls(this.mq) ? mqSyscalls(this.mq)
// In non-sync mode proxy to server // In non-sync mode proxy to server
: mqProxySyscalls(this.client), : mqProxySyscalls(this.client),
this.client.syncMode ...this.client.syncMode
? dataStoreSyscalls(this.ds) ? [dataStoreReadSyscalls(this.ds), dataStoreWriteSyscalls(this.ds)]
: dataStoreProxySyscalls(this.client), : [dataStoreProxySyscalls(this.client)],
debugSyscalls(), debugSyscalls(this.client),
syncSyscalls(this.client), syncSyscalls(this.client),
clientStoreSyscalls(this.ds), clientStoreSyscalls(this.ds),
); );
// Syscalls that require some additional permissions if (!this.readOnlyMode) {
this.system.registerSyscalls( // Write syscalls
["fetch"], this.system.registerSyscalls(
sandboxFetchSyscalls(this.client), [],
); spaceWriteSyscalls(this.client),
);
// Syscalls that require some additional permissions
this.system.registerSyscalls(
["fetch"],
sandboxFetchSyscalls(this.client),
);
this.system.registerSyscalls( this.system.registerSyscalls(
["shell"], ["shell"],
shellSyscalls(this.client), shellSyscalls(this.client),
); );
}
} }
async reloadPlugsFromSpace(space: Space) { async reloadPlugsFromSpace(space: Space) {

View File

@ -34,7 +34,9 @@ export class MainUI {
console.log("Closing search panel"); console.log("Closing search panel");
closeSearchPanel(client.editorView); closeSearchPanel(client.editorView);
return; return;
} else if (target.closest(".cm-content")) { } else if (
target.className === "cm-textfield" || target.closest(".cm-content")
) {
// In some cm element, let's back out // In some cm element, let's back out
return; return;
} }

View File

@ -22,6 +22,7 @@ export type CommandDef = {
mac?: string; mac?: string;
hide?: boolean; hide?: boolean;
requireMode?: "rw" | "ro";
}; };
export type AppCommand = { export type AppCommand = {
@ -58,6 +59,10 @@ export class CommandHook extends EventEmitter<CommandHookEvents>
continue; continue;
} }
const cmd = functionDef.command; const cmd = functionDef.command;
if (cmd.requireMode === "rw" && window.silverBulletConfig.readOnly) {
// Bit hacky, but don't expose commands that require write mode in read-only mode
continue;
}
this.editorCommands.set(cmd.name, { this.editorCommands.set(cmd.name, {
command: cmd, command: cmd,
run: (args?: string[]) => { run: (args?: string[]) => {

View File

@ -39,6 +39,7 @@
// These {{VARIABLES}} are replaced by http_server.ts // These {{VARIABLES}} are replaced by http_server.ts
spaceFolderPath: "{{SPACE_PATH}}", spaceFolderPath: "{{SPACE_PATH}}",
syncOnly: "{{SYNC_ONLY}}" === "true", syncOnly: "{{SYNC_ONLY}}" === "true",
readOnly: "{{READ_ONLY}}" === "true",
clientEncryption: "{{CLIENT_ENCRYPTION}}" === "true", clientEncryption: "{{CLIENT_ENCRYPTION}}" === "true",
}; };
// But in case these variables aren't replaced by the server, fall back sync only mode // But in case these variables aren't replaced by the server, fall back sync only mode
@ -46,6 +47,7 @@
window.silverBulletConfig = { window.silverBulletConfig = {
spaceFolderPath: "", spaceFolderPath: "",
syncOnly: true, syncOnly: true,
readOnly: false,
clientEncryption: false, clientEncryption: false,
}; };
} }

View File

@ -1,107 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="icon" type="image/x-icon" href="/favicon.png" />
<title>Reset SilverBullet</title>
<style>
html,
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
border: 0;
margin: 0;
}
footer {
margin-top: 10px;
}
header {
background-color: #e1e1e1;
border-bottom: #cacaca 1px solid;
}
h1 {
margin: 0;
margin: 0 auto;
max-width: 800px;
padding: 8px;
font-size: 28px;
font-weight: normal;
}
form {
max-width: 800px;
margin: 0 auto;
padding: 10px;
}
input {
font-size: 18px;
}
form>div {
margin-bottom: 5px;
}
.error-message {
color: red;
}
</style>
</head>
<body>
<header>
<h1>Logout</h1>
</header>
<button onclick="resetAll()">Logout</button>
<button onclick="javascript:location='/'">Cancel</button>
<script>
function resetAll() {
if (indexedDB.databases) {
// get a list of all existing IndexedDB databases
indexedDB.databases().then((databases) => {
// loop through the list and delete each database
return Promise.all(
databases.map((database) => {
console.log("Now deleting", database.name);
return new Promise((resolve) => {
return indexedDB.deleteDatabase(database.name).onsuccess = resolve;
});
})
);
}).then(() => {
alert("Flushed local data, you're now logged out");
location.href = "/.auth?logout";
});
} else {
alert("Cannot flush local data (Firefox user?), will now log you out");
location.href = "/.auth?logout";
}
if (navigator.serviceWorker) {
navigator.serviceWorker.ready.then((registration) => {
registration.active.postMessage({ type: 'flushCache' });
});
navigator.serviceWorker.addEventListener('message', (event) => {
if (event.data.type === 'cacheFlushed') {
console.log('Cache flushed');
navigator.serviceWorker.getRegistrations().then((registrations) => {
for (const registration of registrations) {
registration.unregister();
}
});
}
});
}
}
</script>
</body>
</html>

View File

@ -7,7 +7,6 @@ const CACHE_NAME = "{{CACHE_NAME}}_{{CONFIG_HASH}}";
const precacheFiles = Object.fromEntries([ const precacheFiles = Object.fromEntries([
"/", "/",
"/.client/logout.html",
"/.client/client.js", "/.client/client.js",
"/.client/favicon.png", "/.client/favicon.png",
"/.client/iAWriterMonoS-Bold.woff2", "/.client/iAWriterMonoS-Bold.woff2",

View File

@ -1,8 +1,6 @@
import { KvQuery } from "$sb/types.ts";
import { LimitedMap } from "../../plug-api/lib/limited_map.ts";
import type { SysCallMapping } from "../../plugos/system.ts"; import type { SysCallMapping } from "../../plugos/system.ts";
import type { Client } from "../client.ts"; import type { Client } from "../client.ts";
import { proxySyscall, proxySyscalls } from "./util.ts"; import { proxySyscalls } from "./util.ts";
export function dataStoreProxySyscalls(client: Client): SysCallMapping { export function dataStoreProxySyscalls(client: Client): SysCallMapping {
return proxySyscalls(client, [ return proxySyscalls(client, [

View File

@ -1,6 +1,8 @@
import { KvKey } from "$sb/types.ts";
import type { SysCallMapping } from "../../plugos/system.ts"; import type { SysCallMapping } from "../../plugos/system.ts";
import { Client } from "../client.ts";
export function debugSyscalls(): SysCallMapping { export function debugSyscalls(client: Client): SysCallMapping {
return { return {
"debug.resetClient": async () => { "debug.resetClient": async () => {
if (navigator.serviceWorker) { if (navigator.serviceWorker) {
@ -44,5 +46,20 @@ export function debugSyscalls(): SysCallMapping {
alert("Reset complete, now reloading the page..."); alert("Reset complete, now reloading the page...");
location.reload(); location.reload();
}, },
"debug.cleanup": async () => {
if (client.spaceKV) {
console.log("Wiping the entire space KV store");
// In sync mode, we can just delete the whole space
const allKeys: KvKey[] = [];
for await (const { key } of client.spaceKV.query({})) {
allKeys.push(key);
}
await client.spaceKV.batchDelete(allKeys);
}
localStorage.clear();
console.log("Wiping the entire state KV store");
await client.stateDataStore.queryDelete({});
console.log("Done");
},
}; };
} }

View File

@ -2,7 +2,7 @@ import { Client } from "../client.ts";
import { SysCallMapping } from "../../plugos/system.ts"; import { SysCallMapping } from "../../plugos/system.ts";
import { AttachmentMeta, FileMeta, PageMeta } from "$sb/types.ts"; import { AttachmentMeta, FileMeta, PageMeta } from "$sb/types.ts";
export function spaceSyscalls(editor: Client): SysCallMapping { export function spaceReadSyscalls(editor: Client): SysCallMapping {
return { return {
"space.listPages": (): Promise<PageMeta[]> => { "space.listPages": (): Promise<PageMeta[]> => {
return editor.space.fetchPageList(); return editor.space.fetchPageList();
@ -13,6 +13,36 @@ export function spaceSyscalls(editor: Client): SysCallMapping {
"space.getPageMeta": (_ctx, name: string): Promise<PageMeta> => { "space.getPageMeta": (_ctx, name: string): Promise<PageMeta> => {
return editor.space.getPageMeta(name); return editor.space.getPageMeta(name);
}, },
"space.listPlugs": (): Promise<FileMeta[]> => {
return editor.space.listPlugs();
},
"space.listAttachments": async (): Promise<AttachmentMeta[]> => {
return await editor.space.fetchAttachmentList();
},
"space.readAttachment": async (_ctx, name: string): Promise<Uint8Array> => {
return (await editor.space.readAttachment(name)).data;
},
"space.getAttachmentMeta": async (
_ctx,
name: string,
): Promise<AttachmentMeta> => {
return await editor.space.getAttachmentMeta(name);
},
// FS
"space.listFiles": (): Promise<FileMeta[]> => {
return editor.space.spacePrimitives.fetchFileList();
},
"space.getFileMeta": (_ctx, name: string): Promise<FileMeta> => {
return editor.space.spacePrimitives.getFileMeta(name);
},
"space.readFile": async (_ctx, name: string): Promise<Uint8Array> => {
return (await editor.space.spacePrimitives.readFile(name)).data;
},
};
}
export function spaceWriteSyscalls(editor: Client): SysCallMapping {
return {
"space.writePage": ( "space.writePage": (
_ctx, _ctx,
name: string, name: string,
@ -30,21 +60,6 @@ export function spaceSyscalls(editor: Client): SysCallMapping {
console.log("Deleting page"); console.log("Deleting page");
await editor.space.deletePage(name); await editor.space.deletePage(name);
}, },
"space.listPlugs": (): Promise<FileMeta[]> => {
return editor.space.listPlugs();
},
"space.listAttachments": async (): Promise<AttachmentMeta[]> => {
return await editor.space.fetchAttachmentList();
},
"space.readAttachment": async (_ctx, name: string): Promise<Uint8Array> => {
return (await editor.space.readAttachment(name)).data;
},
"space.getAttachmentMeta": async (
_ctx,
name: string,
): Promise<AttachmentMeta> => {
return await editor.space.getAttachmentMeta(name);
},
"space.writeAttachment": ( "space.writeAttachment": (
_ctx, _ctx,
name: string, name: string,
@ -55,17 +70,6 @@ export function spaceSyscalls(editor: Client): SysCallMapping {
"space.deleteAttachment": async (_ctx, name: string) => { "space.deleteAttachment": async (_ctx, name: string) => {
await editor.space.deleteAttachment(name); await editor.space.deleteAttachment(name);
}, },
// FS
"space.listFiles": (): Promise<FileMeta[]> => {
return editor.space.spacePrimitives.fetchFileList();
},
"space.getFileMeta": (_ctx, name: string): Promise<FileMeta> => {
return editor.space.spacePrimitives.getFileMeta(name);
},
"space.readFile": async (_ctx, name: string): Promise<Uint8Array> => {
return (await editor.space.spacePrimitives.readFile(name)).data;
},
"space.writeFile": ( "space.writeFile": (
_ctx, _ctx,
name: string, name: string,

View File

@ -12,6 +12,8 @@ _The changes below are not yet released “properly”. To them out early, check
* Action buttons (top right buttons) can now be configured, see [[SETTINGS]] how to do this. * Action buttons (top right buttons) can now be configured, see [[SETTINGS]] how to do this.
* Headers are now indexed, meaning you can query them [[Objects#header]] and also reference them by name via page links using `#` that I just demonstrated 👈. See [[Links]] for more information on all the type of link formats that SilverBullet now supports. * Headers are now indexed, meaning you can query them [[Objects#header]] and also reference them by name via page links using `#` that I just demonstrated 👈. See [[Links]] for more information on all the type of link formats that SilverBullet now supports.
* New {[Task: Remove Completed]} command to remove all completed tasks from a page * New {[Task: Remove Completed]} command to remove all completed tasks from a page
* **Read-only mode** (experimental) is here, see [[Install/Configuration#Run mode]] on how to enable it. Allowing you expose your space to the outside world in all its glory, but without allowing anybody to edit anything. This should be fairly locked down and secure, but back up your stuff!
* New {[Clear Local Storage & Logout]} command to wipe out any locally synced data (and log you out if you use [[Authentication]]).
* Bug fixes: * Bug fixes:
* Improved Ctrl/Cmd-click (to open links in a new window) behavior: now actually follow `@pos` and `$anchor` links. * Improved Ctrl/Cmd-click (to open links in a new window) behavior: now actually follow `@pos` and `$anchor` links.
* Right-clicking links now opens browser native context menu again * Right-clicking links now opens browser native context menu again

View File

@ -1,21 +1,18 @@
SilverBullet is primarily configured via environment variables. This page gives a comprehensive overview of all configuration options. You can set these ad-hoc when running the SilverBullet server, or e.g. in your [[Install/Docker|docker-compose file]]. SilverBullet is primarily configured via environment variables. This page gives a comprehensive overview of all configuration options. You can set these ad-hoc when running the SilverBullet server, or e.g. in your [[Install/Docker|docker-compose file]].
# Network # Network
$network
Note: these options are primarily useful for [[Install/Deno]] deployments, not so much for [[Install/Docker]]. Note: these options are primarily useful for [[Install/Deno]] deployments, not so much for [[Install/Docker]].
* `SB_HOSTNAME`: Set to the hostname to bind to (defaults to `127.0.0.0`, set to `0.0.0.0` to accept outside connections for the local deno setup, defaults to `0.0.0.0` for docker) * `SB_HOSTNAME`: Set to the hostname to bind to (defaults to `127.0.0.0`, set to `0.0.0.0` to accept outside connections for the local deno setup, defaults to `0.0.0.0` for docker)
* `SB_PORT`: Sets the port to listen to, e.g. `SB_PORT=1234`, default is `3000` * `SB_PORT`: Sets the port to listen to, e.g. `SB_PORT=1234`, default is `3000`
# Authentication # Authentication
$authentication
SilverBullet supports basic authentication for a single user. SilverBullet supports basic authentication for a single user.
* `SB_USER`: Sets single-user credentials, e.g. `SB_USER=pete:1234` allows you to login with username “pete” and password “1234”. * `SB_USER`: Sets single-user credentials, e.g. `SB_USER=pete:1234` allows you to login with username “pete” and password “1234”.
* `SB_AUTH_TOKEN`: Enables `Authorization: Bearer <token>` style authentication on the [[API]] (useful for [[Sync]] and remote HTTP storage backends). * `SB_AUTH_TOKEN`: Enables `Authorization: Bearer <token>` style authentication on the [[API]] (useful for [[Sync]] and remote HTTP storage backends).
# Storage # Storage
$storage
SilverBullet supports multiple storage backends for keeping your [[Spaces]] content. SilverBullet supports multiple storage backends for keeping your [[Spaces]] content.
## Disk storage ## Disk storage
@ -26,7 +23,8 @@ This is the default and simplest backend to use: a folder on disk. It is configu
## AWS S3 bucket storage ## AWS S3 bucket storage
It is also possible to use an S3 bucket as storage. For this, you need to create a bucket, create an IAM user and configure access to it appropriately. It is also possible to use an S3 bucket as storage. For this, you need to create a bucket, create an IAM user and configure access to it appropriately.
Since S3 doesnt support an efficient way to store custom metadata, this mode does require a [[$database]] configuration (see below) to keep all file metadata. Since S3 doesnt support an efficient way to store custom metadata, this mode does require a [[
]] configuration (see below) to keep all file metadata.
S3 is configured as follows: S3 is configured as follows:
@ -38,13 +36,13 @@ S3 is configured as follows:
* `AWS_REGION`: e.g. `eu-central-1` * `AWS_REGION`: e.g. `eu-central-1`
## Database storage ## Database storage
It is also possible to store space content in the [[$database]]. While not necessarily recommended, it is a viable way to set up a simple deployment of SilverBullet on e.g. [[Install/Deno Deploy]]. Large files will automatically be chunked to avoid limits the used database may have on value size. It is also possible to store space content in the [[#Database]]. While not necessarily recommended, it is a viable way to set up a simple deployment of SilverBullet on e.g. [[Install/Deno Deploy]]. Large files will automatically be chunked to avoid limits the used database may have on value size.
This mode is configured as follows: This mode is configured as follows:
* `SB_FOLDER`: set to `db://` * `SB_FOLDER`: set to `db://`
The database configured via [[$database]] will be used. The database configured via [[#Database]] will be used.
## HTTP storage ## HTTP storage
While not particularly useful stand-alone (primarily for [[Sync]]), it is possible to store space content on _another_ SilverBullet installation via its [[API]]. While not particularly useful stand-alone (primarily for [[Sync]]), it is possible to store space content on _another_ SilverBullet installation via its [[API]].
@ -52,10 +50,9 @@ While not particularly useful stand-alone (primarily for [[Sync]]), it is possib
This mode is configured as follows: This mode is configured as follows:
* `SB_FOLDER`: set to the URL of the other SilverBullet server, e.g. `https://mynotes.mydomain.com` * `SB_FOLDER`: set to the URL of the other SilverBullet server, e.g. `https://mynotes.mydomain.com`
* `SB_AUTH_TOKEN`: matching the authorization token (configured via [[$authentication]] on the other end) to use for authorization. * `SB_AUTH_TOKEN`: matching the authorization token (configured via [[#Authentication]] on the other end) to use for authorization.
# Database # Database
$database
SilverBullet requires a database backend to (potentially) keep various types of data: SilverBullet requires a database backend to (potentially) keep various types of data:
* Indexes for e.g. [[Objects]] * Indexes for e.g. [[Objects]]
@ -80,16 +77,13 @@ The in-memory database is only useful for testing.
* `SB_DB_BACKEND`: `memory` * `SB_DB_BACKEND`: `memory`
# Run mode # Run mode
$runmode
* `SB_SYNC_ONLY`: If you want to run SilverBullet in a mode where the server purely functions as a simple file store and doesnt index or process content on the server, you can do so by setting this environment variable to `true`. As a result, the client will always run in the Sync [[Client Modes|client mode]]. * `SB_SYNC_ONLY`: If you want to run SilverBullet in a mode where the server purely functions as a simple file store and doesnt index or process content on the server, you can do so by setting this environment variable to `true`. As a result, the client will always run in the Sync [[Client Modes|client mode]].
* `SB_READ_ONLY` (==Experimental==): If you want to run the SilverBullet client and server in read-only mode (you get the full SilverBullet client, but all edit functionality and commands are disabled), you can do this by setting this environment variable to `true`. Upon the server start a full space index will happen, after which all write operations will be disabled.
# Security # Security
$security
SilverBullet enables plugs to run shell commands. This is used by e.g. the [[Plugs/Git]] plug to perform git commands. This is potentially unsafe. If you dont need this, you can disable this functionality: SilverBullet enables plugs to run shell commands. This is used by e.g. the [[Plugs/Git]] plug to perform git commands. This is potentially unsafe. If you dont need this, you can disable this functionality:
* `SB_SHELL_BACKEND`: Enable/disable running of shell commands from plugs, defaults to `local` (enabled), set to `off` to disable. It is only enabled when using a local folder for [[$storage]]. * `SB_SHELL_BACKEND`: Enable/disable running of shell commands from plugs, defaults to `local` (enabled), set to `off` to disable. It is only enabled when using a local folder for [[#Storage]].
# Docker # Docker

View File

@ -1,11 +1,11 @@
The SilverBullet CLI has a `sync` command that can be used to synchronize local as well as remote [[Spaces]]. This can be useful when migrating between different [[Install/Configuration$storage|storage implementations]]. It can also be used to back up content elsewhere. Under the hood, this sync mechanism uses the exact same sync engine used for the Sync [[Client Modes]]. The SilverBullet CLI has a `sync` command that can be used to synchronize local as well as remote [[Spaces]]. This can be useful when migrating between different [[Install/Configuration#Storage|storage implementations]]. It can also be used to back up content elsewhere. Under the hood, this sync mechanism uses the exact same sync engine used for the Sync [[Client Modes]].
# Use cases # Use cases
* **Migration**: you hosted SilverBullet on your local device until now, but have since set up an instance via [[Install/Deno Deploy]] and want to migrate your content there. * **Migration**: you hosted SilverBullet on your local device until now, but have since set up an instance via [[Install/Deno Deploy]] and want to migrate your content there.
* **Backup**: you host SilverBullet on a remote server, but would like to make backups elsewhere from time to time. * **Backup**: you host SilverBullet on a remote server, but would like to make backups elsewhere from time to time.
# Setup # Setup
To use `silverbullet sync` you need a [[Install/Local$deno|local deno installation of SilverBullet]]. To use `silverbullet sync` you need a [[Install/Deno|local deno installation of SilverBullet]].
# General use # General use
To perform a sync between two locations: To perform a sync between two locations:
@ -14,7 +14,7 @@ To perform a sync between two locations:
silverbullet sync --snapshot snapshot.json <primaryPath> <secondaryPath> silverbullet sync --snapshot snapshot.json <primaryPath> <secondaryPath>
``` ```
Where both `primaryPath` and `secondaryPath` can use any [[Install/Configuration$storage]] configuration. Where both `primaryPath` and `secondaryPath` can use any [[Install/Configuration#Storage]] configuration.
The `--snapshot` argument is optional; when set, it will read/write a snapshot to the given location. This snapshot will be used to speed up future synchronizations. The `--snapshot` argument is optional; when set, it will read/write a snapshot to the given location. This snapshot will be used to speed up future synchronizations.
@ -25,7 +25,7 @@ silverbullet sync --snapshot snapshot.json testspace testspace2
``` ```
# Migrate # Migrate
To synchronize a local folder (the current directory `.`) to a remote server (located at `https://notes.myserver.com`) for which you have setup an [[Install/Configuration$authentication|auth token]] using the `SB_AUTH_TOKEN` environment variable of `1234`: To synchronize a local folder (the current directory `.`) to a remote server (located at `https://notes.myserver.com`) for which you have setup an [[Install/Configuration#Authentication|auth token]] using the `SB_AUTH_TOKEN` environment variable of `1234`:
```shell ```shell
SB_AUTH_TOKEN=1234 silverbullet sync . https://notes.myserver.com SB_AUTH_TOKEN=1234 silverbullet sync . https://notes.myserver.com