Fixes #161 by implementing read-only mode, first iteration
parent
1d3bc9cf44
commit
5bc7193fb0
11
build_web.ts
11
build_web.ts
|
@ -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()}`);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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 ||
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import { editor } from "$sb/syscalls.ts";
|
||||
|
||||
export async function accountLogoutCommand() {
|
||||
await editor.openUrl("/.client/logout.html", true);
|
||||
}
|
|
@ -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 });
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -34,3 +34,4 @@ functions:
|
|||
path: library.ts:importLibraryCommand
|
||||
command:
|
||||
name: "Library: Import"
|
||||
requireMode: rw
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,3 +6,4 @@ functions:
|
|||
name: "Share: Publish"
|
||||
key: "Ctrl-s"
|
||||
mac: "Cmd-s"
|
||||
requireMode: rw
|
||||
|
|
|
@ -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:
|
||||
|
@ -38,3 +40,4 @@ functions:
|
|||
path: task.ts:removeCompletedTasksCommand
|
||||
command:
|
||||
name: "Task: Remove Completed"
|
||||
requireMode: rw
|
||||
|
|
|
@ -44,6 +44,7 @@ functions:
|
|||
command:
|
||||
name: "Page: From Template"
|
||||
key: "Alt-Shift-t"
|
||||
requireMode: rw
|
||||
|
||||
|
||||
# Lint
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,19 +126,26 @@ 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(),
|
||||
);
|
||||
|
||||
if (!this.readOnlyMode) {
|
||||
// Write mode only
|
||||
this.system.registerSyscalls(
|
||||
[],
|
||||
spaceWriteSyscalls(space),
|
||||
dataStoreWriteSyscalls(this.ds),
|
||||
);
|
||||
|
||||
// Syscalls that require some additional permissions
|
||||
this.system.registerSyscalls(
|
||||
["fetch"],
|
||||
|
@ -146,6 +156,7 @@ export class ServerSystem {
|
|||
["shell"],
|
||||
shellSyscalls(this.shellBackend),
|
||||
);
|
||||
}
|
||||
|
||||
await this.loadPlugs();
|
||||
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,14 +171,20 @@ 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),
|
||||
);
|
||||
|
||||
if (!this.readOnlyMode) {
|
||||
// Write syscalls
|
||||
this.system.registerSyscalls(
|
||||
[],
|
||||
spaceWriteSyscalls(this.client),
|
||||
);
|
||||
// Syscalls that require some additional permissions
|
||||
this.system.registerSyscalls(
|
||||
["fetch"],
|
||||
|
@ -186,6 +196,7 @@ export class ClientSystem {
|
|||
shellSyscalls(this.client),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async reloadPlugsFromSpace(space: Space) {
|
||||
console.log("Loading plugs");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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[]) => {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
107
web/logout.html
107
web/logout.html
|
@ -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>
|
|
@ -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",
|
||||
|
|
|
@ -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, [
|
||||
|
|
|
@ -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");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 doesn’t support an efficient way to store custom metadata, this mode does require a [[$database]] configuration (see below) to keep all file metadata.
|
||||
Since S3 doesn’t 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 doesn’t 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 don’t 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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue