From e66f29ff85392e3c06e9033e37ead06a9d9d5daa Mon Sep 17 00:00:00 2001 From: Tom Smeding Date: Thu, 18 Jun 2020 21:48:33 +0200 Subject: Add blog --- modules/blog/.gitignore | 1 + modules/blog/blog.js | 135 +++++++++++++++++++++++++++++++++++++++++++++++ modules/blog/template.js | 56 ++++++++++++++++++++ 3 files changed, 192 insertions(+) create mode 100644 modules/blog/.gitignore create mode 100644 modules/blog/blog.js create mode 100644 modules/blog/template.js (limited to 'modules/blog') 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 = `
${entry}
\n`; + out += elt; + } else { // subdirectory + out += '
\n'; + out += `${entry}/\n`; + out += '
\n'; + out += generateTree(sub, path + "/" + entry); + out += "
\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("", generateTree(tree, pathRoot)); + if (contentPath) { + const content = await fs.readFile(repodir + "/" + contentPath, { encoding: "utf-8" }); + html = html.replace("", content); + } + + return html; +} + +module.exports = template; -- cgit v1.2.3-70-g09d2