1
0
silverbullet/packages/server/api_server.ts

322 lines
9.7 KiB
TypeScript
Raw Normal View History

import express, { Express } from "express";
2022-04-26 17:04:36 +00:00
import { Manifest, SilverBulletHooks } from "@silverbulletmd/common/manifest";
2022-04-25 08:33:38 +00:00
import { EndpointHook } from "@plugos/plugos/hooks/endpoint";
import { readFile } from "fs/promises";
2022-04-25 08:33:38 +00:00
import { System } from "@plugos/plugos/system";
import cors from "cors";
import { DiskSpacePrimitives } from "@silverbulletmd/common/spaces/disk_space_primitives";
import path from "path";
import bodyParser from "body-parser";
2022-04-25 08:33:38 +00:00
import { EventHook } from "@plugos/plugos/hooks/event";
import spaceSyscalls from "./syscalls/space";
2022-04-25 08:33:38 +00:00
import { eventSyscalls } from "@plugos/plugos/syscalls/event";
2022-04-21 13:16:42 +00:00
import { ensurePageIndexTable, pageIndexSyscalls } from "./syscalls";
import knex, { Knex } from "knex";
2022-04-25 08:33:38 +00:00
import shellSyscalls from "@plugos/plugos/syscalls/shell.node";
import { NodeCronHook } from "@plugos/plugos/hooks/node_cron";
import { markdownSyscalls } from "@silverbulletmd/common/syscalls/markdown";
import { EventedSpacePrimitives } from "@silverbulletmd/common/spaces/evented_space_primitives";
import { Space } from "@silverbulletmd/common/spaces/space";
import { safeRun, throttle } from "@silverbulletmd/common/util";
2022-04-25 08:33:38 +00:00
import { createSandbox } from "@plugos/plugos/environments/node_sandbox";
import { jwtSyscalls } from "@plugos/plugos/syscalls/jwt";
import buildMarkdown from "@silverbulletmd/web/parser";
import { loadMarkdownExtensions } from "@silverbulletmd/web/markdown_ext";
2022-04-24 16:06:34 +00:00
import http, { Server } from "http";
2022-04-25 17:46:08 +00:00
import { esbuildSyscalls } from "@plugos/plugos/syscalls/esbuild";
2022-04-26 17:04:36 +00:00
import { systemSyscalls } from "./syscalls/system";
2022-03-21 14:21:34 +00:00
export class ExpressServer {
app: Express;
system: System<SilverBulletHooks>;
private space: Space;
private distDir: string;
private eventHook: EventHook;
private db: Knex<any, unknown[]>;
2022-04-24 16:06:34 +00:00
private port: number;
private server?: Server;
2022-04-26 17:04:36 +00:00
builtinPlugDir: string;
preloadedModules: string[];
2022-03-21 14:21:34 +00:00
2022-04-25 09:24:13 +00:00
constructor(
port: number,
2022-04-26 17:04:36 +00:00
pagesPath: string,
2022-04-25 09:24:13 +00:00
distDir: string,
2022-04-26 17:04:36 +00:00
builtinPlugDir: string,
2022-04-25 09:24:13 +00:00
preloadedModules: string[]
) {
2022-04-24 16:06:34 +00:00
this.port = port;
this.app = express();
2022-04-26 17:04:36 +00:00
this.builtinPlugDir = builtinPlugDir;
this.distDir = distDir;
2022-04-24 16:06:34 +00:00
this.system = new System<SilverBulletHooks>("server");
2022-04-26 17:04:36 +00:00
this.preloadedModules = preloadedModules;
2022-03-21 14:21:34 +00:00
// Setup system
this.eventHook = new EventHook();
2022-04-24 16:06:34 +00:00
this.system.addHook(this.eventHook);
this.space = new Space(
new EventedSpacePrimitives(
2022-04-26 17:04:36 +00:00
new DiskSpacePrimitives(pagesPath),
this.eventHook
),
true
);
this.db = knex({
client: "better-sqlite3",
connection: {
2022-04-26 17:04:36 +00:00
filename: path.join(pagesPath, "data.db"),
},
useNullAsDefault: true,
});
2022-03-31 15:25:34 +00:00
2022-04-26 17:04:36 +00:00
this.system.registerSyscalls(["shell"], shellSyscalls(pagesPath));
2022-04-24 16:06:34 +00:00
this.system.addHook(new NodeCronHook());
2022-03-31 15:25:34 +00:00
2022-04-24 16:06:34 +00:00
this.system.registerSyscalls([], pageIndexSyscalls(this.db));
this.system.registerSyscalls([], spaceSyscalls(this.space));
this.system.registerSyscalls([], eventSyscalls(this.eventHook));
this.system.registerSyscalls([], markdownSyscalls(buildMarkdown([])));
2022-04-25 17:46:08 +00:00
this.system.registerSyscalls([], esbuildSyscalls());
2022-04-26 17:04:36 +00:00
this.system.registerSyscalls([], systemSyscalls(this));
2022-04-24 16:06:34 +00:00
this.system.registerSyscalls([], jwtSyscalls());
this.system.addHook(new EndpointHook(this.app, "/_/"));
2022-04-11 18:34:09 +00:00
let throttledRebuildMdExtensions = throttle(() => {
this.rebuildMdExtensions();
}, 100);
2022-04-26 17:04:36 +00:00
this.eventHook.addLocalListener(
"get-plug:builtin",
async (plugName: string): Promise<Manifest> => {
// console.log("Ok, resovling a plugin", plugName);
try {
let manifestJson = await readFile(
path.join(this.builtinPlugDir, `${plugName}.plug.json`),
"utf8"
2022-04-25 09:24:13 +00:00
);
2022-04-26 17:04:36 +00:00
return JSON.parse(manifestJson);
} catch (e) {
throw new Error(`No such builtin: ${plugName}`);
}
}
);
setInterval(() => {
2022-04-26 17:04:36 +00:00
this.space.updatePageList().catch(console.error);
}, 5000);
2022-04-26 17:04:36 +00:00
this.reloadPlugs().catch(console.error);
}
2022-04-11 18:34:09 +00:00
rebuildMdExtensions() {
this.system.registerSyscalls(
[],
markdownSyscalls(buildMarkdown(loadMarkdownExtensions(this.system)))
);
}
2022-04-26 17:04:36 +00:00
async reloadPlugs() {
await this.space.updatePageList();
await this.system.unloadAll();
console.log("Reloading plugs");
for (let pageInfo of this.space.listPlugs()) {
let { text } = await this.space.readPage(pageInfo.name);
await this.system.load(JSON.parse(text), (p) =>
createSandbox(p, this.preloadedModules)
);
}
this.rebuildMdExtensions();
}
2022-04-24 16:06:34 +00:00
async start() {
2022-04-21 13:16:42 +00:00
await ensurePageIndexTable(this.db);
console.log("Setting up router");
2022-04-24 16:06:34 +00:00
this.app.use("/", express.static(this.distDir));
let fsRouter = express.Router();
// Page list
fsRouter.route("/").get(async (req, res) => {
let { nowTimestamp, pages } = await this.space.fetchPageList();
res.header("Now-Timestamp", "" + nowTimestamp);
res.json([...pages]);
});
fsRouter.route("/").post(bodyParser.json(), async (req, res) => {});
fsRouter
.route(/\/(.+)/)
.get(async (req, res) => {
let pageName = req.params[0];
2022-03-31 15:25:34 +00:00
// console.log("Getting", pageName);
try {
let pageData = await this.space.readPage(pageName);
res.status(200);
res.header("Last-Modified", "" + pageData.meta.lastModified);
res.header("Content-Type", "text/markdown");
res.send(pageData.text);
} catch (e) {
// CORS
res.status(200);
res.header("X-Status", "404");
res.send("");
}
2022-04-01 13:02:35 +00:00
})
.put(bodyParser.text({ type: "*/*" }), async (req, res) => {
let pageName = req.params[0];
console.log("Saving", pageName);
try {
let meta = await this.space.writePage(
pageName,
req.body,
false,
req.header("Last-Modified")
? +req.header("Last-Modified")!
: undefined
);
res.status(200);
res.header("Last-Modified", "" + meta.lastModified);
res.send("OK");
} catch (err) {
res.status(500);
res.send("Write failed");
console.error("Pipeline failed", err);
}
})
.options(async (req, res) => {
let pageName = req.params[0];
try {
const meta = await this.space.getPageMeta(pageName);
res.status(200);
res.header("Last-Modified", "" + meta.lastModified);
res.header("Content-Type", "text/markdown");
res.send("");
} catch (e) {
// CORS
res.status(200);
res.header("X-Status", "404");
res.send("Not found");
}
})
.delete(async (req, res) => {
let pageName = req.params[0];
try {
await this.space.deletePage(pageName);
res.status(200);
res.send("OK");
} catch (e) {
console.error("Error deleting file", e);
res.status(500);
res.send("OK");
}
});
this.app.use(
"/fs",
cors({
methods: "GET,HEAD,PUT,OPTIONS,POST,DELETE",
preflightContinue: true,
}),
fsRouter
);
let plugRouter = express.Router();
plugRouter.post(
"/:plug/syscall/:name",
bodyParser.json(),
async (req, res) => {
const name = req.params.name;
const plugName = req.params.plug;
const args = req.body as any;
const plug = this.system.loadedPlugs.get(plugName);
if (!plug) {
res.status(404);
return res.send(`Plug ${plugName} not found`);
}
try {
const result = await this.system.syscallWithContext(
{ plug },
name,
args
);
res.status(200);
res.send(result);
} catch (e: any) {
res.status(500);
return res.send(e.message);
}
}
);
plugRouter.post(
"/:plug/function/:name",
bodyParser.json(),
async (req, res) => {
const name = req.params.name;
const plugName = req.params.plug;
const args = req.body as any[];
const plug = this.system.loadedPlugs.get(plugName);
if (!plug) {
res.status(404);
return res.send(`Plug ${plugName} not found`);
}
try {
2022-04-09 12:28:41 +00:00
console.log("Invoking", name);
const result = await plug.invoke(name, args);
res.status(200);
res.send(result);
} catch (e: any) {
res.status(500);
console.log("Error invoking function", e);
return res.send(e.message);
}
}
);
this.app.use(
"/plug",
cors({
methods: "GET,HEAD,PUT,OPTIONS,POST,DELETE",
preflightContinue: true,
}),
plugRouter
);
2022-03-21 14:21:34 +00:00
// Fallback, serve index.html
2022-04-25 17:46:08 +00:00
// let cachedIndex: string | undefined = undefined;
this.app.get("/*", async (req, res) => {
2022-04-25 17:46:08 +00:00
// if (!cachedIndex) {
// let cachedIndex = await readFile(`${this.distDir}/index.html`, "utf8");
// }
res.sendFile(`${this.distDir}/index.html`, {});
// res.status(200).header("Content-Type", "text/html").send(cachedIndex);
2022-03-21 14:21:34 +00:00
});
2022-04-24 16:06:34 +00:00
this.server = http.createServer(this.app);
this.server.listen(this.port, () => {
console.log(`Server listening on port ${this.port}`);
});
}
async stop() {
if (this.server) {
console.log("Stopping");
await this.system.unloadAll();
console.log("Stopped plugs");
return new Promise<void>((resolve, reject) => {
this.server!.close((err) => {
this.server = undefined;
console.log("stopped server");
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
2022-03-21 14:21:34 +00:00
}
}