summaryrefslogtreecommitdiff
path: root/modules/blog
diff options
context:
space:
mode:
Diffstat (limited to 'modules/blog')
-rw-r--r--modules/blog/.gitignore1
-rw-r--r--modules/blog/blog.js135
-rw-r--r--modules/blog/template.js56
3 files changed, 192 insertions, 0 deletions
diff --git a/modules/blog/.gitignore b/modules/blog/.gitignore
new file mode 100644
index 0000000..ff33cf1
--- /dev/null
+++ b/modules/blog/.gitignore
@@ -0,0 +1 @@
+/repo/
diff --git a/modules/blog/blog.js b/modules/blog/blog.js
new file mode 100644
index 0000000..0b7785b
--- /dev/null
+++ b/modules/blog/blog.js
@@ -0,0 +1,135 @@
+const cmn = require("../$common.js");
+const fs = require("fs");
+const https = require("https");
+const child_process = require("child_process");
+
+const generateTemplate = require("./template.js");
+
+let moddir = null;
+let repodir = null;
+const repoRemote = "https://git.tomsmeding.com/blog";
+
+let templateCache = new Map();
+
+function fetch(url) {
+ return new Promise((resolve, reject) => {
+ https.get(url, res => {
+ if (res.statusCode != 200) {
+ reject(`HTTP status code ${res.statusCode}`);
+ return;
+ }
+
+ let buffers = [];
+ res.on("data", d => buffers.push(d));
+ res.on("end", () => {
+ resolve(Buffer.concat(buffers));
+ });
+ }).on("error", err => {
+ reject(err);
+ });
+ });
+}
+
+function runCommand(cmd, args) {
+ console.log(`blog: ${cmd} ${JSON.stringify(args)}`);
+ child_process.execFileSync(
+ cmd, args,
+ { stdio: "inherit", timeout: 20000 }
+ );
+}
+
+function runCommandOutput(cmd, args) {
+ return child_process.execFileSync(
+ cmd, args,
+ { timeout: 20000 }
+ );
+}
+
+function ensureRepo() {
+ try {
+ const stats = fs.statSync(repodir);
+ if (stats.isDirectory()) return;
+ } catch (e) {}
+
+ runCommand("git", ["clone", "https://git.tomsmeding.com/blog", repodir]);
+}
+
+function currentCommit() {
+ return runCommandOutput("git", ["-C", repodir, "rev-parse", "HEAD"]).toString().trim();
+}
+
+async function upstreamCommit() {
+ const body = await fetch(repoRemote + "/info/refs");
+ return body.toString().split("\t")[0];
+}
+
+function updateRepo() {
+ try {
+ runCommand("git", ["-C", repodir, "fetch", "--all"]);
+ runCommand("git", ["-C", repodir, "reset", "origin/master", "--hard"]);
+ // Reset cache _after_ the commands succeeded; if anything failed, we
+ // might at least still have stale cache data
+ templateCache = new Map();
+ } catch (e) {
+ console.error("Cannot update blog git repo!");
+ console.error(e);
+ }
+}
+
+async function periodicUpdateCheck() {
+ let shouldUpdate = false;
+ try {
+ const local = currentCommit();
+ const remote = await upstreamCommit();
+ if (local != remote) shouldUpdate = true;
+ } catch (e) {
+ // Also update if our pre-check fails for some reason
+ shouldUpdate = true;
+ }
+ if (shouldUpdate) updateRepo();
+}
+
+setInterval(function () {
+ periodicUpdateCheck().catch(err => console.error(err));
+}, 3600 * 1000);
+
+module.exports = (app, io, _moddir) => {
+ moddir = _moddir;
+ repodir = moddir + "/repo";
+
+ ensureRepo();
+ updateRepo();
+
+ app.get("/blog", (req, res, next) => {
+ req.url = "/blog/";
+ next();
+ });
+
+ app.get("/blog/*", (req, res) => {
+ if (req.path.indexOf("/.") != -1) {
+ res.sendStatus(404);
+ return;
+ }
+
+ const path = req.path.slice(6).replace(/\.html$/, "");
+
+ if (templateCache.has(path)) {
+ res.send(templateCache.get(path));
+ } else {
+ generateTemplate(repodir, path ? path + ".html" : undefined)
+ .then(rendered => {
+ // TODO: fix rendering race condition
+ templateCache.set(path, rendered);
+ res.send(rendered);
+ })
+ .catch(err => {
+ if (err.code && err.code == "ENOENT") {
+ res.sendStatus(404);
+ } else {
+ console.error(err);
+ res.sendStatus(500);
+ }
+ });
+ }
+ });
+};
diff --git a/modules/blog/template.js b/modules/blog/template.js
new file mode 100644
index 0000000..1f26e82
--- /dev/null
+++ b/modules/blog/template.js
@@ -0,0 +1,56 @@
+const fs = require("fs").promises;
+
+const pathRoot = "blog";
+
+async function recursiveTree(dir) {
+ const res = new Map();
+
+ const entries = await fs.readdir(dir, { withFileTypes: true });
+ for (const entry of entries) {
+ if (entry.name[0] == ".") continue;
+
+ if (entry.isDirectory()) {
+ res.set(entry.name, await recursiveTree(dir + "/" + entry.name));
+ } else if (entry.isFile() && entry.name.endsWith(".html")) {
+ res.set(entry.name.slice(0, entry.name.length - 5), true);
+ }
+ }
+
+ return res;
+}
+
+function generateTree(tree, path) {
+ let out = "";
+
+ for (const [entry, sub] of tree) {
+ if (sub === true) { // file
+ const elt = `<div class="tree-node tree-file"><a href="/${path}/${entry}">${entry}</a></div>\n`;
+ out += elt;
+ } else { // subdirectory
+ out += '<div class="tree-node tree-dir">\n';
+ out += `<span class="tree-dir-name">${entry}/</span>\n`;
+ out += '<div class="tree-sub">\n';
+ out += generateTree(sub, path + "/" + entry);
+ out += "</div></div>\n";
+ }
+ }
+
+ return out;
+}
+
+async function template(repodir, contentPath) {
+ const tree = await recursiveTree(repodir);
+ tree.delete("index");
+
+ let html = await fs.readFile(repodir + "/index.html", { encoding: "utf-8" });
+
+ html = html.replace("<!-- REPLACE TREE -->", generateTree(tree, pathRoot));
+ if (contentPath) {
+ const content = await fs.readFile(repodir + "/" + contentPath, { encoding: "utf-8" });
+ html = html.replace("<!-- REPLACE CONTENT -->", content);
+ }
+
+ return html;
+}
+
+module.exports = template;