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`, {
overwrite: true,
});
await copy("web/logout.html", `${dist}/logout.html`, {
overwrite: true,
});
await copy("web/images/favicon.png", `${dist}/favicon.png`, {
overwrite: true,
});
@ -84,7 +81,7 @@ async function buildCopyBundleAssets() {
console.log("Now ESBuilding the client and service workers...");
await esbuild.build({
const result = await esbuild.build({
entryPoints: [
{
in: "web/boot.ts",
@ -102,6 +99,7 @@ async function buildCopyBundleAssets() {
sourcemap: "linked",
minify: true,
jsxFactory: "h",
// metafile: true,
jsx: "automatic",
jsxFragment: "Fragment",
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}}
let swCode = await Deno.readTextFile("dist_client_bundle/service_worker.js");
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 { assertEquals } from "../test_deps.ts";
import { path } from "../common/deps.ts";
import { MemoryKvPrimitives } from "../plugos/lib/memory_kv_primitives.ts";
Deno.test("Test plug run", {
sanitizeResources: false,
sanitizeOps: false,
}, async () => {
// const tempDir = await Deno.makeTempDir();
const tempDbFile = await Deno.makeTempFile({ suffix: ".db" });
const assetBundle = new AssetBundle(assets);
const testFolder = path.dirname(new URL(import.meta.url).pathname);
@ -31,11 +29,11 @@ Deno.test("Test plug run", {
"test.run",
[],
assetBundle,
new MemoryKvPrimitives(),
),
"Hello",
);
// await Deno.remove(tempDir, { recursive: true });
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 { ServerSystem } from "../server/server_system.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 { determineShellBackend } from "../server/shell_backend.ts";
import { LocalShell } from "../server/shell_backend.ts";
import { Hono } from "../server/deps.ts";
import { KvPrimitives } from "../plugos/lib/kv_primitives.ts";
export async function runPlug(
spacePath: string,
functionName: string | undefined,
args: string[] = [],
builtinAssetBundle: AssetBundle,
httpServerPort = 3123,
httpHostname = "127.0.0.1",
kvPrimitives: KvPrimitives,
httpServerPort?: number,
httpHostname?: string,
) {
const serverController = new AbortController();
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 serverSystem = new ServerSystem(
@ -34,23 +28,25 @@ export async function runPlug(
new DiskSpacePrimitives(spacePath),
builtinAssetBundle,
),
dbBackend,
determineShellBackend(spacePath),
kvPrimitives,
new LocalShell(spacePath),
false,
);
await serverSystem.init(true);
app.use((context, next) => {
return endpointHook.handleRequest(serverSystem.system!, context, next);
});
Deno.serve({
hostname: httpHostname,
port: httpServerPort,
signal: serverController.signal,
}, app.fetch);
if (httpHostname && httpServerPort) {
Deno.serve({
hostname: httpHostname,
port: httpServerPort,
signal: serverController.signal,
}, app.fetch);
}
if (functionName) {
const result = await serverSystem.system.invokeFunction(functionName, args);
await serverSystem.close();
serverSystem.kvPrimitives.close();
serverController.abort();
return result;
} else {

View File

@ -4,6 +4,7 @@ import assets from "../dist/plug_asset_bundle.json" assert {
type: "json",
};
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
import { determineDatabaseBackend } from "../server/db_backend.ts";
export async function plugRunCommand(
{
@ -20,18 +21,28 @@ export async function plugRunCommand(
spacePath = path.resolve(spacePath);
console.log("Space path", spacePath);
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 {
const result = await runPlug(
spacePath,
functionName,
args,
new AssetBundle(assets),
kvPrimitives,
port,
hostname,
);
if (result) {
console.log("Output", result);
}
kvPrimitives.close();
Deno.exit(0);
} catch (e: any) {
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 { 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(
options: {
@ -41,6 +43,8 @@ export async function serveCommand(
const syncOnly = options.syncOnly || !!Deno.env.get("SB_SYNC_ONLY");
const readOnly = !!Deno.env.get("SB_READ_ONLY");
if (syncOnly) {
console.log("Running in sync-only mode (no backend processing)");
}
@ -75,6 +79,9 @@ export async function serveCommand(
const [user, pass] = userAuth.split(":");
userCredentials = { user, pass };
}
const backendConfig = Deno.env.get("SB_SHELL_BACKEND") || "local";
const configs = new Map<string, SpaceServerConfig>();
configs.set("*", {
hostname,
@ -82,15 +89,29 @@ export async function serveCommand(
auth: userCredentials,
authToken: Deno.env.get("SB_AUTH_TOKEN"),
syncOnly,
readOnly,
shellBackend: backendConfig,
clientEncryption,
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({
hostname,
port,
clientAssetBundle: new AssetBundle(clientAssetBundle as AssetJson),
plugAssetBundle: new AssetBundle(plugAssetBundle as AssetJson),
plugAssetBundle: plugAssets,
baseKvPrimitives,
keyFile: options.key,
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 type { Client } from "../client.ts";
import { CommandDef } from "../hooks/command.ts";
import { proxySyscall } from "./util.ts";
import type { Client } from "../../web/client.ts";
import { CommandDef } from "../../web/hooks/command.ts";
import { proxySyscall } from "../../web/syscalls/util.ts";
export function systemSyscalls(
system: System<any>,
readOnlyMode: boolean,
client?: Client,
): SysCallMapping {
const api: SysCallMapping = {
@ -64,6 +65,9 @@ export function systemSyscalls(
"system.getEnv": () => {
return system.env;
},
"system.getMode": () => {
return readOnlyMode ? "ro" : "rw";
},
};
return api;
}

View File

@ -3,3 +3,10 @@ import { syscall } from "./syscall.ts";
export function 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> {
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,
treeShaking: true,
plugins: [
// {
// name: "json",
// setup: (build) =>
// build.onLoad({ filter: /\.json$/ }, () => ({ loader: "json" })),
// },
...denoPlugins({
// TODO do this differently
importMapURL: options.importMap ||

View File

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

View File

@ -33,4 +33,5 @@ functions:
importLibraryCommand:
path: library.ts:importLibraryCommand
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 { QueryProviderEvent } from "$sb/app_event.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";
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> {
const allKeys: KvKey[] = [];
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);
}

View File

@ -1,7 +1,6 @@
import { ObjectValue } from "$sb/types.ts";
import { system } from "$sb/syscalls.ts";
import { indexObjects } from "./api.ts";
import { AttributeObject } from "./attributes.ts";
import { TagObject } from "./tags.ts";
export const builtinPseudoPage = ":builtin:";
@ -92,6 +91,10 @@ export const builtins: Record<string, Record<string, string>> = {
};
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");
const allObjects: ObjectValue<any>[] = [];
for (const [tagName, attributes] of Object.entries(builtins)) {

View File

@ -11,6 +11,10 @@ export async function reindexCommand() {
}
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...");
// Executed this way to not have to embed the search plug code here
await system.invokeFunction("index.clearIndex");
@ -55,6 +59,10 @@ export async function processIndexQueue(messages: MQMessage[]) {
}
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);
if (isTemplate(text)) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import { SilverBulletHooks } from "../common/manifest.ts";
import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_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 { ensureAndLoadSettingsAndIndex } from "../common/util.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 { gitIgnoreCompiler } from "./deps.ts";
import { ServerSystem } from "./server_system.ts";
import { determineShellBackend, NotSupportedShell } from "./shell_backend.ts";
import { ShellBackend } from "./shell_backend.ts";
import { determineStorageBackend } from "./storage_backend.ts";
@ -21,8 +23,10 @@ export type SpaceServerConfig = {
// Additional API auth token
authToken?: string;
pagesPath: string;
syncOnly?: boolean;
clientEncryption?: boolean;
shellBackend: string;
syncOnly: boolean;
readOnly: boolean;
clientEncryption: boolean;
};
export class SpaceServer {
@ -41,10 +45,11 @@ export class SpaceServer {
system?: System<SilverBulletHooks>;
clientEncryption: boolean;
syncOnly: boolean;
readOnly: boolean;
shellBackend: ShellBackend;
constructor(
config: SpaceServerConfig,
public shellBackend: ShellBackend,
private plugAssetBundle: AssetBundle,
private kvPrimitives: KvPrimitives,
) {
@ -53,13 +58,18 @@ export class SpaceServer {
this.auth = config.auth;
this.authToken = config.authToken;
this.clientEncryption = !!config.clientEncryption;
this.syncOnly = !!config.syncOnly;
this.syncOnly = config.syncOnly;
this.readOnly = config.readOnly;
if (this.clientEncryption) {
// Sync only will forced on when encryption is enabled
this.syncOnly = true;
}
this.jwtIssuer = new JWTIssuer(kvPrimitives);
this.shellBackend = config.readOnly
? new NotSupportedShell() // No shell for read only mode
: determineShellBackend(config);
}
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)
if (!this.syncOnly) {
// Enable server-side processing
@ -88,6 +102,7 @@ export class SpaceServer {
this.spacePrimitives,
this.kvPrimitives,
this.shellBackend,
this.readOnly,
);
this.serverSystem = serverSystem;
}

View File

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

View File

@ -1,3 +1,4 @@
import type { SpaceServerConfig } from "./instance.ts";
import { ShellRequest, ShellResponse } from "./rpc.ts";
/**
@ -5,11 +6,13 @@ import { ShellRequest, ShellResponse } from "./rpc.ts";
* - 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";
switch (backendConfig) {
case "local":
return new LocalShell(path);
return new LocalShell(spaceServerConfig.pagesPath);
default:
console.info(
"Running in shellless mode, meaning shell commands are disabled",
@ -22,7 +25,7 @@ export interface ShellBackend {
handle(shellRequest: ShellRequest): Promise<ShellResponse>;
}
class NotSupportedShell implements ShellBackend {
export class NotSupportedShell implements ShellBackend {
handle(): Promise<ShellResponse> {
return Promise.resolve({
stdout: "",
@ -32,7 +35,7 @@ class NotSupportedShell implements ShellBackend {
}
}
class LocalShell implements ShellBackend {
export class LocalShell implements ShellBackend {
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
*/
export function spaceSyscalls(space: Space): SysCallMapping {
export function spaceReadSyscalls(space: Space): SysCallMapping {
return {
"space.listPages": (): Promise<PageMeta[]> => {
return space.fetchPageList();
@ -16,16 +16,6 @@ export function spaceSyscalls(space: Space): SysCallMapping {
"space.getPageMeta": (_ctx, name: string): Promise<PageMeta> => {
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[]> => {
return space.listPlugs();
},
@ -41,16 +31,6 @@ export function spaceSyscalls(space: Space): SysCallMapping {
): Promise<AttachmentMeta> => {
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
"space.listFiles": (): Promise<FileMeta[]> => {
@ -62,6 +42,31 @@ export function spaceSyscalls(space: Space): SysCallMapping {
"space.readFile": async (_ctx, name: string): Promise<Uint8Array> => {
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": (
_ctx,
name: string,

View File

@ -10,9 +10,14 @@ safeRun(async () => {
syncMode ? "in Sync Mode" : "in Online Mode",
);
if (window.silverBulletConfig.readOnly) {
console.log("Running in read-only mode");
}
const client = new Client(
document.getElementById("sb-root")!,
syncMode,
window.silverBulletConfig.readOnly,
);
window.client = client;
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 { buildQueryFunctions } from "../common/query_functions.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 autoSaveInterval = 1000;
@ -69,6 +71,7 @@ declare global {
silverBulletConfig: {
spaceFolderPath: string;
syncOnly: boolean;
readOnly: boolean;
clientEncryption: boolean;
};
client: Client;
@ -87,7 +90,6 @@ export class Client {
private dbPrefix: string;
plugSpaceRemotePrimitives!: PlugSpacePrimitives;
// localSpacePrimitives!: FilteredSpacePrimitives;
httpSpacePrimitives!: HttpSpacePrimitives;
space!: Space;
@ -109,7 +111,7 @@ export class Client {
ui!: MainUI;
stateDataStore!: DataStore;
spaceDataStore!: DataStore;
spaceKV?: KvPrimitives;
mq!: DataStoreMQ;
// Used by the "wiki link" highlighter to check if a page exists
@ -118,7 +120,8 @@ export class Client {
constructor(
private parent: Element,
public syncMode = false,
public syncMode: boolean,
private readOnlyMode: boolean,
) {
if (!syncMode) {
this.fullSyncCompleted = true;
@ -159,6 +162,7 @@ export class Client {
this.mq,
this.stateDataStore,
this.eventHook,
window.silverBulletConfig.readOnly,
);
const localSpacePrimitives = await this.initSpace();
@ -518,6 +522,12 @@ export class Client {
}
}
if (this.readOnlyMode) {
remoteSpacePrimitives = new ReadOnlySpacePrimitives(
remoteSpacePrimitives,
);
}
this.plugSpaceRemotePrimitives = new PlugSpacePrimitives(
remoteSpacePrimitives,
this.system.namespaceHook,
@ -535,6 +545,8 @@ export class Client {
);
await spaceKvPrimitives.init();
this.spaceKV = spaceKvPrimitives;
localSpacePrimitives = new FilteredSpacePrimitives(
new EventedSpacePrimitives(
// 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 { markdownSyscalls } from "../common/syscalls/markdown.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 { systemSyscalls } from "./syscalls/system.ts";
import { systemSyscalls } from "../common/syscalls/system.ts";
import { yamlSyscalls } from "../common/syscalls/yaml.ts";
import { Space } from "./space.ts";
import { MQHook } from "../plugos/hooks/mq.ts";
import { mqSyscalls } from "../plugos/syscalls/mq.ts";
import { mqProxySyscalls } from "./syscalls/mq.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 { MessageQueue } from "../plugos/lib/mq.ts";
import { languageSyscalls } from "../common/syscalls/language.ts";
@ -54,6 +57,7 @@ export class ClientSystem {
private mq: MessageQueue,
private ds: DataStore,
private eventHook: EventHook,
private readOnlyMode: boolean,
) {
// Only set environment to "client" when running in thin client mode, otherwise we run everything locally (hybrid)
this.system = new System(
@ -153,8 +157,8 @@ export class ClientSystem {
[],
eventSyscalls(this.eventHook),
editorSyscalls(this.client),
spaceSyscalls(this.client),
systemSyscalls(this.system, this.client),
spaceReadSyscalls(this.client),
systemSyscalls(this.system, false, this.client),
markdownSyscalls(),
assetSyscalls(this.system),
yamlSyscalls(),
@ -167,24 +171,31 @@ export class ClientSystem {
? mqSyscalls(this.mq)
// In non-sync mode proxy to server
: mqProxySyscalls(this.client),
this.client.syncMode
? dataStoreSyscalls(this.ds)
: dataStoreProxySyscalls(this.client),
debugSyscalls(),
...this.client.syncMode
? [dataStoreReadSyscalls(this.ds), dataStoreWriteSyscalls(this.ds)]
: [dataStoreProxySyscalls(this.client)],
debugSyscalls(this.client),
syncSyscalls(this.client),
clientStoreSyscalls(this.ds),
);
// Syscalls that require some additional permissions
this.system.registerSyscalls(
["fetch"],
sandboxFetchSyscalls(this.client),
);
if (!this.readOnlyMode) {
// Write syscalls
this.system.registerSyscalls(
[],
spaceWriteSyscalls(this.client),
);
// Syscalls that require some additional permissions
this.system.registerSyscalls(
["fetch"],
sandboxFetchSyscalls(this.client),
);
this.system.registerSyscalls(
["shell"],
shellSyscalls(this.client),
);
this.system.registerSyscalls(
["shell"],
shellSyscalls(this.client),
);
}
}
async reloadPlugsFromSpace(space: Space) {

View File

@ -34,7 +34,9 @@ export class MainUI {
console.log("Closing search panel");
closeSearchPanel(client.editorView);
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
return;
}

View File

@ -22,6 +22,7 @@ export type CommandDef = {
mac?: string;
hide?: boolean;
requireMode?: "rw" | "ro";
};
export type AppCommand = {
@ -58,6 +59,10 @@ export class CommandHook extends EventEmitter<CommandHookEvents>
continue;
}
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, {
command: cmd,
run: (args?: string[]) => {

View File

@ -39,6 +39,7 @@
// These {{VARIABLES}} are replaced by http_server.ts
spaceFolderPath: "{{SPACE_PATH}}",
syncOnly: "{{SYNC_ONLY}}" === "true",
readOnly: "{{READ_ONLY}}" === "true",
clientEncryption: "{{CLIENT_ENCRYPTION}}" === "true",
};
// But in case these variables aren't replaced by the server, fall back sync only mode
@ -46,6 +47,7 @@
window.silverBulletConfig = {
spaceFolderPath: "",
syncOnly: true,
readOnly: 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([
"/",
"/.client/logout.html",
"/.client/client.js",
"/.client/favicon.png",
"/.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 { Client } from "../client.ts";
import { proxySyscall, proxySyscalls } from "./util.ts";
import { proxySyscalls } from "./util.ts";
export function dataStoreProxySyscalls(client: Client): SysCallMapping {
return proxySyscalls(client, [

View File

@ -1,6 +1,8 @@
import { KvKey } from "$sb/types.ts";
import type { SysCallMapping } from "../../plugos/system.ts";
import { Client } from "../client.ts";
export function debugSyscalls(): SysCallMapping {
export function debugSyscalls(client: Client): SysCallMapping {
return {
"debug.resetClient": async () => {
if (navigator.serviceWorker) {
@ -44,5 +46,20 @@ export function debugSyscalls(): SysCallMapping {
alert("Reset complete, now reloading the page...");
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 { AttachmentMeta, FileMeta, PageMeta } from "$sb/types.ts";
export function spaceSyscalls(editor: Client): SysCallMapping {
export function spaceReadSyscalls(editor: Client): SysCallMapping {
return {
"space.listPages": (): Promise<PageMeta[]> => {
return editor.space.fetchPageList();
@ -13,6 +13,36 @@ export function spaceSyscalls(editor: Client): SysCallMapping {
"space.getPageMeta": (_ctx, name: string): Promise<PageMeta> => {
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": (
_ctx,
name: string,
@ -30,21 +60,6 @@ export function spaceSyscalls(editor: Client): SysCallMapping {
console.log("Deleting page");
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": (
_ctx,
name: string,
@ -55,17 +70,6 @@ export function spaceSyscalls(editor: Client): SysCallMapping {
"space.deleteAttachment": async (_ctx, name: string) => {
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": (
_ctx,
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.
* 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
* **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:
* 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

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]].
# Network
$network
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_PORT`: Sets the port to listen to, e.g. `SB_PORT=1234`, default is `3000`
# Authentication
$authentication
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_AUTH_TOKEN`: Enables `Authorization: Bearer <token>` style authentication on the [[API]] (useful for [[Sync]] and remote HTTP storage backends).
# Storage
$storage
SilverBullet supports multiple storage backends for keeping your [[Spaces]] content.
## 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
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:
@ -38,13 +36,13 @@ S3 is configured as follows:
* `AWS_REGION`: e.g. `eu-central-1`
## 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:
* `SB_FOLDER`: set to `db://`
The database configured via [[$database]] will be used.
The database configured via [[#Database]] will be used.
## 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]].
@ -52,10 +50,9 @@ While not particularly useful stand-alone (primarily for [[Sync]]), it is possib
This mode is configured as follows:
* `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
SilverBullet requires a database backend to (potentially) keep various types of data:
* Indexes for e.g. [[Objects]]
@ -80,16 +77,13 @@ The in-memory database is only useful for testing.
* `SB_DB_BACKEND`: `memory`
# 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_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
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

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
* **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.
# 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
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>
```
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.
@ -25,7 +25,7 @@ silverbullet sync --snapshot snapshot.json testspace testspace2
```
# 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
SB_AUTH_TOKEN=1234 silverbullet sync . https://notes.myserver.com