2022-04-06 13:39:20 +00:00
|
|
|
import express, { Express } from "express";
|
2022-04-21 11:57:45 +00:00
|
|
|
import { SilverBulletHooks } from "@silverbulletmd/common/manifest";
|
|
|
|
import { EndpointHook } from "@silverbulletmd/plugos/hooks/endpoint";
|
2022-04-06 13:39:20 +00:00
|
|
|
import { readFile } from "fs/promises";
|
2022-04-21 11:57:45 +00:00
|
|
|
import { System } from "@silverbulletmd/plugos/system";
|
2022-03-31 12:28:07 +00:00
|
|
|
import cors from "cors";
|
2022-04-21 11:57:45 +00:00
|
|
|
import { DiskSpacePrimitives } from "@silverbulletmd/common/spaces/disk_space_primitives";
|
2022-03-31 12:28:07 +00:00
|
|
|
import path from "path";
|
|
|
|
import bodyParser from "body-parser";
|
2022-04-21 11:57:45 +00:00
|
|
|
import { EventHook } from "@silverbulletmd/plugos/hooks/event";
|
2022-03-31 12:28:07 +00:00
|
|
|
import spaceSyscalls from "./syscalls/space";
|
2022-04-21 11:57:45 +00:00
|
|
|
import { eventSyscalls } from "@silverbulletmd/plugos/syscalls/event";
|
2022-04-21 13:16:42 +00:00
|
|
|
import { ensurePageIndexTable, pageIndexSyscalls } from "./syscalls";
|
2022-04-06 13:39:20 +00:00
|
|
|
import knex, { Knex } from "knex";
|
2022-04-21 11:57:45 +00:00
|
|
|
import shellSyscalls from "@silverbulletmd/plugos/syscalls/shell.node";
|
|
|
|
import { NodeCronHook } from "@silverbulletmd/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";
|
2022-04-21 12:16:40 +00:00
|
|
|
import { safeRun, throttle } from "@silverbulletmd/common/util";
|
2022-04-21 11:57:45 +00:00
|
|
|
import { createSandbox } from "@silverbulletmd/plugos/environments/node_sandbox";
|
|
|
|
import { jwtSyscalls } from "@silverbulletmd/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-03-21 14:21:34 +00:00
|
|
|
|
|
|
|
export class ExpressServer {
|
|
|
|
app: Express;
|
|
|
|
system: System<SilverBulletHooks>;
|
|
|
|
private rootPath: string;
|
2022-04-08 15:46:09 +00:00
|
|
|
private space: Space;
|
2022-03-31 12:28:07 +00:00
|
|
|
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-03-21 14:21:34 +00:00
|
|
|
|
2022-04-24 16:06:34 +00:00
|
|
|
constructor(port: number, rootPath: string, distDir: string) {
|
|
|
|
this.port = port;
|
|
|
|
this.app = express();
|
2022-03-21 14:21:34 +00:00
|
|
|
this.rootPath = rootPath;
|
2022-03-31 12:28:07 +00:00
|
|
|
this.distDir = distDir;
|
2022-04-24 16:06:34 +00:00
|
|
|
this.system = new System<SilverBulletHooks>("server");
|
2022-03-21 14:21:34 +00:00
|
|
|
|
2022-03-31 12:28:07 +00:00
|
|
|
// Setup system
|
|
|
|
this.eventHook = new EventHook();
|
2022-04-24 16:06:34 +00:00
|
|
|
this.system.addHook(this.eventHook);
|
2022-04-08 15:46:09 +00:00
|
|
|
this.space = new Space(
|
|
|
|
new EventedSpacePrimitives(
|
|
|
|
new DiskSpacePrimitives(rootPath),
|
|
|
|
this.eventHook
|
|
|
|
),
|
|
|
|
true
|
2022-03-31 12:28:07 +00:00
|
|
|
);
|
|
|
|
this.db = knex({
|
|
|
|
client: "better-sqlite3",
|
|
|
|
connection: {
|
|
|
|
filename: path.join(rootPath, "data.db"),
|
|
|
|
},
|
|
|
|
useNullAsDefault: true,
|
|
|
|
});
|
2022-03-31 15:25:34 +00:00
|
|
|
|
2022-04-24 16:06:34 +00:00
|
|
|
this.system.registerSyscalls(["shell"], shellSyscalls(rootPath));
|
|
|
|
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([])));
|
|
|
|
this.system.registerSyscalls([], jwtSyscalls());
|
|
|
|
this.system.addHook(new EndpointHook(this.app, "/_/"));
|
2022-04-08 15:46:09 +00:00
|
|
|
|
2022-04-11 18:34:09 +00:00
|
|
|
let throttledRebuildMdExtensions = throttle(() => {
|
|
|
|
this.rebuildMdExtensions();
|
|
|
|
}, 100);
|
|
|
|
|
2022-04-08 15:46:09 +00:00
|
|
|
this.space.on({
|
|
|
|
plugLoaded: (plugName, plug) => {
|
|
|
|
safeRun(async () => {
|
|
|
|
console.log("Plug load", plugName);
|
2022-04-24 16:06:34 +00:00
|
|
|
await this.system.load(plugName, plug, createSandbox);
|
2022-04-08 15:46:09 +00:00
|
|
|
});
|
2022-04-11 18:34:09 +00:00
|
|
|
throttledRebuildMdExtensions();
|
2022-04-08 15:46:09 +00:00
|
|
|
},
|
|
|
|
plugUnloaded: (plugName) => {
|
|
|
|
safeRun(async () => {
|
|
|
|
console.log("Plug unload", plugName);
|
2022-04-24 16:06:34 +00:00
|
|
|
await this.system.unload(plugName);
|
2022-04-08 15:46:09 +00:00
|
|
|
});
|
2022-04-11 18:34:09 +00:00
|
|
|
throttledRebuildMdExtensions();
|
2022-04-08 15:46:09 +00:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
setInterval(() => {
|
|
|
|
this.space.updatePageListAsync();
|
|
|
|
}, 5000);
|
|
|
|
this.space.updatePageListAsync();
|
2022-03-31 12:28:07 +00:00
|
|
|
}
|
|
|
|
|
2022-04-11 18:34:09 +00:00
|
|
|
rebuildMdExtensions() {
|
|
|
|
this.system.registerSyscalls(
|
|
|
|
[],
|
|
|
|
markdownSyscalls(buildMarkdown(loadMarkdownExtensions(this.system)))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-04-24 16:06:34 +00:00
|
|
|
async start() {
|
2022-04-21 13:16:42 +00:00
|
|
|
await ensurePageIndexTable(this.db);
|
2022-03-31 12:28:07 +00:00
|
|
|
console.log("Setting up router");
|
|
|
|
|
2022-04-24 16:06:34 +00:00
|
|
|
this.app.use("/", express.static(this.distDir));
|
|
|
|
|
2022-03-31 12:28:07 +00:00
|
|
|
let fsRouter = express.Router();
|
|
|
|
|
|
|
|
// Page list
|
|
|
|
fsRouter.route("/").get(async (req, res) => {
|
2022-04-08 15:46:09 +00:00
|
|
|
let { nowTimestamp, pages } = await this.space.fetchPageList();
|
|
|
|
res.header("Now-Timestamp", "" + nowTimestamp);
|
|
|
|
res.json([...pages]);
|
2022-03-31 12:28:07 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
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);
|
2022-03-31 12:28:07 +00:00
|
|
|
try {
|
2022-04-08 15:46:09 +00:00
|
|
|
let pageData = await this.space.readPage(pageName);
|
2022-03-31 12:28:07 +00:00
|
|
|
res.status(200);
|
|
|
|
res.header("Last-Modified", "" + pageData.meta.lastModified);
|
|
|
|
res.header("Content-Type", "text/markdown");
|
|
|
|
res.send(pageData.text);
|
|
|
|
} catch (e) {
|
2022-04-06 13:39:20 +00:00
|
|
|
// CORS
|
2022-03-31 12:28:07 +00:00
|
|
|
res.status(200);
|
2022-04-06 13:39:20 +00:00
|
|
|
res.header("X-Status", "404");
|
2022-03-31 12:28:07 +00:00
|
|
|
res.send("");
|
|
|
|
}
|
2022-04-01 13:02:35 +00:00
|
|
|
})
|
2022-03-31 12:28:07 +00:00
|
|
|
.put(bodyParser.text({ type: "*/*" }), async (req, res) => {
|
|
|
|
let pageName = req.params[0];
|
|
|
|
console.log("Saving", pageName);
|
|
|
|
|
|
|
|
try {
|
2022-04-08 15:46:09 +00:00
|
|
|
let meta = await this.space.writePage(
|
2022-04-06 13:39:20 +00:00
|
|
|
pageName,
|
|
|
|
req.body,
|
2022-04-08 15:46:09 +00:00
|
|
|
false,
|
2022-04-06 13:39:20 +00:00
|
|
|
req.header("Last-Modified")
|
|
|
|
? +req.header("Last-Modified")!
|
|
|
|
: undefined
|
|
|
|
);
|
2022-03-31 12:28:07 +00:00
|
|
|
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 {
|
2022-04-08 15:46:09 +00:00
|
|
|
const meta = await this.space.getPageMeta(pageName);
|
2022-03-31 12:28:07 +00:00
|
|
|
res.status(200);
|
|
|
|
res.header("Last-Modified", "" + meta.lastModified);
|
|
|
|
res.header("Content-Type", "text/markdown");
|
|
|
|
res.send("");
|
|
|
|
} catch (e) {
|
2022-04-06 13:39:20 +00:00
|
|
|
// CORS
|
2022-03-31 12:28:07 +00:00
|
|
|
res.status(200);
|
2022-04-06 13:39:20 +00:00
|
|
|
res.header("X-Status", "404");
|
|
|
|
res.send("Not found");
|
2022-03-31 12:28:07 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.delete(async (req, res) => {
|
|
|
|
let pageName = req.params[0];
|
|
|
|
try {
|
2022-04-08 15:46:09 +00:00
|
|
|
await this.space.deletePage(pageName);
|
2022-03-31 12:28:07 +00:00
|
|
|
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);
|
2022-03-31 12:28:07 +00:00
|
|
|
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
|
|
|
|
let cachedIndex: string | undefined = undefined;
|
2022-03-31 12:28:07 +00:00
|
|
|
this.app.get("/*", async (req, res) => {
|
2022-03-21 14:21:34 +00:00
|
|
|
if (!cachedIndex) {
|
2022-03-31 12:28:07 +00:00
|
|
|
cachedIndex = await readFile(`${this.distDir}/index.html`, "utf8");
|
2022-03-21 14:21:34 +00:00
|
|
|
}
|
|
|
|
res.status(200).header("Content-Type", "text/html").send(cachedIndex);
|
|
|
|
});
|
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
|
|
|
}
|
|
|
|
}
|