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"; // Cache for rendered posts; null if currently being rendered let templateCache = new Map(); // Clients that requested a post that is currently being rendered; values are // callbacks that take (http status code, rendered html [= null if status != 200]) let renderWatchers = new Map(); function triggerRenderWatchers(path, statusCode, rendered) { const list = renderWatchers.get(path); if (list !== undefined) { try { for (const callback of list) callback(statusCode, rendered); } catch(e) { console.error("While triggering render watchers:", e); } renderWatchers.delete(path); } } 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(/\/[\/]*/g, "/") .replace(/\.html$/, ""); const fromCache = templateCache.get(path); if (fromCache != null) { // neither null nor undefined res.send(fromCache); } else if (fromCache !== undefined) { // Is currently being renderered for another client if (!renderWatchers.has(path)) renderWatchers.set(path, []); renderWatchers.get(path).push((statusCode, rendered) => { if (statusCode == 200) res.send(rendered); else res.sendStatus(statusCode); }); } else { // Indicate that this path is currently being rendered templateCache.set(path, null); generateTemplate(repodir, path ? path + ".html" : undefined) .then(rendered => { templateCache.set(path, rendered); triggerRenderWatchers(path, 200, rendered); res.send(rendered); }) .catch(err => { if (err.code && err.code == "ENOENT") { triggerRenderWatchers(path, 400, null); res.sendStatus(404); } else { triggerRenderWatchers(path, 500, null); console.error(err); res.sendStatus(500); } }); } }); };