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";16171819202122const 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";2627const 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 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 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 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 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 136 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}