sourceapproutessource.js

import { existsSync, readFileSync, readdirSync, statSync } from "fs";
import * as path from "path";

import Database from "better-sqlite3";
import express from "express";
import createError from "http-errors";

import grimm from "../grimm.js";
import { highlightFilter, prettyRender, rootFolder } from "../helpers.js";

const router = express.Router();
const db = new Database("./db/site.db");
db.pragma("journal_mode = DELETE");

const permittedPaths = [
	"app",
	"bin",
	"db",
	"hypertext/admin/auth",
	"hypertext/admin/planter",
	"hypertext/admin/stats",
	"hypertext/public",
	"hypertext/views",
	"app.js",
	"package.json"
].map(x => path.join(rootFolder, x));

const folderOnlyPaths = ["hypertext", "hypertext/admin"].map(x =>
	path.join(rootFolder, x)
);

const classifyFile = (filename, granular = false) => {
	if (filename.match(/bin\.www$|\.txt$/))
		return granular ? "text" : "plaintext";
	if (filename.match(/\.html$|\.pug$/))
		return granular ? "markup" : "plaintext";
	if (filename.match(/\.js$/)) return granular ? "js" : "plaintext";
	if (filename.match(/\.css$/)) return granular ? "css" : "plaintext";
	if (filename.match(/\.xml$/)) return granular ? "xml" : "plaintext";
	if (filename.match(/\.csv$|\.json$/))
		return granular ? "data" : "plaintext";
	if (filename.match(/\.vst$|\.srt$/))
		return granular ? "subtitles" : "plaintext";

	if (filename.match(/\.(jpeg|jpg|png|webp|gif|avif|svg)$/))
		return granular ? "image" : "asset";
	if (filename.match(/\.(webm|mkv|mv|avi|mp4)$/))
		return granular ? "video" : "asset";
	if (filename.match(/\.(alac|aac|flac|ogg|mp3|wav)$/))
		return granular ? "audio" : "asset";
	if (filename.match(/\.glb$/)) return granular ? "3d" : "asset";
	if (filename.match(/\.db$/)) return granular ? "database" : "asset";
	if (filename.match(/\.[a-z0-9]+$/))
		return granular ? "other-asset" : "asset";

	return "folder";
};

const sortFolder = (a, b) => {
	if (a.broadClass != "folder" && b.broadClass == "folder") return 1;
	if (a.broadClass == "folder" && b.broadClass != "folder") return -1;
	if (a.name > b.name) return 1;
	if (b.name > a.name) return -1;
	return 0;
};

const fileSizeToText = byteTotal => {
	switch (Math.floor(Math.log2(byteTotal) / 10)) {
		case 0:
			// Should really be using the internationalisation library for this,
			// but when on earth am i going to have a 1-byte file?
			return `${byteTotal} bytes`;
		case 1:
			return `${(byteTotal / 1024).toLocaleString("en-GB", {
				maximumSignificantDigits: 3
			})} KiB`;
		case 2:
			return `${(byteTotal / 1024 ** 2).toLocaleString("en-GB", {
				maximumSignificantDigits: 3
			})} MiB`;
		case 3:
			return `${(byteTotal / 1024 ** 3).toLocaleString("en-GB", {
				maximumSignificantDigits: 3
			})} GiB`;
		default:
			return `${(byteTotal / 1024 ** 4).toLocaleString("en-GB", {
				maximumSignificantDigits: 3
			})} TiB`;
	}
};

const displayPage = (slug, res, next) => {
	const fullAddress = path.join(rootFolder, slug);
	const fileClass = classifyFile(fullAddress, false);

	if (
		!permittedPaths.some(x => fullAddress.startsWith(x)) &&
		!folderOnlyPaths.includes(fullAddress)
	) {
		return next(createError(403));
	}

	if (!existsSync(fullAddress) || fileClass == "asset") {
		return next(createError(404));
	}

	let pathSplit = slug.split(path.sep).map(el => ({ part: el }));
	let pathUrl = "";
	for (let el of pathSplit) {
		pathUrl += `/${el.part}`;
		el.url = pathUrl;
	}

	// If it’s just a regular-ass file, we can stop here
	if (fileClass == "plaintext") {
		const file = readFileSync(fullAddress, { encoding: "utf8" });

		return prettyRender(res, next, "../views/source/viewer", {
			currentFile: slug,
			breadcrumbs: pathSplit,
			grimm: grimm,
			tr: grimm.translator("en"),
			highlitCode: highlightFilter.highlight(file, {
				lang: fullAddress.match(/bin.www$/)
					? "js"
					: path.extname(fullAddress).slice(1)
			})
		});
	}

	// If it’s a folder, on the other hand…

	let folderContents = readdirSync(fullAddress).map(file => ({
		name: file,
		broadClass: classifyFile(file, false),
		narrowClass: classifyFile(file, true),
		noLink: false
	}));

	folderContents.sort(sortFolder);

	folderContents.forEach(file => {
		if (file.broadClass != "folder") {
			const fileStats = statSync(path.join(fullAddress, file.name));
			file.dateEdited = fileStats.mtime.toISOString().slice(0, 10);
			file.relativeDate = fileStats.mtime - new Date();
			file.size = fileSizeToText(fileStats.size);
		}
	});

	if (folderOnlyPaths.includes(fullAddress)) {
		folderContents.forEach(file => {
			const fullFilePath = path.join(fullAddress, file.name);
			if (
				!permittedPaths.some(x => fullFilePath.startsWith(x)) &&
				!folderOnlyPaths.includes(fullFilePath)
			) {
				file.noLink = true;
			}
		});
	}

	return prettyRender(res, next, "../views/source/folder", {
		currentFile: slug,
		breadcrumbs: pathSplit,
		folderContents: folderContents,
		grimm: grimm,
		tr: grimm.translator("en")
	});
};

const displayFrontPage = (res, next) => {
	// If it’s a folder, on the other hand…

	let folderContents = readdirSync(rootFolder).map(file => ({
		name: file,
		broadClass: classifyFile(file, false),
		narrowClass: classifyFile(file, true),
		noLink: false
	}));

	folderContents.sort(sortFolder);

	folderContents.forEach(file => {
		const fullFilePath = path.join(rootFolder, file.name);

		if (file.broadClass != "folder") {
			const fileStats = statSync(fullFilePath);
			file.dateEdited = fileStats.mtime.toISOString().slice(0, 10);
			file.relativeDate = fileStats.mtime - new Date();
			file.size = fileSizeToText(fileStats.size);
		}

		if (
			!permittedPaths.some(x => fullFilePath.startsWith(x)) &&
			!folderOnlyPaths.includes(fullFilePath)
		) {
			file.noLink = true;
		}
	});

	return prettyRender(res, next, "../views/source/root-page", {
		folderContents: folderContents,
		grimm: grimm,
		tr: grimm.translator("en")
	});
};

router.get("/", (req, res, next) => displayFrontPage(res, next));

router.get("/for/:pageSlug*", (req, res, next) =>
	displayPage(
		`${req.params.pageSlug}${req.params[0]}`.replace(/\/$/, ""),
		res,
		next
	)
);

export default router;