Minimale git-remote met publiek/prive-toggle
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
import type { Request, Response } from "express";
import { config } from "../config.ts";
import * as git from "../git.ts";
import { getRepo, listRepos, type Repo } from "../repos.ts";
import {
blobPage,
emptyRepoPage,
errorPage,
highlightSource,
layout,
renderMarkdown,
repoListPage,
treePage,
} from "../views.ts";
const README_NAMES = ["readme.md", "readme.markdown", "readme.txt", "readme"];
export function sendError(req: Request, res: Response, status: number, message: string): void {
res
.status(status)
.type("html")
.send(layout({ title: `${status}`, isOwner: req.isOwner, body: errorPage(status, message) }));
}
export async function repoIndex(req: Request, res: Response): Promise<void> {
const repos = await listRepos();
const visible = req.isOwner ? repos : repos.filter((repo) => repo.public);
res.type("html").send(
layout({
title: config.siteName,
isOwner: req.isOwner,
body: repoListPage(visible, req.isOwner),
}),
);
}
/** Privé-repos bestaan simpelweg niet voor bezoekers die niet ingelogd zijn. */
async function resolveVisibleRepo(req: Request, name: string): Promise<Repo | null> {
const repo = await getRepo(name);
if (!repo) return null;
if (!repo.public && !req.isOwner) return null;
return repo;
}
async function findReadme(
repo: Repo,
branch: string,
filePath: string,
entries: git.TreeEntry[],
): Promise<string | null> {
const match = entries.find(
(entry) => entry.type === "blob" && README_NAMES.includes(entry.name.toLowerCase()),
);
if (!match) return null;
if (match.size !== null && match.size > config.maxRenderBytes) return null;
const fullPath = filePath ? `${filePath}/${match.name}` : match.name;
try {
const buffer = await git.readBlob(repo.publishedDir, branch, fullPath, config.maxRenderBytes);
if (git.looksBinary(buffer)) return null;
const text = buffer.toString("utf8");
const lower = match.name.toLowerCase();
if (lower.endsWith(".md") || lower.endsWith(".markdown")) return renderMarkdown(text);
return `<pre>${text.replaceAll("&", "&").replaceAll("<", "<")}</pre>`;
} catch {
return null;
}
}
/**
* Eén handler voor alles onder `/r`, in plaats van losse routes met wildcards:
* een branchnaam mag schuine strepen bevatten, dus we knippen het pad zelf uit
* elkaar. Alles wat hier gelezen wordt komt uit de gepubliceerde repository, dus
* ook als jij zelf ingelogd bent zie je nooit meer dan een bezoeker.
*/
export async function repoBrowser(req: Request, res: Response): Promise<void> {
const segments = req.path
.split("/")
.filter(Boolean)
.map((part) => {
try {
return decodeURIComponent(part);
} catch {
return part;
}
});
const name = segments[0];
if (!name) {
res.redirect("/");
return;
}
const repo = await resolveVisibleRepo(req, name);
if (!repo) {
sendError(req, res, 404, "Deze repository bestaat niet.");
return;
}
const branches = await git.listPublishedBranches(repo.publishedDir);
const view = segments[1];
if (branches.length === 0) {
res
.type("html")
.send(layout({ title: repo.name, isOwner: req.isOwner, body: emptyRepoPage(repo) }));
return;
}
if (!view) {
const head = (await git.publishedHeadBranch(repo.publishedDir)) ?? branches[0]!;
res.redirect(`/r/${encodeURIComponent(repo.name)}/tree/${head}`);
return;
}
if (view !== "tree" && view !== "blob" && view !== "raw") {
sendError(req, res, 404, "Onbekende weergave.");
return;
}
const resolved = await git.splitBranchAndPath(repo.publishedDir, segments.slice(2).join("/"));
if (!resolved) {
sendError(req, res, 404, "Onbekende branch.");
return;
}
const { branch, filePath } = resolved;
const type = await git.objectType(repo.publishedDir, branch, filePath);
if (type === null) {
sendError(req, res, 404, "Dit bestand of deze map bestaat niet.");
return;
}
if (view === "tree") {
if (type !== "tree") {
res.redirect(
`/r/${encodeURIComponent(repo.name)}/blob/${branch}${filePath ? `/${filePath}` : ""}`,
);
return;
}
const entries = await git.listTree(repo.publishedDir, branch, filePath);
const [readmeHtml, snapshot] = await Promise.all([
findReadme(repo, branch, filePath, entries),
git.snapshotDate(repo.publishedDir, branch),
]);
res.type("html").send(
layout({
title: `${repo.name}${filePath ? `/${filePath}` : ""}`,
isOwner: req.isOwner,
body: treePage({
repo,
branch,
filePath,
entries,
branches,
readmeHtml,
snapshot,
isOwner: req.isOwner,
}),
}),
);
return;
}
if (type !== "blob") {
res.redirect(
`/r/${encodeURIComponent(repo.name)}/tree/${branch}${filePath ? `/${filePath}` : ""}`,
);
return;
}
const size = await git.blobSize(repo.publishedDir, branch, filePath);
const fileName = filePath.slice(filePath.lastIndexOf("/") + 1);
if (view === "raw") {
if (size > 64 * 1024 * 1024) {
sendError(req, res, 413, "Bestand te groot om te serveren.");
return;
}
const buffer = await git.readBlob(repo.publishedDir, branch, filePath, size + 1);
// Nooit het echte content-type raden: dan zou een HTML- of JS-bestand uit een
// publieke repo als script op dit domein kunnen draaien.
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("Content-Security-Policy", "sandbox");
res.setHeader("Content-Disposition", `inline; filename="${fileName.replaceAll('"', "")}"`);
res.type(git.looksBinary(buffer) ? "application/octet-stream" : "text/plain; charset=utf-8");
res.send(buffer);
return;
}
let content: Parameters<typeof blobPage>[0]["content"];
if (size > config.maxRenderBytes) {
content = { kind: "toolarge" };
} else {
const buffer = await git.readBlob(repo.publishedDir, branch, filePath, config.maxRenderBytes);
if (git.looksBinary(buffer)) {
content = { kind: "binary" };
} else {
const text = buffer.toString("utf8");
const lower = fileName.toLowerCase();
content =
lower.endsWith(".md") || lower.endsWith(".markdown")
? { kind: "markdown", html: renderMarkdown(text) }
: { kind: "text", html: highlightSource(text, fileName) };
}
}
res.type("html").send(
layout({
title: `${repo.name}/${filePath}`,
isOwner: req.isOwner,
body: blobPage({ repo, branch, filePath, branches, size, content, isOwner: req.isOwner }),
}),
);
}