import { readFileSync } from "node:fs"; import { join } from "node:path"; import type { Context, Hono } from "hono"; import type { Bucket } from "../bucket/reader.ts"; import { isPlatform, isVersion, type ReleaseFile } from "../bucket/repo.ts"; import { headId, repoStats, saveByPrefix } from "../mirror/queries.ts"; import { notFound, openPublicRepo, type PageContext, type PublicRepo } from "../pages/layout.ts"; import { repoIntro } from "../pages/repo.ts"; import { checksumsTxt, historyJson, metaJson, releaseJson, updateInfoJson } from "./feeds.ts"; import { countDownload, type Release, releaseContentType, visibleReleases } from "./releases.ts"; /** * §22.4: Koh 2's `koh update` reads `/api/koh/releases/latest`, built into every Koh 2 binary. The * route stays on Koh 2's last release forever, never the repo's newest, so a Koh 2 install is never * offered Koh 3. */ const FROZEN_KOH2 = { repo: "koh", version: "2.1.16" } as const; // §22.5: one generic script; each repo gets its own name and address filled in. const INSTALL_SH = readFileSync(join(import.meta.dir, "install.sh"), "utf8"); const JSON_TYPE = "application/json; charset=UTF-8"; const TEXT_TYPE = "text/plain; charset=UTF-8"; // Kepr 2's error bodies. A private repo answers exactly as an unknown one (§20). const REPO_NOT_FOUND = '{"error":"repo_not_found","message":"repo not found"}'; const NO_RELEASES = '{"error":"not_found","message":"no releases"}'; const RELEASE_NOT_FOUND = '{"error":"not_found","message":"release not found"}'; type Feed = { repo: PublicRepo; releases: Release[] }; export function registerReleaseRoutes(app: Hono, ctx: PageContext, bucket: Bucket): void { const send = (c: Context, status: 200 | 404, body: string, type = JSON_TYPE) => c.body(body, status, { "Content-Type": type }); const feed = (name: string): Feed | null => { const repo = openPublicRepo(ctx, name); return repo && { repo, releases: visibleReleases(ctx.kepr, repo.mirror, name) }; }; // Counted before the redirect (§22.2); Kepr never streams a release file itself. const download = (c: Context, repo: string, version: string, file: ReleaseFile) => { countDownload(ctx.kepr, repo, version, file.platform); const key = `${repo}/releases/${version}/${file.platform}/${file.name}`; c.header("Cache-Control", "no-store"); return c.redirect(bucket.downloadUrl(key, file.name, releaseContentType(file.name)), 302); }; const downloadAt = (c: Context, name: string, version: string, requested: string) => { const f = feed(name); if (!f) return send(c, 404, REPO_NOT_FOUND); // Kepr 2's download links end in `-`, so `curl -O` saves a telling name. const platform = requested.startsWith(`${name}-`) ? requested.slice(name.length + 1) : requested; const release = f.releases.find((r) => r.version === version); const file = isPlatform(platform) && release?.files.find((x) => x.platform === platform); return file ? download(c, name, version, file) : send(c, 404, RELEASE_NOT_FOUND); }; // The newest visible release, or the response that says why there is none. const newest = (c: Context, name: string): Release | Response => { const f = feed(name); if (!f) return send(c, 404, REPO_NOT_FOUND); return f.releases[0] ?? send(c, 404, NO_RELEASES); }; app.get("/api/koh/releases/latest", (c) => send(c, 404, '{"error":"no_releases","message":"no releases available"}'), ); app.get("/api/koh/releases/:version/:platform", (c) => downloadAt(c, FROZEN_KOH2.repo, c.req.param("version"), c.req.param("platform")), ); for (const [path, cors] of [ ["/api/:repo/releases/latest", true], ["/:repo/releases/latest.json", false], ] as const) { app.get(path, (c) => { // §22.4 puts CORS on the route, so its 404s carry it too (Kepr 2 sent it only on success). if (cors) c.header("Access-Control-Allow-Origin", "*"); const release = newest(c, c.req.param("repo")); if (release instanceof Response) return release; return send(c, 200, releaseJson(ctx.domain, c.req.param("repo"), release)); }); } for (const [path, cors] of [ ["/api/:repo/releases/history", true], ["/:repo/releases/history.json", false], ] as const) { app.get(path, (c) => { if (cors) c.header("Access-Control-Allow-Origin", "*"); const f = feed(c.req.param("repo")); if (!f) return send(c, 404, REPO_NOT_FOUND); if (f.releases.length === 0) return send(c, 404, NO_RELEASES); return send(c, 200, historyJson(f.releases)); }); } app.get("/:repo/releases/latest", (c) => { const repo = openPublicRepo(ctx, c.req.param("repo")); if (!repo) return notFound(c, ctx); return c.redirect(`/${repo.settings.name}/releases`, 302); }); app.get("/:repo/releases/latest/version.txt", (c) => { const release = newest(c, c.req.param("repo")); return release instanceof Response ? release : send(c, 200, release.version, TEXT_TYPE); }); app.get("/:repo/releases/:version/checksums.txt", (c) => { const name = c.req.param("repo"); const f = feed(name); if (!f) return send(c, 404, REPO_NOT_FOUND); const release = f.releases.find((r) => r.version === c.req.param("version")); if (!release) return send(c, 404, RELEASE_NOT_FOUND); return send(c, 200, checksumsTxt(name, release), TEXT_TYPE); }); // §22.1: the newest file for that platform, from the newest release that has one. app.get("/:repo/releases/latest/:platform", (c) => { const name = c.req.param("repo"); const f = feed(name); if (!f) return send(c, 404, REPO_NOT_FOUND); const platform = c.req.param("platform"); for (const release of f.releases) { const file = release.files.find((x) => x.platform === platform); if (file) return download(c, name, release.version, file); } return send(c, 404, RELEASE_NOT_FOUND); }); app.get("/:repo/releases/:version/:platform", (c) => { const version = c.req.param("version"); if (!isVersion(version)) return send(c, 404, RELEASE_NOT_FOUND); return downloadAt(c, c.req.param("repo"), version, c.req.param("platform")); }); // Only for a repo with a release, since the script would have nothing to install. app.get("/:repo/install.sh", (c) => { const name = c.req.param("repo"); const release = newest(c, name); if (release instanceof Response) return release; const script = INSTALL_SH.replaceAll("@REPO@", name).replaceAll( "@BASE@", `https://${ctx.domain}/${name}`, ); c.header("Cache-Control", "public, max-age=60"); return send(c, 200, script, TEXT_TYPE); }); app.get("/:repo/update-info", (c) => { const name = c.req.param("repo"); const f = feed(name); if (!f) return send(c, 404, REPO_NOT_FOUND); // §22.4: head is the newest save on the line, a checkpoint included. Its version is the // release built from it, else its ID, as in Kepr 2. const id = headId(f.repo.mirror); const save = id ? saveByPrefix(f.repo.mirror, id) : null; const saveHead = save && { id: save.id, version: f.releases.find((r) => r.save === save.id)?.version ?? save.id, createdAt: save.created_at, }; return send(c, 200, updateInfoJson(ctx.domain, name, saveHead ?? null, f.releases[0])); }); app.get("/:repo/meta", (c) => { const f = feed(c.req.param("repo")); if (!f) return send(c, 404, REPO_NOT_FOUND); const { name, category } = f.repo.settings; const stats = repoStats(f.repo.mirror); const meta = { repo: name, saveCount: stats.saves, lineCount: stats.lines, lastSaveAt: stats.updated ?? 0, latestRelease: f.releases[0]?.version ?? null, description: repoIntro(f.repo)?.text ?? null, language: stats.language || null, category: category || null, }; return send(c, 200, metaJson(meta)); }); }