Minimale git-remote met publiek/prive-toggle

main

Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.

6.5 kB Ruw bestand
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("&", "&amp;").replaceAll("<", "&lt;")}</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 }),
    }),
  );
}