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`, {
|
await copy("web/auth.html", `${dist}/auth.html`, {
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
});
|
});
|
||||||
await copy("web/logout.html", `${dist}/logout.html`, {
|
|
||||||
overwrite: true,
|
|
||||||
});
|
|
||||||
await copy("web/images/favicon.png", `${dist}/favicon.png`, {
|
await copy("web/images/favicon.png", `${dist}/favicon.png`, {
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
});
|
});
|
||||||
|
@ -84,7 +81,7 @@ async function buildCopyBundleAssets() {
|
||||||
|
|
||||||
console.log("Now ESBuilding the client and service workers...");
|
console.log("Now ESBuilding the client and service workers...");
|
||||||
|
|
||||||
await esbuild.build({
|
const result = await esbuild.build({
|
||||||
entryPoints: [
|
entryPoints: [
|
||||||
{
|
{
|
||||||
in: "web/boot.ts",
|
in: "web/boot.ts",
|
||||||
|
@ -102,6 +99,7 @@ async function buildCopyBundleAssets() {
|
||||||
sourcemap: "linked",
|
sourcemap: "linked",
|
||||||
minify: true,
|
minify: true,
|
||||||
jsxFactory: "h",
|
jsxFactory: "h",
|
||||||
|
// metafile: true,
|
||||||
jsx: "automatic",
|
jsx: "automatic",
|
||||||
jsxFragment: "Fragment",
|
jsxFragment: "Fragment",
|
||||||
jsxImportSource: "https://esm.sh/preact@10.11.1",
|
jsxImportSource: "https://esm.sh/preact@10.11.1",
|
||||||
|
@ -111,6 +109,11 @@ async function buildCopyBundleAssets() {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (result.metafile) {
|
||||||
|
const text = await esbuild.analyzeMetafile(result.metafile!);
|
||||||
|
console.log("Bundle info", text);
|
||||||
|
}
|
||||||
|
|
||||||
// Patch the service_worker {{CACHE_NAME}}
|
// Patch the service_worker {{CACHE_NAME}}
|
||||||
let swCode = await Deno.readTextFile("dist_client_bundle/service_worker.js");
|
let swCode = await Deno.readTextFile("dist_client_bundle/service_worker.js");
|
||||||
swCode = swCode.replaceAll("{{CACHE_NAME}}", `cache-${Date.now()}`);
|
swCode = swCode.replaceAll("{{CACHE_NAME}}", `cache-${Date.now()}`);
|
||||||
|
|
|
@ -5,14 +5,12 @@ import { runPlug } from "./plug_run.ts";
|
||||||
import assets from "../dist/plug_asset_bundle.json" assert { type: "json" };
|
import assets from "../dist/plug_asset_bundle.json" assert { type: "json" };
|
||||||
import { assertEquals } from "../test_deps.ts";
|
import { assertEquals } from "../test_deps.ts";
|
||||||
import { path } from "../common/deps.ts";
|
import { path } from "../common/deps.ts";
|
||||||
|
import { MemoryKvPrimitives } from "../plugos/lib/memory_kv_primitives.ts";
|
||||||
|
|
||||||
Deno.test("Test plug run", {
|
Deno.test("Test plug run", {
|
||||||
sanitizeResources: false,
|
sanitizeResources: false,
|
||||||
sanitizeOps: false,
|
sanitizeOps: false,
|
||||||
}, async () => {
|
}, async () => {
|
||||||
// const tempDir = await Deno.makeTempDir();
|
|
||||||
const tempDbFile = await Deno.makeTempFile({ suffix: ".db" });
|
|
||||||
|
|
||||||
const assetBundle = new AssetBundle(assets);
|
const assetBundle = new AssetBundle(assets);
|
||||||
|
|
||||||
const testFolder = path.dirname(new URL(import.meta.url).pathname);
|
const testFolder = path.dirname(new URL(import.meta.url).pathname);
|
||||||
|
@ -31,11 +29,11 @@ Deno.test("Test plug run", {
|
||||||
"test.run",
|
"test.run",
|
||||||
[],
|
[],
|
||||||
assetBundle,
|
assetBundle,
|
||||||
|
new MemoryKvPrimitives(),
|
||||||
),
|
),
|
||||||
"Hello",
|
"Hello",
|
||||||
);
|
);
|
||||||
|
|
||||||
// await Deno.remove(tempDir, { recursive: true });
|
// await Deno.remove(tempDir, { recursive: true });
|
||||||
esbuild.stop();
|
esbuild.stop();
|
||||||
await Deno.remove(tempDbFile);
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,29 +4,23 @@ import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
|
||||||
import { sleep } from "$sb/lib/async.ts";
|
import { sleep } from "$sb/lib/async.ts";
|
||||||
import { ServerSystem } from "../server/server_system.ts";
|
import { ServerSystem } from "../server/server_system.ts";
|
||||||
import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts";
|
import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts";
|
||||||
import { determineDatabaseBackend } from "../server/db_backend.ts";
|
|
||||||
import { EndpointHook } from "../plugos/hooks/endpoint.ts";
|
import { EndpointHook } from "../plugos/hooks/endpoint.ts";
|
||||||
import { determineShellBackend } from "../server/shell_backend.ts";
|
import { LocalShell } from "../server/shell_backend.ts";
|
||||||
import { Hono } from "../server/deps.ts";
|
import { Hono } from "../server/deps.ts";
|
||||||
|
import { KvPrimitives } from "../plugos/lib/kv_primitives.ts";
|
||||||
|
|
||||||
export async function runPlug(
|
export async function runPlug(
|
||||||
spacePath: string,
|
spacePath: string,
|
||||||
functionName: string | undefined,
|
functionName: string | undefined,
|
||||||
args: string[] = [],
|
args: string[] = [],
|
||||||
builtinAssetBundle: AssetBundle,
|
builtinAssetBundle: AssetBundle,
|
||||||
httpServerPort = 3123,
|
kvPrimitives: KvPrimitives,
|
||||||
httpHostname = "127.0.0.1",
|
httpServerPort?: number,
|
||||||
|
httpHostname?: string,
|
||||||
) {
|
) {
|
||||||
const serverController = new AbortController();
|
const serverController = new AbortController();
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
const dbBackend = await determineDatabaseBackend(spacePath);
|
|
||||||
|
|
||||||
if (!dbBackend) {
|
|
||||||
console.error("Cannot run plugs in databaseless mode.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const endpointHook = new EndpointHook("/_/");
|
const endpointHook = new EndpointHook("/_/");
|
||||||
|
|
||||||
const serverSystem = new ServerSystem(
|
const serverSystem = new ServerSystem(
|
||||||
|
@ -34,23 +28,25 @@ export async function runPlug(
|
||||||
new DiskSpacePrimitives(spacePath),
|
new DiskSpacePrimitives(spacePath),
|
||||||
builtinAssetBundle,
|
builtinAssetBundle,
|
||||||
),
|
),
|
||||||
dbBackend,
|
kvPrimitives,
|
||||||
determineShellBackend(spacePath),
|
new LocalShell(spacePath),
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
await serverSystem.init(true);
|
await serverSystem.init(true);
|
||||||
app.use((context, next) => {
|
app.use((context, next) => {
|
||||||
return endpointHook.handleRequest(serverSystem.system!, context, next);
|
return endpointHook.handleRequest(serverSystem.system!, context, next);
|
||||||
});
|
});
|
||||||
Deno.serve({
|
if (httpHostname && httpServerPort) {
|
||||||
hostname: httpHostname,
|
Deno.serve({
|
||||||
port: httpServerPort,
|
hostname: httpHostname,
|
||||||
signal: serverController.signal,
|
port: httpServerPort,
|
||||||
}, app.fetch);
|
signal: serverController.signal,
|
||||||
|
}, app.fetch);
|
||||||
|
}
|
||||||
|
|
||||||
if (functionName) {
|
if (functionName) {
|
||||||
const result = await serverSystem.system.invokeFunction(functionName, args);
|
const result = await serverSystem.system.invokeFunction(functionName, args);
|
||||||
await serverSystem.close();
|
await serverSystem.close();
|
||||||
serverSystem.kvPrimitives.close();
|
|
||||||
serverController.abort();
|
serverController.abort();
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import assets from "../dist/plug_asset_bundle.json" assert {
|
||||||
type: "json",
|
type: "json",
|
||||||
};
|
};
|
||||||
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
|
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
|
||||||
|
import { determineDatabaseBackend } from "../server/db_backend.ts";
|
||||||
|
|
||||||
export async function plugRunCommand(
|
export async function plugRunCommand(
|
||||||
{
|
{
|
||||||
|
@ -20,18 +21,28 @@ export async function plugRunCommand(
|
||||||
spacePath = path.resolve(spacePath);
|
spacePath = path.resolve(spacePath);
|
||||||
console.log("Space path", spacePath);
|
console.log("Space path", spacePath);
|
||||||
console.log("Function to run:", functionName, "with arguments", args);
|
console.log("Function to run:", functionName, "with arguments", args);
|
||||||
|
|
||||||
|
const kvPrimitives = await determineDatabaseBackend(spacePath);
|
||||||
|
|
||||||
|
if (!kvPrimitives) {
|
||||||
|
console.error("Cannot run plugs in databaseless mode.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await runPlug(
|
const result = await runPlug(
|
||||||
spacePath,
|
spacePath,
|
||||||
functionName,
|
functionName,
|
||||||
args,
|
args,
|
||||||
new AssetBundle(assets),
|
new AssetBundle(assets),
|
||||||
|
kvPrimitives,
|
||||||
port,
|
port,
|
||||||
hostname,
|
hostname,
|
||||||
);
|
);
|
||||||
if (result) {
|
if (result) {
|
||||||
console.log("Output", result);
|
console.log("Output", result);
|
||||||
}
|
}
|
||||||
|
kvPrimitives.close();
|
||||||
Deno.exit(0);
|
Deno.exit(0);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e.message);
|
console.error(e.message);
|
||||||
|
|
|
@ -10,6 +10,8 @@ import { sleep } from "$sb/lib/async.ts";
|
||||||
|
|
||||||
import { determineDatabaseBackend } from "../server/db_backend.ts";
|
import { determineDatabaseBackend } from "../server/db_backend.ts";
|
||||||
import { SpaceServerConfig } from "../server/instance.ts";
|
import { SpaceServerConfig } from "../server/instance.ts";
|
||||||
|
import { runPlug } from "../cli/plug_run.ts";
|
||||||
|
import { PrefixedKvPrimitives } from "../plugos/lib/prefixed_kv_primitives.ts";
|
||||||
|
|
||||||
export async function serveCommand(
|
export async function serveCommand(
|
||||||
options: {
|
options: {
|
||||||
|
@ -41,6 +43,8 @@ export async function serveCommand(
|
||||||
|
|
||||||
const syncOnly = options.syncOnly || !!Deno.env.get("SB_SYNC_ONLY");
|
const syncOnly = options.syncOnly || !!Deno.env.get("SB_SYNC_ONLY");
|
||||||
|
|
||||||
|
const readOnly = !!Deno.env.get("SB_READ_ONLY");
|
||||||
|
|
||||||
if (syncOnly) {
|
if (syncOnly) {
|
||||||
console.log("Running in sync-only mode (no backend processing)");
|
console.log("Running in sync-only mode (no backend processing)");
|
||||||
}
|
}
|
||||||
|
@ -75,6 +79,9 @@ export async function serveCommand(
|
||||||
const [user, pass] = userAuth.split(":");
|
const [user, pass] = userAuth.split(":");
|
||||||
userCredentials = { user, pass };
|
userCredentials = { user, pass };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const backendConfig = Deno.env.get("SB_SHELL_BACKEND") || "local";
|
||||||
|
|
||||||
const configs = new Map<string, SpaceServerConfig>();
|
const configs = new Map<string, SpaceServerConfig>();
|
||||||
configs.set("*", {
|
configs.set("*", {
|
||||||
hostname,
|
hostname,
|
||||||
|
@ -82,15 +89,29 @@ export async function serveCommand(
|
||||||
auth: userCredentials,
|
auth: userCredentials,
|
||||||
authToken: Deno.env.get("SB_AUTH_TOKEN"),
|
authToken: Deno.env.get("SB_AUTH_TOKEN"),
|
||||||
syncOnly,
|
syncOnly,
|
||||||
|
readOnly,
|
||||||
|
shellBackend: backendConfig,
|
||||||
clientEncryption,
|
clientEncryption,
|
||||||
pagesPath: folder,
|
pagesPath: folder,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const plugAssets = new AssetBundle(plugAssetBundle as AssetJson);
|
||||||
|
|
||||||
|
if (readOnly) {
|
||||||
|
console.log("Indexing the space first. Hang on...");
|
||||||
|
await runPlug(
|
||||||
|
folder,
|
||||||
|
"index.reindexSpace",
|
||||||
|
[],
|
||||||
|
plugAssets,
|
||||||
|
new PrefixedKvPrimitives(baseKvPrimitives, ["*"]),
|
||||||
|
);
|
||||||
|
}
|
||||||
const httpServer = new HttpServer({
|
const httpServer = new HttpServer({
|
||||||
hostname,
|
hostname,
|
||||||
port,
|
port,
|
||||||
clientAssetBundle: new AssetBundle(clientAssetBundle as AssetJson),
|
clientAssetBundle: new AssetBundle(clientAssetBundle as AssetJson),
|
||||||
plugAssetBundle: new AssetBundle(plugAssetBundle as AssetJson),
|
plugAssetBundle: plugAssets,
|
||||||
baseKvPrimitives,
|
baseKvPrimitives,
|
||||||
keyFile: options.key,
|
keyFile: options.key,
|
||||||
certFile: options.cert,
|
certFile: options.cert,
|
||||||
|
|
|
@ -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 { SysCallMapping, System } from "../../plugos/system.ts";
|
||||||
import type { Client } from "../client.ts";
|
import type { Client } from "../../web/client.ts";
|
||||||
import { CommandDef } from "../hooks/command.ts";
|
import { CommandDef } from "../../web/hooks/command.ts";
|
||||||
import { proxySyscall } from "./util.ts";
|
import { proxySyscall } from "../../web/syscalls/util.ts";
|
||||||
|
|
||||||
export function systemSyscalls(
|
export function systemSyscalls(
|
||||||
system: System<any>,
|
system: System<any>,
|
||||||
|
readOnlyMode: boolean,
|
||||||
client?: Client,
|
client?: Client,
|
||||||
): SysCallMapping {
|
): SysCallMapping {
|
||||||
const api: SysCallMapping = {
|
const api: SysCallMapping = {
|
||||||
|
@ -64,6 +65,9 @@ export function systemSyscalls(
|
||||||
"system.getEnv": () => {
|
"system.getEnv": () => {
|
||||||
return system.env;
|
return system.env;
|
||||||
},
|
},
|
||||||
|
"system.getMode": () => {
|
||||||
|
return readOnlyMode ? "ro" : "rw";
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return api;
|
return api;
|
||||||
}
|
}
|
|
@ -3,3 +3,10 @@ import { syscall } from "./syscall.ts";
|
||||||
export function resetClient() {
|
export function resetClient() {
|
||||||
return syscall("debug.resetClient");
|
return syscall("debug.resetClient");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wipes the entire state KV store and the entire space KV store.
|
||||||
|
*/
|
||||||
|
export function cleanup() {
|
||||||
|
return syscall("debug.cleanup");
|
||||||
|
}
|
||||||
|
|
|
@ -26,3 +26,7 @@ export function reloadPlugs() {
|
||||||
export function getEnv(): Promise<string | undefined> {
|
export function getEnv(): Promise<string | undefined> {
|
||||||
return syscall("system.getEnv");
|
return syscall("system.getEnv");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMode(): Promise<"ro" | "rw"> {
|
||||||
|
return syscall("system.getMode");
|
||||||
|
}
|
||||||
|
|
|
@ -101,11 +101,6 @@ setupMessageListener(functionMapping, manifest);
|
||||||
metafile: options.info,
|
metafile: options.info,
|
||||||
treeShaking: true,
|
treeShaking: true,
|
||||||
plugins: [
|
plugins: [
|
||||||
// {
|
|
||||||
// name: "json",
|
|
||||||
// setup: (build) =>
|
|
||||||
// build.onLoad({ filter: /\.json$/ }, () => ({ loader: "json" })),
|
|
||||||
// },
|
|
||||||
...denoPlugins({
|
...denoPlugins({
|
||||||
// TODO do this differently
|
// TODO do this differently
|
||||||
importMapURL: options.importMap ||
|
importMapURL: options.importMap ||
|
||||||
|
|
|
@ -7,7 +7,26 @@ import type { SysCallMapping } from "../system.ts";
|
||||||
* @param ds the datastore to wrap
|
* @param ds the datastore to wrap
|
||||||
* @param prefix prefix to scope all keys to to which the plug name will be appended
|
* @param prefix prefix to scope all keys to to which the plug name will be appended
|
||||||
*/
|
*/
|
||||||
export function dataStoreSyscalls(ds: DataStore): SysCallMapping {
|
export function dataStoreReadSyscalls(ds: DataStore): SysCallMapping {
|
||||||
|
return {
|
||||||
|
"datastore.batchGet": (
|
||||||
|
_ctx,
|
||||||
|
keys: KvKey[],
|
||||||
|
): Promise<(any | undefined)[]> => {
|
||||||
|
return ds.batchGet(keys);
|
||||||
|
},
|
||||||
|
|
||||||
|
"datastore.get": (_ctx, key: KvKey): Promise<any | null> => {
|
||||||
|
return ds.get(key);
|
||||||
|
},
|
||||||
|
|
||||||
|
"datastore.query": async (_ctx, query: KvQuery): Promise<KV[]> => {
|
||||||
|
return (await ds.query(query));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dataStoreWriteSyscalls(ds: DataStore): SysCallMapping {
|
||||||
return {
|
return {
|
||||||
"datastore.delete": (_ctx, key: KvKey) => {
|
"datastore.delete": (_ctx, key: KvKey) => {
|
||||||
return ds.delete(key);
|
return ds.delete(key);
|
||||||
|
@ -25,21 +44,6 @@ export function dataStoreSyscalls(ds: DataStore): SysCallMapping {
|
||||||
return ds.batchDelete(keys);
|
return ds.batchDelete(keys);
|
||||||
},
|
},
|
||||||
|
|
||||||
"datastore.batchGet": (
|
|
||||||
_ctx,
|
|
||||||
keys: KvKey[],
|
|
||||||
): Promise<(any | undefined)[]> => {
|
|
||||||
return ds.batchGet(keys);
|
|
||||||
},
|
|
||||||
|
|
||||||
"datastore.get": (_ctx, key: KvKey): Promise<any | null> => {
|
|
||||||
return ds.get(key);
|
|
||||||
},
|
|
||||||
|
|
||||||
"datastore.query": async (_ctx, query: KvQuery): Promise<KV[]> => {
|
|
||||||
return (await ds.query(query));
|
|
||||||
},
|
|
||||||
|
|
||||||
"datastore.queryDelete": (_ctx, query: KvQuery): Promise<void> => {
|
"datastore.queryDelete": (_ctx, query: KvQuery): Promise<void> => {
|
||||||
return ds.queryDelete(query);
|
return ds.queryDelete(query);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"
|
path: "./page.ts:deletePage"
|
||||||
command:
|
command:
|
||||||
name: "Page: Delete"
|
name: "Page: Delete"
|
||||||
|
requireMode: rw
|
||||||
copyPage:
|
copyPage:
|
||||||
path: "./page.ts:copyPage"
|
path: "./page.ts:copyPage"
|
||||||
command:
|
command:
|
||||||
name: "Page: Copy"
|
name: "Page: Copy"
|
||||||
|
requireMode: rw
|
||||||
|
|
||||||
# Completion
|
# Completion
|
||||||
pageComplete:
|
pageComplete:
|
||||||
|
@ -54,6 +56,7 @@ functions:
|
||||||
path: editor.ts:reloadSettingsAndCommands
|
path: editor.ts:reloadSettingsAndCommands
|
||||||
command:
|
command:
|
||||||
name: "System: Reload Settings and Commands"
|
name: "System: Reload Settings and Commands"
|
||||||
|
requireMode: rw
|
||||||
|
|
||||||
# Navigation
|
# Navigation
|
||||||
linkNavigate:
|
linkNavigate:
|
||||||
|
@ -89,22 +92,26 @@ functions:
|
||||||
name: "Text: Quote Selection"
|
name: "Text: Quote Selection"
|
||||||
key: "Ctrl-Shift-."
|
key: "Ctrl-Shift-."
|
||||||
mac: "Cmd-Shift-."
|
mac: "Cmd-Shift-."
|
||||||
|
requireMode: rw
|
||||||
listifySelection:
|
listifySelection:
|
||||||
path: ./text.ts:listifySelection
|
path: ./text.ts:listifySelection
|
||||||
command:
|
command:
|
||||||
name: "Text: Listify Selection"
|
name: "Text: Listify Selection"
|
||||||
key: "Ctrl-Shift-8"
|
key: "Ctrl-Shift-8"
|
||||||
mac: "Cmd-Shift-8"
|
mac: "Cmd-Shift-8"
|
||||||
|
requireMode: rw
|
||||||
numberListifySelection:
|
numberListifySelection:
|
||||||
path: ./text.ts:numberListifySelection
|
path: ./text.ts:numberListifySelection
|
||||||
command:
|
command:
|
||||||
name: "Text: Number Listify Selection"
|
name: "Text: Number Listify Selection"
|
||||||
|
requireMode: rw
|
||||||
linkSelection:
|
linkSelection:
|
||||||
path: ./text.ts:linkSelection
|
path: ./text.ts:linkSelection
|
||||||
command:
|
command:
|
||||||
name: "Text: Link Selection"
|
name: "Text: Link Selection"
|
||||||
key: "Ctrl-Shift-k"
|
key: "Ctrl-Shift-k"
|
||||||
mac: "Cmd-Shift-k"
|
mac: "Cmd-Shift-k"
|
||||||
|
requireMode: rw
|
||||||
bold:
|
bold:
|
||||||
path: ./text.ts:wrapSelection
|
path: ./text.ts:wrapSelection
|
||||||
command:
|
command:
|
||||||
|
@ -112,6 +119,7 @@ functions:
|
||||||
key: "Ctrl-b"
|
key: "Ctrl-b"
|
||||||
mac: "Cmd-b"
|
mac: "Cmd-b"
|
||||||
wrapper: "**"
|
wrapper: "**"
|
||||||
|
requireMode: rw
|
||||||
italic:
|
italic:
|
||||||
path: ./text.ts:wrapSelection
|
path: ./text.ts:wrapSelection
|
||||||
command:
|
command:
|
||||||
|
@ -119,23 +127,27 @@ functions:
|
||||||
key: "Ctrl-i"
|
key: "Ctrl-i"
|
||||||
mac: "Cmd-i"
|
mac: "Cmd-i"
|
||||||
wrapper: "_"
|
wrapper: "_"
|
||||||
|
requireMode: rw
|
||||||
strikethrough:
|
strikethrough:
|
||||||
path: ./text.ts:wrapSelection
|
path: ./text.ts:wrapSelection
|
||||||
command:
|
command:
|
||||||
name: "Text: Strikethrough"
|
name: "Text: Strikethrough"
|
||||||
key: "Ctrl-Shift-s"
|
key: "Ctrl-Shift-s"
|
||||||
wrapper: "~~"
|
wrapper: "~~"
|
||||||
|
requireMode: rw
|
||||||
marker:
|
marker:
|
||||||
path: ./text.ts:wrapSelection
|
path: ./text.ts:wrapSelection
|
||||||
command:
|
command:
|
||||||
name: "Text: Marker"
|
name: "Text: Marker"
|
||||||
key: "Alt-m"
|
key: "Alt-m"
|
||||||
wrapper: "=="
|
wrapper: "=="
|
||||||
|
requireMode: rw
|
||||||
centerCursor:
|
centerCursor:
|
||||||
path: "./editor.ts:centerCursorCommand"
|
path: "./editor.ts:centerCursorCommand"
|
||||||
command:
|
command:
|
||||||
name: "Navigate: Center Cursor"
|
name: "Navigate: Center Cursor"
|
||||||
key: "Ctrl-Alt-l"
|
key: "Ctrl-Alt-l"
|
||||||
|
requireMode: rw
|
||||||
|
|
||||||
# Debug commands
|
# Debug commands
|
||||||
parseCommand:
|
parseCommand:
|
||||||
|
@ -150,6 +162,7 @@ functions:
|
||||||
name: "Link: Unfurl"
|
name: "Link: Unfurl"
|
||||||
key: "Ctrl-Shift-u"
|
key: "Ctrl-Shift-u"
|
||||||
mac: "Cmd-Shift-u"
|
mac: "Cmd-Shift-u"
|
||||||
|
requireMode: rw
|
||||||
contexts:
|
contexts:
|
||||||
- NakedURL
|
- NakedURL
|
||||||
|
|
||||||
|
@ -180,11 +193,6 @@ functions:
|
||||||
events:
|
events:
|
||||||
- editor:modeswitch
|
- editor:modeswitch
|
||||||
|
|
||||||
brokenLinksCommand:
|
|
||||||
path: ./broken_links.ts:brokenLinksCommand
|
|
||||||
command:
|
|
||||||
name: "Broken Links: Show"
|
|
||||||
|
|
||||||
# Random stuff
|
# Random stuff
|
||||||
statsCommand:
|
statsCommand:
|
||||||
path: ./stats.ts:statsCommand
|
path: ./stats.ts:statsCommand
|
||||||
|
@ -210,14 +218,15 @@ functions:
|
||||||
name: "Help: Getting Started"
|
name: "Help: Getting Started"
|
||||||
|
|
||||||
accountLogoutCommand:
|
accountLogoutCommand:
|
||||||
path: ./account.ts:accountLogoutCommand
|
path: clean.ts:cleanCommand
|
||||||
command:
|
command:
|
||||||
name: "Account: Logout"
|
name: "Clear Local Storage & Logout"
|
||||||
|
|
||||||
uploadFileCommand:
|
uploadFileCommand:
|
||||||
path: ./upload.ts:uploadFile
|
path: ./upload.ts:uploadFile
|
||||||
command:
|
command:
|
||||||
name: "Upload: File"
|
name: "Upload: File"
|
||||||
|
requireMode: rw
|
||||||
|
|
||||||
# Outline commands
|
# Outline commands
|
||||||
outlineMoveUp:
|
outlineMoveUp:
|
||||||
|
@ -225,24 +234,28 @@ functions:
|
||||||
command:
|
command:
|
||||||
name: "Outline: Move Up"
|
name: "Outline: Move Up"
|
||||||
key: "Alt-ArrowUp"
|
key: "Alt-ArrowUp"
|
||||||
|
requireMode: rw
|
||||||
|
|
||||||
outlineMoveDown:
|
outlineMoveDown:
|
||||||
path: ./outline.ts:moveItemDown
|
path: ./outline.ts:moveItemDown
|
||||||
command:
|
command:
|
||||||
name: "Outline: Move Down"
|
name: "Outline: Move Down"
|
||||||
key: "Alt-ArrowDown"
|
key: "Alt-ArrowDown"
|
||||||
|
requireMode: rw
|
||||||
|
|
||||||
outlineIndent:
|
outlineIndent:
|
||||||
path: ./outline.ts:indentItem
|
path: ./outline.ts:indentItem
|
||||||
command:
|
command:
|
||||||
name: "Outline: Move Right"
|
name: "Outline: Move Right"
|
||||||
key: "Alt->"
|
key: "Alt->"
|
||||||
|
requireMode: rw
|
||||||
|
|
||||||
outlineOutdent:
|
outlineOutdent:
|
||||||
path: ./outline.ts:outdentItem
|
path: ./outline.ts:outdentItem
|
||||||
command:
|
command:
|
||||||
name: "Outline: Move Left"
|
name: "Outline: Move Left"
|
||||||
key: "Alt-<"
|
key: "Alt-<"
|
||||||
|
requireMode: rw
|
||||||
|
|
||||||
# Outline folding commands
|
# Outline folding commands
|
||||||
foldCommand:
|
foldCommand:
|
||||||
|
|
|
@ -33,4 +33,5 @@ functions:
|
||||||
importLibraryCommand:
|
importLibraryCommand:
|
||||||
path: library.ts:importLibraryCommand
|
path: library.ts:importLibraryCommand
|
||||||
command:
|
command:
|
||||||
name: "Library: Import"
|
name: "Library: Import"
|
||||||
|
requireMode: rw
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { datastore } from "$sb/syscalls.ts";
|
||||||
import { KV, KvKey, KvQuery, ObjectQuery, ObjectValue } from "$sb/types.ts";
|
import { KV, KvKey, KvQuery, ObjectQuery, ObjectValue } from "$sb/types.ts";
|
||||||
import { QueryProviderEvent } from "$sb/app_event.ts";
|
import { QueryProviderEvent } from "$sb/app_event.ts";
|
||||||
import { builtins } from "./builtins.ts";
|
import { builtins } from "./builtins.ts";
|
||||||
import { AttributeObject, determineType } from "./attributes.ts";
|
import { determineType } from "./attributes.ts";
|
||||||
import { ttlCache } from "$sb/lib/memory_cache.ts";
|
import { ttlCache } from "$sb/lib/memory_cache.ts";
|
||||||
|
|
||||||
const indexKey = "idx";
|
const indexKey = "idx";
|
||||||
|
@ -47,12 +47,17 @@ export async function clearPageIndex(page: string): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the entire datastore for this indexKey plug
|
* Clears the entire page index
|
||||||
*/
|
*/
|
||||||
export async function clearIndex(): Promise<void> {
|
export async function clearIndex(): Promise<void> {
|
||||||
const allKeys: KvKey[] = [];
|
const allKeys: KvKey[] = [];
|
||||||
for (
|
for (
|
||||||
const { key } of await datastore.query({ prefix: [] })
|
const { key } of await datastore.query({ prefix: [indexKey] })
|
||||||
|
) {
|
||||||
|
allKeys.push(key);
|
||||||
|
}
|
||||||
|
for (
|
||||||
|
const { key } of await datastore.query({ prefix: [pageKey] })
|
||||||
) {
|
) {
|
||||||
allKeys.push(key);
|
allKeys.push(key);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { ObjectValue } from "$sb/types.ts";
|
import { ObjectValue } from "$sb/types.ts";
|
||||||
|
import { system } from "$sb/syscalls.ts";
|
||||||
import { indexObjects } from "./api.ts";
|
import { indexObjects } from "./api.ts";
|
||||||
import { AttributeObject } from "./attributes.ts";
|
|
||||||
import { TagObject } from "./tags.ts";
|
|
||||||
|
|
||||||
export const builtinPseudoPage = ":builtin:";
|
export const builtinPseudoPage = ":builtin:";
|
||||||
|
|
||||||
|
@ -92,6 +91,10 @@ export const builtins: Record<string, Record<string, string>> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function loadBuiltinsIntoIndex() {
|
export async function loadBuiltinsIntoIndex() {
|
||||||
|
if (await system.getMode() === "ro") {
|
||||||
|
console.log("Running in read-only mode, not loading builtins");
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log("Loading builtins attributes into index");
|
console.log("Loading builtins attributes into index");
|
||||||
const allObjects: ObjectValue<any>[] = [];
|
const allObjects: ObjectValue<any>[] = [];
|
||||||
for (const [tagName, attributes] of Object.entries(builtins)) {
|
for (const [tagName, attributes] of Object.entries(builtins)) {
|
||||||
|
|
|
@ -11,6 +11,10 @@ export async function reindexCommand() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function reindexSpace() {
|
export async function reindexSpace() {
|
||||||
|
if (await system.getMode() === "ro") {
|
||||||
|
console.info("Not reindexing because we're in read-only mode");
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log("Clearing page index...");
|
console.log("Clearing page index...");
|
||||||
// Executed this way to not have to embed the search plug code here
|
// Executed this way to not have to embed the search plug code here
|
||||||
await system.invokeFunction("index.clearIndex");
|
await system.invokeFunction("index.clearIndex");
|
||||||
|
@ -55,6 +59,10 @@ export async function processIndexQueue(messages: MQMessage[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function parseIndexTextRepublish({ name, text }: IndexEvent) {
|
export async function parseIndexTextRepublish({ name, text }: IndexEvent) {
|
||||||
|
if (await system.getMode() === "ro") {
|
||||||
|
console.info("Not reindexing", name, "because we're in read-only mode");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const parsed = await markdown.parseMarkdown(text);
|
const parsed = await markdown.parseMarkdown(text);
|
||||||
|
|
||||||
if (isTemplate(text)) {
|
if (isTemplate(text)) {
|
||||||
|
|
|
@ -51,6 +51,7 @@ functions:
|
||||||
path: "./command.ts:reindexCommand"
|
path: "./command.ts:reindexCommand"
|
||||||
command:
|
command:
|
||||||
name: "Space: Reindex"
|
name: "Space: Reindex"
|
||||||
|
requireMode: rw
|
||||||
processIndexQueue:
|
processIndexQueue:
|
||||||
path: ./command.ts:processIndexQueue
|
path: ./command.ts:processIndexQueue
|
||||||
mqSubscriptions:
|
mqSubscriptions:
|
||||||
|
@ -142,16 +143,19 @@ functions:
|
||||||
mac: Cmd-Alt-r
|
mac: Cmd-Alt-r
|
||||||
key: Ctrl-Alt-r
|
key: Ctrl-Alt-r
|
||||||
page: ""
|
page: ""
|
||||||
|
requireMode: rw
|
||||||
renamePrefixCommand:
|
renamePrefixCommand:
|
||||||
path: "./refactor.ts:renamePrefixCommand"
|
path: "./refactor.ts:renamePrefixCommand"
|
||||||
command:
|
command:
|
||||||
name: "Page: Batch Rename Prefix"
|
name: "Page: Batch Rename Prefix"
|
||||||
|
requireMode: rw
|
||||||
|
|
||||||
# Refactoring Commands
|
# Refactoring Commands
|
||||||
extractToPageCommand:
|
extractToPageCommand:
|
||||||
path: ./refactor.ts:extractToPageCommand
|
path: ./refactor.ts:extractToPageCommand
|
||||||
command:
|
command:
|
||||||
name: "Page: Extract"
|
name: "Page: Extract"
|
||||||
|
requireMode: rw
|
||||||
|
|
||||||
# TOC
|
# TOC
|
||||||
tocWidget:
|
tocWidget:
|
||||||
|
|
|
@ -8,6 +8,7 @@ functions:
|
||||||
name: "Plugs: Update"
|
name: "Plugs: Update"
|
||||||
key: "Ctrl-Shift-p"
|
key: "Ctrl-Shift-p"
|
||||||
mac: "Cmd-Shift-p"
|
mac: "Cmd-Shift-p"
|
||||||
|
requireMode: rw
|
||||||
getPlugHTTPS:
|
getPlugHTTPS:
|
||||||
path: "./plugmanager.ts:getPlugHTTPS"
|
path: "./plugmanager.ts:getPlugHTTPS"
|
||||||
events:
|
events:
|
||||||
|
@ -24,3 +25,4 @@ functions:
|
||||||
path: ./plugmanager.ts:addPlugCommand
|
path: ./plugmanager.ts:addPlugCommand
|
||||||
command:
|
command:
|
||||||
name: "Plugs: Add"
|
name: "Plugs: Add"
|
||||||
|
requireMode: rw
|
||||||
|
|
|
@ -5,4 +5,5 @@ functions:
|
||||||
command:
|
command:
|
||||||
name: "Share: Publish"
|
name: "Share: Publish"
|
||||||
key: "Ctrl-s"
|
key: "Ctrl-s"
|
||||||
mac: "Cmd-s"
|
mac: "Cmd-s"
|
||||||
|
requireMode: rw
|
||||||
|
|
|
@ -17,11 +17,13 @@ functions:
|
||||||
command:
|
command:
|
||||||
name: "Task: Cycle State"
|
name: "Task: Cycle State"
|
||||||
key: Alt-t
|
key: Alt-t
|
||||||
|
requireMode: rw
|
||||||
taskPostponeCommand:
|
taskPostponeCommand:
|
||||||
path: ./task.ts:postponeCommand
|
path: ./task.ts:postponeCommand
|
||||||
command:
|
command:
|
||||||
name: "Task: Postpone"
|
name: "Task: Postpone"
|
||||||
key: Alt-+
|
key: Alt-+
|
||||||
|
requireMode: rw
|
||||||
contexts:
|
contexts:
|
||||||
- DeadlineDate
|
- DeadlineDate
|
||||||
previewTaskToggle:
|
previewTaskToggle:
|
||||||
|
@ -37,4 +39,5 @@ functions:
|
||||||
removeCompletedTasksCommand:
|
removeCompletedTasksCommand:
|
||||||
path: task.ts:removeCompletedTasksCommand
|
path: task.ts:removeCompletedTasksCommand
|
||||||
command:
|
command:
|
||||||
name: "Task: Remove Completed"
|
name: "Task: Remove Completed"
|
||||||
|
requireMode: rw
|
||||||
|
|
|
@ -44,6 +44,7 @@ functions:
|
||||||
command:
|
command:
|
||||||
name: "Page: From Template"
|
name: "Page: From Template"
|
||||||
key: "Alt-Shift-t"
|
key: "Alt-Shift-t"
|
||||||
|
requireMode: rw
|
||||||
|
|
||||||
|
|
||||||
# Lint
|
# Lint
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
|
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
|
||||||
import { FileMeta } from "$sb/types.ts";
|
import { FileMeta } from "$sb/types.ts";
|
||||||
import { ShellRequest } from "./rpc.ts";
|
import { ShellRequest } from "./rpc.ts";
|
||||||
import { determineShellBackend } from "./shell_backend.ts";
|
|
||||||
import { SpaceServer, SpaceServerConfig } from "./instance.ts";
|
import { SpaceServer, SpaceServerConfig } from "./instance.ts";
|
||||||
import { KvPrimitives } from "../plugos/lib/kv_primitives.ts";
|
import { KvPrimitives } from "../plugos/lib/kv_primitives.ts";
|
||||||
import { PrefixedKvPrimitives } from "../plugos/lib/prefixed_kv_primitives.ts";
|
import { PrefixedKvPrimitives } from "../plugos/lib/prefixed_kv_primitives.ts";
|
||||||
|
@ -58,7 +57,6 @@ export class HttpServer {
|
||||||
async bootSpaceServer(config: SpaceServerConfig): Promise<SpaceServer> {
|
async bootSpaceServer(config: SpaceServerConfig): Promise<SpaceServer> {
|
||||||
const spaceServer = new SpaceServer(
|
const spaceServer = new SpaceServer(
|
||||||
config,
|
config,
|
||||||
determineShellBackend(config.pagesPath),
|
|
||||||
this.plugAssetBundle,
|
this.plugAssetBundle,
|
||||||
new PrefixedKvPrimitives(this.baseKvPrimitives, [
|
new PrefixedKvPrimitives(this.baseKvPrimitives, [
|
||||||
config.namespace,
|
config.namespace,
|
||||||
|
@ -119,6 +117,9 @@ export class HttpServer {
|
||||||
).replaceAll(
|
).replaceAll(
|
||||||
"{{SYNC_ONLY}}",
|
"{{SYNC_ONLY}}",
|
||||||
spaceServer.syncOnly ? "true" : "false",
|
spaceServer.syncOnly ? "true" : "false",
|
||||||
|
).replaceAll(
|
||||||
|
"{{READ_ONLY}}",
|
||||||
|
spaceServer.readOnly ? "true" : "false",
|
||||||
).replaceAll(
|
).replaceAll(
|
||||||
"{{CLIENT_ENCRYPTION}}",
|
"{{CLIENT_ENCRYPTION}}",
|
||||||
spaceServer.clientEncryption ? "true" : "false",
|
spaceServer.clientEncryption ? "true" : "false",
|
||||||
|
@ -217,6 +218,7 @@ export class HttpServer {
|
||||||
JSON.stringify([
|
JSON.stringify([
|
||||||
spaceServer.clientEncryption,
|
spaceServer.clientEncryption,
|
||||||
spaceServer.syncOnly,
|
spaceServer.syncOnly,
|
||||||
|
spaceServer.readOnly,
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -510,7 +512,10 @@ export class HttpServer {
|
||||||
const req = c.req;
|
const req = c.req;
|
||||||
const name = req.param("path")!;
|
const name = req.param("path")!;
|
||||||
const spaceServer = await this.ensureSpaceServer(req);
|
const spaceServer = await this.ensureSpaceServer(req);
|
||||||
console.log("Saving file", name);
|
if (spaceServer.readOnly) {
|
||||||
|
return c.text("Read only mode, no writes allowed", 405);
|
||||||
|
}
|
||||||
|
console.log("Writing file", name);
|
||||||
if (name.startsWith(".")) {
|
if (name.startsWith(".")) {
|
||||||
// Don't expose hidden files
|
// Don't expose hidden files
|
||||||
return c.text("Forbidden", 403);
|
return c.text("Forbidden", 403);
|
||||||
|
@ -533,6 +538,9 @@ export class HttpServer {
|
||||||
const req = c.req;
|
const req = c.req;
|
||||||
const name = req.param("path")!;
|
const name = req.param("path")!;
|
||||||
const spaceServer = await this.ensureSpaceServer(req);
|
const spaceServer = await this.ensureSpaceServer(req);
|
||||||
|
if (spaceServer.readOnly) {
|
||||||
|
return c.text("Read only mode, no writes allowed", 405);
|
||||||
|
}
|
||||||
console.log("Deleting file", name);
|
console.log("Deleting file", name);
|
||||||
if (name.startsWith(".")) {
|
if (name.startsWith(".")) {
|
||||||
// Don't expose hidden files
|
// Don't expose hidden files
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { SilverBulletHooks } from "../common/manifest.ts";
|
import { SilverBulletHooks } from "../common/manifest.ts";
|
||||||
import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts";
|
import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts";
|
||||||
import { FilteredSpacePrimitives } from "../common/spaces/filtered_space_primitives.ts";
|
import { FilteredSpacePrimitives } from "../common/spaces/filtered_space_primitives.ts";
|
||||||
|
import { ReadOnlySpacePrimitives } from "../common/spaces/ro_space_primitives.ts";
|
||||||
import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
|
import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
|
||||||
import { ensureAndLoadSettingsAndIndex } from "../common/util.ts";
|
import { ensureAndLoadSettingsAndIndex } from "../common/util.ts";
|
||||||
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
|
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
|
||||||
|
@ -10,6 +11,7 @@ import { BuiltinSettings } from "../web/types.ts";
|
||||||
import { JWTIssuer } from "./crypto.ts";
|
import { JWTIssuer } from "./crypto.ts";
|
||||||
import { gitIgnoreCompiler } from "./deps.ts";
|
import { gitIgnoreCompiler } from "./deps.ts";
|
||||||
import { ServerSystem } from "./server_system.ts";
|
import { ServerSystem } from "./server_system.ts";
|
||||||
|
import { determineShellBackend, NotSupportedShell } from "./shell_backend.ts";
|
||||||
import { ShellBackend } from "./shell_backend.ts";
|
import { ShellBackend } from "./shell_backend.ts";
|
||||||
import { determineStorageBackend } from "./storage_backend.ts";
|
import { determineStorageBackend } from "./storage_backend.ts";
|
||||||
|
|
||||||
|
@ -21,8 +23,10 @@ export type SpaceServerConfig = {
|
||||||
// Additional API auth token
|
// Additional API auth token
|
||||||
authToken?: string;
|
authToken?: string;
|
||||||
pagesPath: string;
|
pagesPath: string;
|
||||||
syncOnly?: boolean;
|
shellBackend: string;
|
||||||
clientEncryption?: boolean;
|
syncOnly: boolean;
|
||||||
|
readOnly: boolean;
|
||||||
|
clientEncryption: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class SpaceServer {
|
export class SpaceServer {
|
||||||
|
@ -41,10 +45,11 @@ export class SpaceServer {
|
||||||
system?: System<SilverBulletHooks>;
|
system?: System<SilverBulletHooks>;
|
||||||
clientEncryption: boolean;
|
clientEncryption: boolean;
|
||||||
syncOnly: boolean;
|
syncOnly: boolean;
|
||||||
|
readOnly: boolean;
|
||||||
|
shellBackend: ShellBackend;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
config: SpaceServerConfig,
|
config: SpaceServerConfig,
|
||||||
public shellBackend: ShellBackend,
|
|
||||||
private plugAssetBundle: AssetBundle,
|
private plugAssetBundle: AssetBundle,
|
||||||
private kvPrimitives: KvPrimitives,
|
private kvPrimitives: KvPrimitives,
|
||||||
) {
|
) {
|
||||||
|
@ -53,13 +58,18 @@ export class SpaceServer {
|
||||||
this.auth = config.auth;
|
this.auth = config.auth;
|
||||||
this.authToken = config.authToken;
|
this.authToken = config.authToken;
|
||||||
this.clientEncryption = !!config.clientEncryption;
|
this.clientEncryption = !!config.clientEncryption;
|
||||||
this.syncOnly = !!config.syncOnly;
|
this.syncOnly = config.syncOnly;
|
||||||
|
this.readOnly = config.readOnly;
|
||||||
if (this.clientEncryption) {
|
if (this.clientEncryption) {
|
||||||
// Sync only will forced on when encryption is enabled
|
// Sync only will forced on when encryption is enabled
|
||||||
this.syncOnly = true;
|
this.syncOnly = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.jwtIssuer = new JWTIssuer(kvPrimitives);
|
this.jwtIssuer = new JWTIssuer(kvPrimitives);
|
||||||
|
|
||||||
|
this.shellBackend = config.readOnly
|
||||||
|
? new NotSupportedShell() // No shell for read only mode
|
||||||
|
: determineShellBackend(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
@ -81,6 +91,10 @@ export class SpaceServer {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (this.readOnly) {
|
||||||
|
this.spacePrimitives = new ReadOnlySpacePrimitives(this.spacePrimitives);
|
||||||
|
}
|
||||||
|
|
||||||
// system = undefined in databaseless mode (no PlugOS instance on the server and no DB)
|
// system = undefined in databaseless mode (no PlugOS instance on the server and no DB)
|
||||||
if (!this.syncOnly) {
|
if (!this.syncOnly) {
|
||||||
// Enable server-side processing
|
// Enable server-side processing
|
||||||
|
@ -88,6 +102,7 @@ export class SpaceServer {
|
||||||
this.spacePrimitives,
|
this.spacePrimitives,
|
||||||
this.kvPrimitives,
|
this.kvPrimitives,
|
||||||
this.shellBackend,
|
this.shellBackend,
|
||||||
|
this.readOnly,
|
||||||
);
|
);
|
||||||
this.serverSystem = serverSystem;
|
this.serverSystem = serverSystem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,10 +11,9 @@ import { eventSyscalls } from "../plugos/syscalls/event.ts";
|
||||||
import { mqSyscalls } from "../plugos/syscalls/mq.ts";
|
import { mqSyscalls } from "../plugos/syscalls/mq.ts";
|
||||||
import { System } from "../plugos/system.ts";
|
import { System } from "../plugos/system.ts";
|
||||||
import { Space } from "../web/space.ts";
|
import { Space } from "../web/space.ts";
|
||||||
import { debugSyscalls } from "../web/syscalls/debug.ts";
|
|
||||||
import { markdownSyscalls } from "../common/syscalls/markdown.ts";
|
import { markdownSyscalls } from "../common/syscalls/markdown.ts";
|
||||||
import { spaceSyscalls } from "./syscalls/space.ts";
|
import { spaceReadSyscalls, spaceWriteSyscalls } from "./syscalls/space.ts";
|
||||||
import { systemSyscalls } from "../web/syscalls/system.ts";
|
import { systemSyscalls } from "../common/syscalls/system.ts";
|
||||||
import { yamlSyscalls } from "../common/syscalls/yaml.ts";
|
import { yamlSyscalls } from "../common/syscalls/yaml.ts";
|
||||||
import { sandboxFetchSyscalls } from "../plugos/syscalls/fetch.ts";
|
import { sandboxFetchSyscalls } from "../plugos/syscalls/fetch.ts";
|
||||||
import { shellSyscalls } from "./syscalls/shell.ts";
|
import { shellSyscalls } from "./syscalls/shell.ts";
|
||||||
|
@ -22,7 +21,10 @@ import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
|
||||||
import { base64EncodedDataUrl } from "../plugos/asset_bundle/base64.ts";
|
import { base64EncodedDataUrl } from "../plugos/asset_bundle/base64.ts";
|
||||||
import { Plug } from "../plugos/plug.ts";
|
import { Plug } from "../plugos/plug.ts";
|
||||||
import { DataStore } from "../plugos/lib/datastore.ts";
|
import { DataStore } from "../plugos/lib/datastore.ts";
|
||||||
import { dataStoreSyscalls } from "../plugos/syscalls/datastore.ts";
|
import {
|
||||||
|
dataStoreReadSyscalls,
|
||||||
|
dataStoreWriteSyscalls,
|
||||||
|
} from "../plugos/syscalls/datastore.ts";
|
||||||
import { DataStoreMQ } from "../plugos/lib/mq.datastore.ts";
|
import { DataStoreMQ } from "../plugos/lib/mq.datastore.ts";
|
||||||
import { languageSyscalls } from "../common/syscalls/language.ts";
|
import { languageSyscalls } from "../common/syscalls/language.ts";
|
||||||
import { handlebarsSyscalls } from "../common/syscalls/handlebars.ts";
|
import { handlebarsSyscalls } from "../common/syscalls/handlebars.ts";
|
||||||
|
@ -65,6 +67,7 @@ export class ServerSystem {
|
||||||
private baseSpacePrimitives: SpacePrimitives,
|
private baseSpacePrimitives: SpacePrimitives,
|
||||||
readonly kvPrimitives: KvPrimitives,
|
readonly kvPrimitives: KvPrimitives,
|
||||||
private shellBackend: ShellBackend,
|
private shellBackend: ShellBackend,
|
||||||
|
private readOnlyMode: boolean,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,29 +126,37 @@ export class ServerSystem {
|
||||||
this.system.registerSyscalls(
|
this.system.registerSyscalls(
|
||||||
[],
|
[],
|
||||||
eventSyscalls(eventHook),
|
eventSyscalls(eventHook),
|
||||||
spaceSyscalls(space),
|
spaceReadSyscalls(space),
|
||||||
assetSyscalls(this.system),
|
assetSyscalls(this.system),
|
||||||
yamlSyscalls(),
|
yamlSyscalls(),
|
||||||
systemSyscalls(this.system),
|
systemSyscalls(this.system, this.readOnlyMode),
|
||||||
mqSyscalls(mq),
|
mqSyscalls(mq),
|
||||||
languageSyscalls(),
|
languageSyscalls(),
|
||||||
handlebarsSyscalls(),
|
handlebarsSyscalls(),
|
||||||
dataStoreSyscalls(this.ds),
|
dataStoreReadSyscalls(this.ds),
|
||||||
debugSyscalls(),
|
|
||||||
codeWidgetSyscalls(codeWidgetHook),
|
codeWidgetSyscalls(codeWidgetHook),
|
||||||
markdownSyscalls(),
|
markdownSyscalls(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Syscalls that require some additional permissions
|
if (!this.readOnlyMode) {
|
||||||
this.system.registerSyscalls(
|
// Write mode only
|
||||||
["fetch"],
|
this.system.registerSyscalls(
|
||||||
sandboxFetchSyscalls(),
|
[],
|
||||||
);
|
spaceWriteSyscalls(space),
|
||||||
|
dataStoreWriteSyscalls(this.ds),
|
||||||
|
);
|
||||||
|
|
||||||
this.system.registerSyscalls(
|
// Syscalls that require some additional permissions
|
||||||
["shell"],
|
this.system.registerSyscalls(
|
||||||
shellSyscalls(this.shellBackend),
|
["fetch"],
|
||||||
);
|
sandboxFetchSyscalls(),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.system.registerSyscalls(
|
||||||
|
["shell"],
|
||||||
|
shellSyscalls(this.shellBackend),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await this.loadPlugs();
|
await this.loadPlugs();
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { SpaceServerConfig } from "./instance.ts";
|
||||||
import { ShellRequest, ShellResponse } from "./rpc.ts";
|
import { ShellRequest, ShellResponse } from "./rpc.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -5,11 +6,13 @@ import { ShellRequest, ShellResponse } from "./rpc.ts";
|
||||||
* - SB_SHELL_BACKEND: "local" or "off"
|
* - SB_SHELL_BACKEND: "local" or "off"
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function determineShellBackend(path: string): ShellBackend {
|
export function determineShellBackend(
|
||||||
|
spaceServerConfig: SpaceServerConfig,
|
||||||
|
): ShellBackend {
|
||||||
const backendConfig = Deno.env.get("SB_SHELL_BACKEND") || "local";
|
const backendConfig = Deno.env.get("SB_SHELL_BACKEND") || "local";
|
||||||
switch (backendConfig) {
|
switch (backendConfig) {
|
||||||
case "local":
|
case "local":
|
||||||
return new LocalShell(path);
|
return new LocalShell(spaceServerConfig.pagesPath);
|
||||||
default:
|
default:
|
||||||
console.info(
|
console.info(
|
||||||
"Running in shellless mode, meaning shell commands are disabled",
|
"Running in shellless mode, meaning shell commands are disabled",
|
||||||
|
@ -22,7 +25,7 @@ export interface ShellBackend {
|
||||||
handle(shellRequest: ShellRequest): Promise<ShellResponse>;
|
handle(shellRequest: ShellRequest): Promise<ShellResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotSupportedShell implements ShellBackend {
|
export class NotSupportedShell implements ShellBackend {
|
||||||
handle(): Promise<ShellResponse> {
|
handle(): Promise<ShellResponse> {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
stdout: "",
|
stdout: "",
|
||||||
|
@ -32,7 +35,7 @@ class NotSupportedShell implements ShellBackend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocalShell implements ShellBackend {
|
export class LocalShell implements ShellBackend {
|
||||||
constructor(private cwd: string) {
|
constructor(private cwd: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import type { Space } from "../../web/space.ts";
|
||||||
/**
|
/**
|
||||||
* Almost the same as web/syscalls/space.ts except leaving out client-specific stuff
|
* Almost the same as web/syscalls/space.ts except leaving out client-specific stuff
|
||||||
*/
|
*/
|
||||||
export function spaceSyscalls(space: Space): SysCallMapping {
|
export function spaceReadSyscalls(space: Space): SysCallMapping {
|
||||||
return {
|
return {
|
||||||
"space.listPages": (): Promise<PageMeta[]> => {
|
"space.listPages": (): Promise<PageMeta[]> => {
|
||||||
return space.fetchPageList();
|
return space.fetchPageList();
|
||||||
|
@ -16,16 +16,6 @@ export function spaceSyscalls(space: Space): SysCallMapping {
|
||||||
"space.getPageMeta": (_ctx, name: string): Promise<PageMeta> => {
|
"space.getPageMeta": (_ctx, name: string): Promise<PageMeta> => {
|
||||||
return space.getPageMeta(name);
|
return space.getPageMeta(name);
|
||||||
},
|
},
|
||||||
"space.writePage": (
|
|
||||||
_ctx,
|
|
||||||
name: string,
|
|
||||||
text: string,
|
|
||||||
): Promise<PageMeta> => {
|
|
||||||
return space.writePage(name, text);
|
|
||||||
},
|
|
||||||
"space.deletePage": async (_ctx, name: string) => {
|
|
||||||
await space.deletePage(name);
|
|
||||||
},
|
|
||||||
"space.listPlugs": (): Promise<FileMeta[]> => {
|
"space.listPlugs": (): Promise<FileMeta[]> => {
|
||||||
return space.listPlugs();
|
return space.listPlugs();
|
||||||
},
|
},
|
||||||
|
@ -41,16 +31,6 @@ export function spaceSyscalls(space: Space): SysCallMapping {
|
||||||
): Promise<AttachmentMeta> => {
|
): Promise<AttachmentMeta> => {
|
||||||
return await space.getAttachmentMeta(name);
|
return await space.getAttachmentMeta(name);
|
||||||
},
|
},
|
||||||
"space.writeAttachment": (
|
|
||||||
_ctx,
|
|
||||||
name: string,
|
|
||||||
data: Uint8Array,
|
|
||||||
): Promise<AttachmentMeta> => {
|
|
||||||
return space.writeAttachment(name, data);
|
|
||||||
},
|
|
||||||
"space.deleteAttachment": async (_ctx, name: string) => {
|
|
||||||
await space.deleteAttachment(name);
|
|
||||||
},
|
|
||||||
|
|
||||||
// FS
|
// FS
|
||||||
"space.listFiles": (): Promise<FileMeta[]> => {
|
"space.listFiles": (): Promise<FileMeta[]> => {
|
||||||
|
@ -62,6 +42,31 @@ export function spaceSyscalls(space: Space): SysCallMapping {
|
||||||
"space.readFile": async (_ctx, name: string): Promise<Uint8Array> => {
|
"space.readFile": async (_ctx, name: string): Promise<Uint8Array> => {
|
||||||
return (await space.spacePrimitives.readFile(name)).data;
|
return (await space.spacePrimitives.readFile(name)).data;
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spaceWriteSyscalls(space: Space): SysCallMapping {
|
||||||
|
return {
|
||||||
|
"space.writePage": (
|
||||||
|
_ctx,
|
||||||
|
name: string,
|
||||||
|
text: string,
|
||||||
|
): Promise<PageMeta> => {
|
||||||
|
return space.writePage(name, text);
|
||||||
|
},
|
||||||
|
"space.deletePage": async (_ctx, name: string) => {
|
||||||
|
await space.deletePage(name);
|
||||||
|
},
|
||||||
|
"space.writeAttachment": (
|
||||||
|
_ctx,
|
||||||
|
name: string,
|
||||||
|
data: Uint8Array,
|
||||||
|
): Promise<AttachmentMeta> => {
|
||||||
|
return space.writeAttachment(name, data);
|
||||||
|
},
|
||||||
|
"space.deleteAttachment": async (_ctx, name: string) => {
|
||||||
|
await space.deleteAttachment(name);
|
||||||
|
},
|
||||||
"space.writeFile": (
|
"space.writeFile": (
|
||||||
_ctx,
|
_ctx,
|
||||||
name: string,
|
name: string,
|
||||||
|
|
|
@ -10,9 +10,14 @@ safeRun(async () => {
|
||||||
syncMode ? "in Sync Mode" : "in Online Mode",
|
syncMode ? "in Sync Mode" : "in Online Mode",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (window.silverBulletConfig.readOnly) {
|
||||||
|
console.log("Running in read-only mode");
|
||||||
|
}
|
||||||
|
|
||||||
const client = new Client(
|
const client = new Client(
|
||||||
document.getElementById("sb-root")!,
|
document.getElementById("sb-root")!,
|
||||||
syncMode,
|
syncMode,
|
||||||
|
window.silverBulletConfig.readOnly,
|
||||||
);
|
);
|
||||||
window.client = client;
|
window.client = client;
|
||||||
await client.init();
|
await client.init();
|
||||||
|
|
|
@ -59,6 +59,8 @@ import { LimitedMap } from "$sb/lib/limited_map.ts";
|
||||||
import { renderHandlebarsTemplate } from "../common/syscalls/handlebars.ts";
|
import { renderHandlebarsTemplate } from "../common/syscalls/handlebars.ts";
|
||||||
import { buildQueryFunctions } from "../common/query_functions.ts";
|
import { buildQueryFunctions } from "../common/query_functions.ts";
|
||||||
import { PageRef } from "$sb/lib/page.ts";
|
import { PageRef } from "$sb/lib/page.ts";
|
||||||
|
import { ReadOnlySpacePrimitives } from "../common/spaces/ro_space_primitives.ts";
|
||||||
|
import { KvPrimitives } from "../plugos/lib/kv_primitives.ts";
|
||||||
const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/;
|
const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/;
|
||||||
|
|
||||||
const autoSaveInterval = 1000;
|
const autoSaveInterval = 1000;
|
||||||
|
@ -69,6 +71,7 @@ declare global {
|
||||||
silverBulletConfig: {
|
silverBulletConfig: {
|
||||||
spaceFolderPath: string;
|
spaceFolderPath: string;
|
||||||
syncOnly: boolean;
|
syncOnly: boolean;
|
||||||
|
readOnly: boolean;
|
||||||
clientEncryption: boolean;
|
clientEncryption: boolean;
|
||||||
};
|
};
|
||||||
client: Client;
|
client: Client;
|
||||||
|
@ -87,7 +90,6 @@ export class Client {
|
||||||
private dbPrefix: string;
|
private dbPrefix: string;
|
||||||
|
|
||||||
plugSpaceRemotePrimitives!: PlugSpacePrimitives;
|
plugSpaceRemotePrimitives!: PlugSpacePrimitives;
|
||||||
// localSpacePrimitives!: FilteredSpacePrimitives;
|
|
||||||
httpSpacePrimitives!: HttpSpacePrimitives;
|
httpSpacePrimitives!: HttpSpacePrimitives;
|
||||||
space!: Space;
|
space!: Space;
|
||||||
|
|
||||||
|
@ -109,7 +111,7 @@ export class Client {
|
||||||
|
|
||||||
ui!: MainUI;
|
ui!: MainUI;
|
||||||
stateDataStore!: DataStore;
|
stateDataStore!: DataStore;
|
||||||
spaceDataStore!: DataStore;
|
spaceKV?: KvPrimitives;
|
||||||
mq!: DataStoreMQ;
|
mq!: DataStoreMQ;
|
||||||
|
|
||||||
// Used by the "wiki link" highlighter to check if a page exists
|
// Used by the "wiki link" highlighter to check if a page exists
|
||||||
|
@ -118,7 +120,8 @@ export class Client {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private parent: Element,
|
private parent: Element,
|
||||||
public syncMode = false,
|
public syncMode: boolean,
|
||||||
|
private readOnlyMode: boolean,
|
||||||
) {
|
) {
|
||||||
if (!syncMode) {
|
if (!syncMode) {
|
||||||
this.fullSyncCompleted = true;
|
this.fullSyncCompleted = true;
|
||||||
|
@ -159,6 +162,7 @@ export class Client {
|
||||||
this.mq,
|
this.mq,
|
||||||
this.stateDataStore,
|
this.stateDataStore,
|
||||||
this.eventHook,
|
this.eventHook,
|
||||||
|
window.silverBulletConfig.readOnly,
|
||||||
);
|
);
|
||||||
|
|
||||||
const localSpacePrimitives = await this.initSpace();
|
const localSpacePrimitives = await this.initSpace();
|
||||||
|
@ -518,6 +522,12 @@ export class Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.readOnlyMode) {
|
||||||
|
remoteSpacePrimitives = new ReadOnlySpacePrimitives(
|
||||||
|
remoteSpacePrimitives,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.plugSpaceRemotePrimitives = new PlugSpacePrimitives(
|
this.plugSpaceRemotePrimitives = new PlugSpacePrimitives(
|
||||||
remoteSpacePrimitives,
|
remoteSpacePrimitives,
|
||||||
this.system.namespaceHook,
|
this.system.namespaceHook,
|
||||||
|
@ -535,6 +545,8 @@ export class Client {
|
||||||
);
|
);
|
||||||
await spaceKvPrimitives.init();
|
await spaceKvPrimitives.init();
|
||||||
|
|
||||||
|
this.spaceKV = spaceKvPrimitives;
|
||||||
|
|
||||||
localSpacePrimitives = new FilteredSpacePrimitives(
|
localSpacePrimitives = new FilteredSpacePrimitives(
|
||||||
new EventedSpacePrimitives(
|
new EventedSpacePrimitives(
|
||||||
// Using fallback space primitives here to allow (by default) local reads to "fall through" to HTTP when files aren't synced yet
|
// Using fallback space primitives here to allow (by default) local reads to "fall through" to HTTP when files aren't synced yet
|
||||||
|
|
|
@ -17,16 +17,19 @@ import { editorSyscalls } from "./syscalls/editor.ts";
|
||||||
import { sandboxFetchSyscalls } from "./syscalls/fetch.ts";
|
import { sandboxFetchSyscalls } from "./syscalls/fetch.ts";
|
||||||
import { markdownSyscalls } from "../common/syscalls/markdown.ts";
|
import { markdownSyscalls } from "../common/syscalls/markdown.ts";
|
||||||
import { shellSyscalls } from "./syscalls/shell.ts";
|
import { shellSyscalls } from "./syscalls/shell.ts";
|
||||||
import { spaceSyscalls } from "./syscalls/space.ts";
|
import { spaceReadSyscalls, spaceWriteSyscalls } from "./syscalls/space.ts";
|
||||||
import { syncSyscalls } from "./syscalls/sync.ts";
|
import { syncSyscalls } from "./syscalls/sync.ts";
|
||||||
import { systemSyscalls } from "./syscalls/system.ts";
|
import { systemSyscalls } from "../common/syscalls/system.ts";
|
||||||
import { yamlSyscalls } from "../common/syscalls/yaml.ts";
|
import { yamlSyscalls } from "../common/syscalls/yaml.ts";
|
||||||
import { Space } from "./space.ts";
|
import { Space } from "./space.ts";
|
||||||
import { MQHook } from "../plugos/hooks/mq.ts";
|
import { MQHook } from "../plugos/hooks/mq.ts";
|
||||||
import { mqSyscalls } from "../plugos/syscalls/mq.ts";
|
import { mqSyscalls } from "../plugos/syscalls/mq.ts";
|
||||||
import { mqProxySyscalls } from "./syscalls/mq.proxy.ts";
|
import { mqProxySyscalls } from "./syscalls/mq.proxy.ts";
|
||||||
import { dataStoreProxySyscalls } from "./syscalls/datastore.proxy.ts";
|
import { dataStoreProxySyscalls } from "./syscalls/datastore.proxy.ts";
|
||||||
import { dataStoreSyscalls } from "../plugos/syscalls/datastore.ts";
|
import {
|
||||||
|
dataStoreReadSyscalls,
|
||||||
|
dataStoreWriteSyscalls,
|
||||||
|
} from "../plugos/syscalls/datastore.ts";
|
||||||
import { DataStore } from "../plugos/lib/datastore.ts";
|
import { DataStore } from "../plugos/lib/datastore.ts";
|
||||||
import { MessageQueue } from "../plugos/lib/mq.ts";
|
import { MessageQueue } from "../plugos/lib/mq.ts";
|
||||||
import { languageSyscalls } from "../common/syscalls/language.ts";
|
import { languageSyscalls } from "../common/syscalls/language.ts";
|
||||||
|
@ -54,6 +57,7 @@ export class ClientSystem {
|
||||||
private mq: MessageQueue,
|
private mq: MessageQueue,
|
||||||
private ds: DataStore,
|
private ds: DataStore,
|
||||||
private eventHook: EventHook,
|
private eventHook: EventHook,
|
||||||
|
private readOnlyMode: boolean,
|
||||||
) {
|
) {
|
||||||
// Only set environment to "client" when running in thin client mode, otherwise we run everything locally (hybrid)
|
// Only set environment to "client" when running in thin client mode, otherwise we run everything locally (hybrid)
|
||||||
this.system = new System(
|
this.system = new System(
|
||||||
|
@ -153,8 +157,8 @@ export class ClientSystem {
|
||||||
[],
|
[],
|
||||||
eventSyscalls(this.eventHook),
|
eventSyscalls(this.eventHook),
|
||||||
editorSyscalls(this.client),
|
editorSyscalls(this.client),
|
||||||
spaceSyscalls(this.client),
|
spaceReadSyscalls(this.client),
|
||||||
systemSyscalls(this.system, this.client),
|
systemSyscalls(this.system, false, this.client),
|
||||||
markdownSyscalls(),
|
markdownSyscalls(),
|
||||||
assetSyscalls(this.system),
|
assetSyscalls(this.system),
|
||||||
yamlSyscalls(),
|
yamlSyscalls(),
|
||||||
|
@ -167,24 +171,31 @@ export class ClientSystem {
|
||||||
? mqSyscalls(this.mq)
|
? mqSyscalls(this.mq)
|
||||||
// In non-sync mode proxy to server
|
// In non-sync mode proxy to server
|
||||||
: mqProxySyscalls(this.client),
|
: mqProxySyscalls(this.client),
|
||||||
this.client.syncMode
|
...this.client.syncMode
|
||||||
? dataStoreSyscalls(this.ds)
|
? [dataStoreReadSyscalls(this.ds), dataStoreWriteSyscalls(this.ds)]
|
||||||
: dataStoreProxySyscalls(this.client),
|
: [dataStoreProxySyscalls(this.client)],
|
||||||
debugSyscalls(),
|
debugSyscalls(this.client),
|
||||||
syncSyscalls(this.client),
|
syncSyscalls(this.client),
|
||||||
clientStoreSyscalls(this.ds),
|
clientStoreSyscalls(this.ds),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Syscalls that require some additional permissions
|
if (!this.readOnlyMode) {
|
||||||
this.system.registerSyscalls(
|
// Write syscalls
|
||||||
["fetch"],
|
this.system.registerSyscalls(
|
||||||
sandboxFetchSyscalls(this.client),
|
[],
|
||||||
);
|
spaceWriteSyscalls(this.client),
|
||||||
|
);
|
||||||
|
// Syscalls that require some additional permissions
|
||||||
|
this.system.registerSyscalls(
|
||||||
|
["fetch"],
|
||||||
|
sandboxFetchSyscalls(this.client),
|
||||||
|
);
|
||||||
|
|
||||||
this.system.registerSyscalls(
|
this.system.registerSyscalls(
|
||||||
["shell"],
|
["shell"],
|
||||||
shellSyscalls(this.client),
|
shellSyscalls(this.client),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async reloadPlugsFromSpace(space: Space) {
|
async reloadPlugsFromSpace(space: Space) {
|
||||||
|
|
|
@ -34,7 +34,9 @@ export class MainUI {
|
||||||
console.log("Closing search panel");
|
console.log("Closing search panel");
|
||||||
closeSearchPanel(client.editorView);
|
closeSearchPanel(client.editorView);
|
||||||
return;
|
return;
|
||||||
} else if (target.closest(".cm-content")) {
|
} else if (
|
||||||
|
target.className === "cm-textfield" || target.closest(".cm-content")
|
||||||
|
) {
|
||||||
// In some cm element, let's back out
|
// In some cm element, let's back out
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ export type CommandDef = {
|
||||||
mac?: string;
|
mac?: string;
|
||||||
|
|
||||||
hide?: boolean;
|
hide?: boolean;
|
||||||
|
requireMode?: "rw" | "ro";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AppCommand = {
|
export type AppCommand = {
|
||||||
|
@ -58,6 +59,10 @@ export class CommandHook extends EventEmitter<CommandHookEvents>
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const cmd = functionDef.command;
|
const cmd = functionDef.command;
|
||||||
|
if (cmd.requireMode === "rw" && window.silverBulletConfig.readOnly) {
|
||||||
|
// Bit hacky, but don't expose commands that require write mode in read-only mode
|
||||||
|
continue;
|
||||||
|
}
|
||||||
this.editorCommands.set(cmd.name, {
|
this.editorCommands.set(cmd.name, {
|
||||||
command: cmd,
|
command: cmd,
|
||||||
run: (args?: string[]) => {
|
run: (args?: string[]) => {
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
// These {{VARIABLES}} are replaced by http_server.ts
|
// These {{VARIABLES}} are replaced by http_server.ts
|
||||||
spaceFolderPath: "{{SPACE_PATH}}",
|
spaceFolderPath: "{{SPACE_PATH}}",
|
||||||
syncOnly: "{{SYNC_ONLY}}" === "true",
|
syncOnly: "{{SYNC_ONLY}}" === "true",
|
||||||
|
readOnly: "{{READ_ONLY}}" === "true",
|
||||||
clientEncryption: "{{CLIENT_ENCRYPTION}}" === "true",
|
clientEncryption: "{{CLIENT_ENCRYPTION}}" === "true",
|
||||||
};
|
};
|
||||||
// But in case these variables aren't replaced by the server, fall back sync only mode
|
// But in case these variables aren't replaced by the server, fall back sync only mode
|
||||||
|
@ -46,6 +47,7 @@
|
||||||
window.silverBulletConfig = {
|
window.silverBulletConfig = {
|
||||||
spaceFolderPath: "",
|
spaceFolderPath: "",
|
||||||
syncOnly: true,
|
syncOnly: true,
|
||||||
|
readOnly: false,
|
||||||
clientEncryption: false,
|
clientEncryption: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
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([
|
const precacheFiles = Object.fromEntries([
|
||||||
"/",
|
"/",
|
||||||
"/.client/logout.html",
|
|
||||||
"/.client/client.js",
|
"/.client/client.js",
|
||||||
"/.client/favicon.png",
|
"/.client/favicon.png",
|
||||||
"/.client/iAWriterMonoS-Bold.woff2",
|
"/.client/iAWriterMonoS-Bold.woff2",
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { KvQuery } from "$sb/types.ts";
|
|
||||||
import { LimitedMap } from "../../plug-api/lib/limited_map.ts";
|
|
||||||
import type { SysCallMapping } from "../../plugos/system.ts";
|
import type { SysCallMapping } from "../../plugos/system.ts";
|
||||||
import type { Client } from "../client.ts";
|
import type { Client } from "../client.ts";
|
||||||
import { proxySyscall, proxySyscalls } from "./util.ts";
|
import { proxySyscalls } from "./util.ts";
|
||||||
|
|
||||||
export function dataStoreProxySyscalls(client: Client): SysCallMapping {
|
export function dataStoreProxySyscalls(client: Client): SysCallMapping {
|
||||||
return proxySyscalls(client, [
|
return proxySyscalls(client, [
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
import { KvKey } from "$sb/types.ts";
|
||||||
import type { SysCallMapping } from "../../plugos/system.ts";
|
import type { SysCallMapping } from "../../plugos/system.ts";
|
||||||
|
import { Client } from "../client.ts";
|
||||||
|
|
||||||
export function debugSyscalls(): SysCallMapping {
|
export function debugSyscalls(client: Client): SysCallMapping {
|
||||||
return {
|
return {
|
||||||
"debug.resetClient": async () => {
|
"debug.resetClient": async () => {
|
||||||
if (navigator.serviceWorker) {
|
if (navigator.serviceWorker) {
|
||||||
|
@ -44,5 +46,20 @@ export function debugSyscalls(): SysCallMapping {
|
||||||
alert("Reset complete, now reloading the page...");
|
alert("Reset complete, now reloading the page...");
|
||||||
location.reload();
|
location.reload();
|
||||||
},
|
},
|
||||||
|
"debug.cleanup": async () => {
|
||||||
|
if (client.spaceKV) {
|
||||||
|
console.log("Wiping the entire space KV store");
|
||||||
|
// In sync mode, we can just delete the whole space
|
||||||
|
const allKeys: KvKey[] = [];
|
||||||
|
for await (const { key } of client.spaceKV.query({})) {
|
||||||
|
allKeys.push(key);
|
||||||
|
}
|
||||||
|
await client.spaceKV.batchDelete(allKeys);
|
||||||
|
}
|
||||||
|
localStorage.clear();
|
||||||
|
console.log("Wiping the entire state KV store");
|
||||||
|
await client.stateDataStore.queryDelete({});
|
||||||
|
console.log("Done");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Client } from "../client.ts";
|
||||||
import { SysCallMapping } from "../../plugos/system.ts";
|
import { SysCallMapping } from "../../plugos/system.ts";
|
||||||
import { AttachmentMeta, FileMeta, PageMeta } from "$sb/types.ts";
|
import { AttachmentMeta, FileMeta, PageMeta } from "$sb/types.ts";
|
||||||
|
|
||||||
export function spaceSyscalls(editor: Client): SysCallMapping {
|
export function spaceReadSyscalls(editor: Client): SysCallMapping {
|
||||||
return {
|
return {
|
||||||
"space.listPages": (): Promise<PageMeta[]> => {
|
"space.listPages": (): Promise<PageMeta[]> => {
|
||||||
return editor.space.fetchPageList();
|
return editor.space.fetchPageList();
|
||||||
|
@ -13,6 +13,36 @@ export function spaceSyscalls(editor: Client): SysCallMapping {
|
||||||
"space.getPageMeta": (_ctx, name: string): Promise<PageMeta> => {
|
"space.getPageMeta": (_ctx, name: string): Promise<PageMeta> => {
|
||||||
return editor.space.getPageMeta(name);
|
return editor.space.getPageMeta(name);
|
||||||
},
|
},
|
||||||
|
"space.listPlugs": (): Promise<FileMeta[]> => {
|
||||||
|
return editor.space.listPlugs();
|
||||||
|
},
|
||||||
|
"space.listAttachments": async (): Promise<AttachmentMeta[]> => {
|
||||||
|
return await editor.space.fetchAttachmentList();
|
||||||
|
},
|
||||||
|
"space.readAttachment": async (_ctx, name: string): Promise<Uint8Array> => {
|
||||||
|
return (await editor.space.readAttachment(name)).data;
|
||||||
|
},
|
||||||
|
"space.getAttachmentMeta": async (
|
||||||
|
_ctx,
|
||||||
|
name: string,
|
||||||
|
): Promise<AttachmentMeta> => {
|
||||||
|
return await editor.space.getAttachmentMeta(name);
|
||||||
|
},
|
||||||
|
// FS
|
||||||
|
"space.listFiles": (): Promise<FileMeta[]> => {
|
||||||
|
return editor.space.spacePrimitives.fetchFileList();
|
||||||
|
},
|
||||||
|
"space.getFileMeta": (_ctx, name: string): Promise<FileMeta> => {
|
||||||
|
return editor.space.spacePrimitives.getFileMeta(name);
|
||||||
|
},
|
||||||
|
"space.readFile": async (_ctx, name: string): Promise<Uint8Array> => {
|
||||||
|
return (await editor.space.spacePrimitives.readFile(name)).data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spaceWriteSyscalls(editor: Client): SysCallMapping {
|
||||||
|
return {
|
||||||
"space.writePage": (
|
"space.writePage": (
|
||||||
_ctx,
|
_ctx,
|
||||||
name: string,
|
name: string,
|
||||||
|
@ -30,21 +60,6 @@ export function spaceSyscalls(editor: Client): SysCallMapping {
|
||||||
console.log("Deleting page");
|
console.log("Deleting page");
|
||||||
await editor.space.deletePage(name);
|
await editor.space.deletePage(name);
|
||||||
},
|
},
|
||||||
"space.listPlugs": (): Promise<FileMeta[]> => {
|
|
||||||
return editor.space.listPlugs();
|
|
||||||
},
|
|
||||||
"space.listAttachments": async (): Promise<AttachmentMeta[]> => {
|
|
||||||
return await editor.space.fetchAttachmentList();
|
|
||||||
},
|
|
||||||
"space.readAttachment": async (_ctx, name: string): Promise<Uint8Array> => {
|
|
||||||
return (await editor.space.readAttachment(name)).data;
|
|
||||||
},
|
|
||||||
"space.getAttachmentMeta": async (
|
|
||||||
_ctx,
|
|
||||||
name: string,
|
|
||||||
): Promise<AttachmentMeta> => {
|
|
||||||
return await editor.space.getAttachmentMeta(name);
|
|
||||||
},
|
|
||||||
"space.writeAttachment": (
|
"space.writeAttachment": (
|
||||||
_ctx,
|
_ctx,
|
||||||
name: string,
|
name: string,
|
||||||
|
@ -55,17 +70,6 @@ export function spaceSyscalls(editor: Client): SysCallMapping {
|
||||||
"space.deleteAttachment": async (_ctx, name: string) => {
|
"space.deleteAttachment": async (_ctx, name: string) => {
|
||||||
await editor.space.deleteAttachment(name);
|
await editor.space.deleteAttachment(name);
|
||||||
},
|
},
|
||||||
|
|
||||||
// FS
|
|
||||||
"space.listFiles": (): Promise<FileMeta[]> => {
|
|
||||||
return editor.space.spacePrimitives.fetchFileList();
|
|
||||||
},
|
|
||||||
"space.getFileMeta": (_ctx, name: string): Promise<FileMeta> => {
|
|
||||||
return editor.space.spacePrimitives.getFileMeta(name);
|
|
||||||
},
|
|
||||||
"space.readFile": async (_ctx, name: string): Promise<Uint8Array> => {
|
|
||||||
return (await editor.space.spacePrimitives.readFile(name)).data;
|
|
||||||
},
|
|
||||||
"space.writeFile": (
|
"space.writeFile": (
|
||||||
_ctx,
|
_ctx,
|
||||||
name: string,
|
name: string,
|
||||||
|
|
|
@ -12,6 +12,8 @@ _The changes below are not yet released “properly”. To them out early, check
|
||||||
* Action buttons (top right buttons) can now be configured, see [[SETTINGS]] how to do this.
|
* Action buttons (top right buttons) can now be configured, see [[SETTINGS]] how to do this.
|
||||||
* Headers are now indexed, meaning you can query them [[Objects#header]] and also reference them by name via page links using `#` that I just demonstrated 👈. See [[Links]] for more information on all the type of link formats that SilverBullet now supports.
|
* Headers are now indexed, meaning you can query them [[Objects#header]] and also reference them by name via page links using `#` that I just demonstrated 👈. See [[Links]] for more information on all the type of link formats that SilverBullet now supports.
|
||||||
* New {[Task: Remove Completed]} command to remove all completed tasks from a page
|
* New {[Task: Remove Completed]} command to remove all completed tasks from a page
|
||||||
|
* **Read-only mode** (experimental) is here, see [[Install/Configuration#Run mode]] on how to enable it. Allowing you expose your space to the outside world in all its glory, but without allowing anybody to edit anything. This should be fairly locked down and secure, but back up your stuff!
|
||||||
|
* New {[Clear Local Storage & Logout]} command to wipe out any locally synced data (and log you out if you use [[Authentication]]).
|
||||||
* Bug fixes:
|
* Bug fixes:
|
||||||
* Improved Ctrl/Cmd-click (to open links in a new window) behavior: now actually follow `@pos` and `$anchor` links.
|
* Improved Ctrl/Cmd-click (to open links in a new window) behavior: now actually follow `@pos` and `$anchor` links.
|
||||||
* Right-clicking links now opens browser native context menu again
|
* Right-clicking links now opens browser native context menu again
|
||||||
|
|
|
@ -1,21 +1,18 @@
|
||||||
SilverBullet is primarily configured via environment variables. This page gives a comprehensive overview of all configuration options. You can set these ad-hoc when running the SilverBullet server, or e.g. in your [[Install/Docker|docker-compose file]].
|
SilverBullet is primarily configured via environment variables. This page gives a comprehensive overview of all configuration options. You can set these ad-hoc when running the SilverBullet server, or e.g. in your [[Install/Docker|docker-compose file]].
|
||||||
|
|
||||||
# Network
|
# Network
|
||||||
$network
|
|
||||||
Note: these options are primarily useful for [[Install/Deno]] deployments, not so much for [[Install/Docker]].
|
Note: these options are primarily useful for [[Install/Deno]] deployments, not so much for [[Install/Docker]].
|
||||||
|
|
||||||
* `SB_HOSTNAME`: Set to the hostname to bind to (defaults to `127.0.0.0`, set to `0.0.0.0` to accept outside connections for the local deno setup, defaults to `0.0.0.0` for docker)
|
* `SB_HOSTNAME`: Set to the hostname to bind to (defaults to `127.0.0.0`, set to `0.0.0.0` to accept outside connections for the local deno setup, defaults to `0.0.0.0` for docker)
|
||||||
* `SB_PORT`: Sets the port to listen to, e.g. `SB_PORT=1234`, default is `3000`
|
* `SB_PORT`: Sets the port to listen to, e.g. `SB_PORT=1234`, default is `3000`
|
||||||
|
|
||||||
# Authentication
|
# Authentication
|
||||||
$authentication
|
|
||||||
SilverBullet supports basic authentication for a single user.
|
SilverBullet supports basic authentication for a single user.
|
||||||
|
|
||||||
* `SB_USER`: Sets single-user credentials, e.g. `SB_USER=pete:1234` allows you to login with username “pete” and password “1234”.
|
* `SB_USER`: Sets single-user credentials, e.g. `SB_USER=pete:1234` allows you to login with username “pete” and password “1234”.
|
||||||
* `SB_AUTH_TOKEN`: Enables `Authorization: Bearer <token>` style authentication on the [[API]] (useful for [[Sync]] and remote HTTP storage backends).
|
* `SB_AUTH_TOKEN`: Enables `Authorization: Bearer <token>` style authentication on the [[API]] (useful for [[Sync]] and remote HTTP storage backends).
|
||||||
|
|
||||||
# Storage
|
# Storage
|
||||||
$storage
|
|
||||||
SilverBullet supports multiple storage backends for keeping your [[Spaces]] content.
|
SilverBullet supports multiple storage backends for keeping your [[Spaces]] content.
|
||||||
|
|
||||||
## Disk storage
|
## Disk storage
|
||||||
|
@ -26,7 +23,8 @@ This is the default and simplest backend to use: a folder on disk. It is configu
|
||||||
## AWS S3 bucket storage
|
## AWS S3 bucket storage
|
||||||
It is also possible to use an S3 bucket as storage. For this, you need to create a bucket, create an IAM user and configure access to it appropriately.
|
It is also possible to use an S3 bucket as storage. For this, you need to create a bucket, create an IAM user and configure access to it appropriately.
|
||||||
|
|
||||||
Since S3 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:
|
S3 is configured as follows:
|
||||||
|
|
||||||
|
@ -38,13 +36,13 @@ S3 is configured as follows:
|
||||||
* `AWS_REGION`: e.g. `eu-central-1`
|
* `AWS_REGION`: e.g. `eu-central-1`
|
||||||
|
|
||||||
## Database storage
|
## Database storage
|
||||||
It is also possible to store space content in the [[$database]]. While not necessarily recommended, it is a viable way to set up a simple deployment of SilverBullet on e.g. [[Install/Deno Deploy]]. Large files will automatically be chunked to avoid limits the used database may have on value size.
|
It is also possible to store space content in the [[#Database]]. While not necessarily recommended, it is a viable way to set up a simple deployment of SilverBullet on e.g. [[Install/Deno Deploy]]. Large files will automatically be chunked to avoid limits the used database may have on value size.
|
||||||
|
|
||||||
This mode is configured as follows:
|
This mode is configured as follows:
|
||||||
|
|
||||||
* `SB_FOLDER`: set to `db://`
|
* `SB_FOLDER`: set to `db://`
|
||||||
|
|
||||||
The database configured via [[$database]] will be used.
|
The database configured via [[#Database]] will be used.
|
||||||
|
|
||||||
## HTTP storage
|
## HTTP storage
|
||||||
While not particularly useful stand-alone (primarily for [[Sync]]), it is possible to store space content on _another_ SilverBullet installation via its [[API]].
|
While not particularly useful stand-alone (primarily for [[Sync]]), it is possible to store space content on _another_ SilverBullet installation via its [[API]].
|
||||||
|
@ -52,10 +50,9 @@ While not particularly useful stand-alone (primarily for [[Sync]]), it is possib
|
||||||
This mode is configured as follows:
|
This mode is configured as follows:
|
||||||
|
|
||||||
* `SB_FOLDER`: set to the URL of the other SilverBullet server, e.g. `https://mynotes.mydomain.com`
|
* `SB_FOLDER`: set to the URL of the other SilverBullet server, e.g. `https://mynotes.mydomain.com`
|
||||||
* `SB_AUTH_TOKEN`: matching the authorization token (configured via [[$authentication]] on the other end) to use for authorization.
|
* `SB_AUTH_TOKEN`: matching the authorization token (configured via [[#Authentication]] on the other end) to use for authorization.
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
$database
|
|
||||||
SilverBullet requires a database backend to (potentially) keep various types of data:
|
SilverBullet requires a database backend to (potentially) keep various types of data:
|
||||||
|
|
||||||
* Indexes for e.g. [[Objects]]
|
* Indexes for e.g. [[Objects]]
|
||||||
|
@ -80,16 +77,13 @@ The in-memory database is only useful for testing.
|
||||||
* `SB_DB_BACKEND`: `memory`
|
* `SB_DB_BACKEND`: `memory`
|
||||||
|
|
||||||
# Run mode
|
# Run mode
|
||||||
$runmode
|
|
||||||
|
|
||||||
* `SB_SYNC_ONLY`: If you want to run SilverBullet in a mode where the server purely functions as a simple file store and 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_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
|
||||||
$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:
|
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
|
# 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
|
# Use cases
|
||||||
* **Migration**: you hosted SilverBullet on your local device until now, but have since set up an instance via [[Install/Deno Deploy]] and want to migrate your content there.
|
* **Migration**: you hosted SilverBullet on your local device until now, but have since set up an instance via [[Install/Deno Deploy]] and want to migrate your content there.
|
||||||
* **Backup**: you host SilverBullet on a remote server, but would like to make backups elsewhere from time to time.
|
* **Backup**: you host SilverBullet on a remote server, but would like to make backups elsewhere from time to time.
|
||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
To use `silverbullet sync` you need a [[Install/Local$deno|local deno installation of SilverBullet]].
|
To use `silverbullet sync` you need a [[Install/Deno|local deno installation of SilverBullet]].
|
||||||
|
|
||||||
# General use
|
# General use
|
||||||
To perform a sync between two locations:
|
To perform a sync between two locations:
|
||||||
|
@ -14,7 +14,7 @@ To perform a sync between two locations:
|
||||||
silverbullet sync --snapshot snapshot.json <primaryPath> <secondaryPath>
|
silverbullet sync --snapshot snapshot.json <primaryPath> <secondaryPath>
|
||||||
```
|
```
|
||||||
|
|
||||||
Where both `primaryPath` and `secondaryPath` can use any [[Install/Configuration$storage]] configuration.
|
Where both `primaryPath` and `secondaryPath` can use any [[Install/Configuration#Storage]] configuration.
|
||||||
|
|
||||||
The `--snapshot` argument is optional; when set, it will read/write a snapshot to the given location. This snapshot will be used to speed up future synchronizations.
|
The `--snapshot` argument is optional; when set, it will read/write a snapshot to the given location. This snapshot will be used to speed up future synchronizations.
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ silverbullet sync --snapshot snapshot.json testspace testspace2
|
||||||
```
|
```
|
||||||
|
|
||||||
# Migrate
|
# Migrate
|
||||||
To synchronize a local folder (the current directory `.`) to a remote server (located at `https://notes.myserver.com`) for which you have setup an [[Install/Configuration$authentication|auth token]] using the `SB_AUTH_TOKEN` environment variable of `1234`:
|
To synchronize a local folder (the current directory `.`) to a remote server (located at `https://notes.myserver.com`) for which you have setup an [[Install/Configuration#Authentication|auth token]] using the `SB_AUTH_TOKEN` environment variable of `1234`:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
SB_AUTH_TOKEN=1234 silverbullet sync . https://notes.myserver.com
|
SB_AUTH_TOKEN=1234 silverbullet sync . https://notes.myserver.com
|
||||||
|
|
Loading…
Reference in New Issue