Files
ignis/server/index.js

177 lines
5.2 KiB
JavaScript
Raw Normal View History

2026-03-07 09:51:37 +01:00
const express = require("express");
2026-04-05 22:32:23 +02:00
const fs = require("fs");
2026-03-07 09:51:37 +01:00
const path = require("path");
2026-03-22 15:05:37 +01:00
const compression = require("compression");
2026-03-07 09:51:37 +01:00
const config = require("./config");
2026-04-05 22:32:23 +02:00
const { getVersion } = require("./version");
2026-03-07 09:51:37 +01:00
const { setupWebSocket } = require("./ws");
2026-03-26 22:27:50 +01:00
const watcher = require("./watcher");
const { updateBridgePluginInAllVaults } = require("./bridge-plugin");
2026-03-26 23:55:12 +01:00
const { initPlugins, shutdownPlugins } = require("./plugin-system/manager");
const pluginRoutes = require("./routes/plugins");
2026-03-07 09:51:37 +01:00
2026-03-17 12:38:30 +01:00
const ANSI_RED = "\x1b[31m";
const ANSI_YELLOW = "\x1b[33m";
const ANSI_GREEN = "\x1b[32m";
const ANSI_RESET = "\x1b[0m";
2026-03-07 09:51:37 +01:00
const app = express();
app.use(express.json({ limit: "50mb" }));
2026-03-22 15:05:37 +01:00
app.use(compression());
2026-03-07 09:51:37 +01:00
2026-03-17 12:38:30 +01:00
// logger middleware
2026-03-07 09:51:37 +01:00
app.use((req, res, next) => {
const start = Date.now();
const origEnd = res.end;
2026-03-17 12:38:30 +01:00
2026-03-07 09:51:37 +01:00
res.end = function (...args) {
const duration = Date.now() - start;
const status = res.statusCode;
2026-03-17 12:38:30 +01:00
2026-03-07 09:51:37 +01:00
const color =
2026-03-17 12:38:30 +01:00
status >= 500 ? ANSI_RED : status >= 400 ? ANSI_YELLOW : ANSI_GREEN;
2026-03-07 09:51:37 +01:00
const path =
req.originalUrl.length > 80
? req.originalUrl.slice(0, 80) + "..."
: req.originalUrl;
2026-03-17 12:38:30 +01:00
2026-03-07 09:51:37 +01:00
console.log(
2026-03-17 12:38:30 +01:00
`${color}${req.method} ${status}${ANSI_RESET} ${path} (${duration}ms)`,
2026-03-07 09:51:37 +01:00
);
2026-03-17 12:38:30 +01:00
2026-03-07 09:51:37 +01:00
origEnd.apply(this, args);
};
2026-03-17 12:38:30 +01:00
2026-03-07 09:51:37 +01:00
next();
});
const fsRoutes = require("./routes/fs");
const vaultRoutes = require("./routes/vault");
const proxyRoutes = require("./routes/proxy");
2026-03-22 22:32:24 +01:00
const versionRoutes = require("./routes/version");
2026-03-07 09:51:37 +01:00
2026-03-28 14:52:41 +01:00
app.use("/assets", express.static(path.join(__dirname, "assets")));
2026-03-07 09:51:37 +01:00
app.use("/api/fs", fsRoutes);
app.use("/api/vault", vaultRoutes);
app.use("/api/proxy", proxyRoutes);
2026-03-22 22:32:24 +01:00
app.use("/api/version", versionRoutes);
2026-03-26 23:55:12 +01:00
app.use("/api/plugins", pluginRoutes);
2026-03-07 09:51:37 +01:00
2026-03-10 20:49:10 +01:00
// Serve vault files for resource URLs (images, attachments, etc.)
2026-03-10 22:31:01 +01:00
// Vault ID is the first path segment: /vault-files/<vault-id>/path/to/file
app.use("/vault-files", (req, res, next) => {
// Extract vault ID from the first path segment
const parts = req.path.split("/").filter(Boolean);
2026-03-17 12:38:30 +01:00
if (parts.length === 0) {
2026-03-10 22:31:01 +01:00
return res.status(400).json({ error: "Missing vault ID" });
2026-03-17 12:38:30 +01:00
}
2026-03-10 22:31:01 +01:00
const vaultId = decodeURIComponent(parts[0]);
const vaultPath = config.getVaultPath(vaultId);
2026-03-17 12:38:30 +01:00
if (!vaultPath) {
return res.status(404).json({ error: "Vault not found" });
}
2026-03-10 22:31:01 +01:00
// Rewrite req.url to strip the vault ID prefix, then serve statically
req.url = "/" + parts.slice(1).join("/");
express.static(vaultPath)(req, res, next);
});
2026-03-10 20:49:10 +01:00
// Serve our own index.html. Obsidian's scripts are discovered at startup
// and injected dynamically by the client -- no Obsidian files are read or
// transformed in the response.
2026-04-05 22:32:23 +02:00
let cachedHtml = null;
function buildIndexHtml() {
2026-04-05 22:32:23 +02:00
if (cachedHtml) {
return cachedHtml;
}
const version = getVersion();
// Discover Obsidian's script tags from their index.html
const obsidianHtmlPath = path.join(config.obsidianAssetsPath, "index.html");
const obsidianHtml = fs.readFileSync(obsidianHtmlPath, "utf-8");
const scriptRegex = /<script[^>]+src="([^"]+)"[^>]*>/g;
const scripts = [];
let match;
while ((match = scriptRegex.exec(obsidianHtml)) !== null) {
scripts.push(match[1]);
}
// Build from our own template
const templatePath = path.join(__dirname, "assets", "index.html");
let html = fs.readFileSync(templatePath, "utf-8");
2026-04-05 22:32:23 +02:00
html = html.replace("__IGNIS_UI_SRC__", `ignis-ui.js?v=${version}`);
html = html.replace("__SHIM_LOADER_SRC__", `shim-loader.js?v=${version}`);
html = html.replace("__OBSIDIAN_SCRIPTS__", JSON.stringify(scripts));
2026-04-05 22:32:23 +02:00
cachedHtml = html;
return cachedHtml;
}
app.get(["/", "/index.html"], (req, res) => {
res.set("Content-Type", "text/html; charset=utf-8");
res.set("Cache-Control", "no-cache");
res.send(buildIndexHtml());
2026-04-05 22:32:23 +02:00
});
app.get("/favicon.png", (req, res) => {
res.sendFile(path.join(__dirname, "..", "images", "favicon.png"));
});
2026-03-22 18:50:23 +01:00
// Serve dist files with cache headers based on version param
app.use((req, res, next) => {
if (req.path.match(/\/(ignis-ui|shim-loader)\.js$/)) {
if (req.query.v) {
// Versioned assets - cache for 1 year
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
} else {
// No version param - short cache for dev/fallback
res.setHeader("Cache-Control", "public, max-age=300");
}
}
next();
});
2026-03-10 21:07:19 +01:00
app.use(express.static(path.join(__dirname, "..", "dist")));
2026-03-07 09:51:37 +01:00
app.use(express.static(config.obsidianAssetsPath));
2026-03-18 02:38:36 +01:00
const server = app.listen(config.port, async () => {
2026-03-12 22:49:51 +01:00
console.log(`[ignis] Server running on http://localhost:${config.port}`);
console.log(`[ignis] Vault root: ${config.vaultRoot}`);
console.log(`[ignis] Vaults: ${Object.keys(config.vaults).join(", ")}`);
2026-03-18 02:38:36 +01:00
2026-03-26 22:27:50 +01:00
await updateBridgePluginInAllVaults(config.vaultRoot);
2026-03-26 23:55:12 +01:00
await initPlugins({ app, config, wss, watcher });
2026-03-07 09:51:37 +01:00
});
2026-03-26 22:27:50 +01:00
const wss = setupWebSocket(server);
async function gracefulShutdown(signal) {
console.log(`\n[ignis] Received ${signal}, shutting down gracefully...`);
await shutdownPlugins();
server.close(() => {
console.log("[ignis] Server closed");
process.exit(0);
});
setTimeout(() => {
console.error("[ignis] Forced shutdown after timeout");
process.exit(1);
}, 10000);
}
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));