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:
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 (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)
})
});
}
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) => {
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;