routes.ts

7.0 KB · raw

1import type { Context, Hono } from "hono";2import type { BucketReader } from "../bucket/reader.ts";3import { isPlatform, isVersion, type ReleaseFile } from "../bucket/repo.ts";4import { history, repoStats } from "../mirror/queries.ts";5import { notFound, openPublicRepo, type PageContext, type PublicRepo } from "../pages/layout.ts";6import { readmeAtHead, readmeIntro } from "../pages/repo.ts";7import {8	checksumsTxt,9	historyJson,10	legacyReleaseJson,11	metaJson,12	releaseJson,13	updateInfoJson,14} from "./feeds.ts";15import { countDownload, type Release, releaseContentType, visibleReleases } from "./releases.ts";1617/**18 * §22.4: Koh 2's `koh update` reads `/api/koh/releases/latest`, built into every Koh 2 binary. The19 * route stays on Koh 2's last release forever, never the repo's newest, so a Koh 2 install is never20 * offered Koh 3.21 */22const FROZEN_KOH2 = { repo: "koh", version: "2.1.16" } as const;2324const JSON_TYPE = "application/json; charset=UTF-8";25const TEXT_TYPE = "text/plain; charset=UTF-8";26// Kepr 2's error bodies. A private repo answers exactly as an unknown one (§20).27const REPO_NOT_FOUND = '{"error":"repo_not_found","message":"repo not found"}';28const NO_RELEASES = '{"error":"not_found","message":"no releases"}';29const RELEASE_NOT_FOUND = '{"error":"not_found","message":"release not found"}';3031type Feed = { repo: PublicRepo; releases: Release[] };3233export function registerReleaseRoutes(app: Hono, ctx: PageContext, bucket: BucketReader): void {34	const send = (c: Context, status: 200 | 404, body: string, type = JSON_TYPE, cors = false) => {35		if (cors) c.header("Access-Control-Allow-Origin", "*");36		return c.body(body, status, { "Content-Type": type });37	};38	const feed = (name: string): Feed | null => {39		const repo = openPublicRepo(ctx, name);40		return repo && { repo, releases: visibleReleases(ctx.kepr, repo.mirror, name) };41	};42	// Counted before the redirect (§22.2); Kepr never streams a release file itself.43	const download = (c: Context, repo: string, version: string, file: ReleaseFile) => {44		countDownload(ctx.kepr, repo, version, file.platform);45		const key = `${repo}/releases/${version}/${file.platform}/${file.name}`;46		c.header("Cache-Control", "no-store");47		return c.redirect(bucket.downloadUrl(key, file.name, releaseContentType(file.name)), 302);48	};49	const downloadAt = (c: Context, name: string, version: string, requested: string) => {50		const f = feed(name);51		if (!f) return send(c, 404, REPO_NOT_FOUND);52		// Kepr 2's download links end in `<repo>-<platform>`, so `curl -O` saves a telling name.53		const platform = requested.startsWith(`${name}-`)54			? requested.slice(name.length + 1)55			: requested;56		const release = f.releases.find((r) => r.version === version);57		const file = isPlatform(platform) && release?.files.find((x) => x.platform === platform);58		return file ? download(c, name, version, file) : send(c, 404, RELEASE_NOT_FOUND);59	};60	// The newest visible release, or the response that says why there is none.61	const newest = (c: Context, name: string): Release | Response => {62		const f = feed(name);63		if (!f) return send(c, 404, REPO_NOT_FOUND);64		return f.releases[0] ?? send(c, 404, NO_RELEASES);65	};6667	app.get("/api/koh/releases/latest", (c) => {68		const f = feed(FROZEN_KOH2.repo);69		const release = f?.releases.find((r) => r.version === FROZEN_KOH2.version);70		if (!release) return send(c, 404, '{"error":"no_releases","message":"no releases available"}');71		return send(c, 200, legacyReleaseJson(ctx.domain, FROZEN_KOH2.repo, release));72	});73	app.get("/api/koh/releases/:version/:platform", (c) =>74		downloadAt(c, FROZEN_KOH2.repo, c.req.param("version"), c.req.param("platform")),75	);76	for (const [path, cors] of [77		["/api/:repo/releases/latest", true],78		["/:repo/releases/latest.json", false],79	] as const) {80		app.get(path, (c) => {81			const release = newest(c, c.req.param("repo"));82			if (release instanceof Response) return release;83			return send(c, 200, releaseJson(ctx.domain, c.req.param("repo"), release), JSON_TYPE, cors);84		});85	}86	for (const [path, cors] of [87		["/api/:repo/releases/history", true],88		["/:repo/releases/history.json", false],89	] as const) {90		app.get(path, (c) => {91			const f = feed(c.req.param("repo"));92			if (!f) return send(c, 404, REPO_NOT_FOUND);93			if (f.releases.length === 0) return send(c, 404, NO_RELEASES);94			return send(c, 200, historyJson(f.releases), JSON_TYPE, cors);95		});96	}97	app.get("/:repo/releases/latest", (c) => {98		const repo = openPublicRepo(ctx, c.req.param("repo"));99		if (!repo) return notFound(c, ctx);100		return c.redirect(`/${repo.settings.name}/releases`, 302);101	});102	app.get("/:repo/releases/latest/version.txt", (c) => {103		const release = newest(c, c.req.param("repo"));104		return release instanceof Response ? release : send(c, 200, release.version, TEXT_TYPE);105	});106	app.get("/:repo/releases/:version/checksums.txt", (c) => {107		const name = c.req.param("repo");108		const f = feed(name);109		if (!f) return send(c, 404, REPO_NOT_FOUND);110		const release = f.releases.find((r) => r.version === c.req.param("version"));111		if (!release) return send(c, 404, RELEASE_NOT_FOUND);112		return send(c, 200, checksumsTxt(name, release), TEXT_TYPE);113	});114	// §22.1: the newest file for that platform, from the newest release that has one.115	app.get("/:repo/releases/latest/:platform", (c) => {116		const name = c.req.param("repo");117		const f = feed(name);118		if (!f) return send(c, 404, REPO_NOT_FOUND);119		const platform = c.req.param("platform");120		for (const release of f.releases) {121			const file = release.files.find((x) => x.platform === platform);122			if (file) return download(c, name, release.version, file);123		}124		return send(c, 404, RELEASE_NOT_FOUND);125	});126	app.get("/:repo/releases/:version/:platform", (c) => {127		const version = c.req.param("version");128		if (!isVersion(version)) return send(c, 404, RELEASE_NOT_FOUND);129		return downloadAt(c, c.req.param("repo"), version, c.req.param("platform"));130	});131	app.get("/:repo/update-info", (c) => {132		const name = c.req.param("repo");133		const f = feed(name);134		if (!f) return send(c, 404, REPO_NOT_FOUND);135		// Head is the newest save on the line; checkpoints are never shown (§21.4). Its version is the136		// release built from it, else its ID, as in Kepr 2.137		const save = history(f.repo.mirror, 1, 0)[0];138		const saveHead = save && {139			id: save.id,140			version: f.releases.find((r) => r.save === save.id)?.version ?? save.id,141			createdAt: save.created_at,142		};143		return send(c, 200, updateInfoJson(ctx.domain, name, saveHead ?? null, f.releases[0]));144	});145	app.get("/:repo/meta", (c) => {146		const f = feed(c.req.param("repo"));147		if (!f) return send(c, 404, REPO_NOT_FOUND);148		const { name, category } = f.repo.settings;149		const stats = repoStats(f.repo.mirror);150		const meta = {151			repo: name,152			saveCount: stats.saves,153			lineCount: stats.lines,154			lastSaveAt: stats.updated ?? 0,155			latestRelease: f.releases[0]?.version ?? null,156			description: readmeIntro(name, readmeAtHead(f.repo))?.text ?? null,157			language: stats.language || null,158			category: category || null,159		};160		return send(c, 200, metaJson(meta));161	});162}