releases.ts

3.1 KB · raw

1import type { Database } from "bun:sqlite";2import type { ReleaseFile } from "../bucket/repo.ts";3import { releaseRecords } from "../mirror/queries.ts";45/** A release as the public sees it: its notes are the operator's override when one is set (§24). */6export type Release = {7	version: string;8	save: string;9	publishedAt: number;10	notes: string;11	files: ReleaseFile[];12};1314type ReleaseSetting = { version: string; hidden: number; notes: string | null };1516/**17 * A repo's releases without the hidden ones, newest first by numeric dot-separated comparison (the18 * rule `koh update` uses, §12). Files are ordered by platform name, as Kepr 2 listed them.19 */20export function visibleReleases(kepr: Database, mirror: Database, repo: string): Release[] {21	const settings = new Map(22		kepr23			.query<ReleaseSetting, [string]>(24				"SELECT version, hidden, notes FROM release_settings WHERE repo = ?",25			)26			.all(repo)27			.map((row) => [row.version, row]),28	);29	return releaseRecords(mirror)30		.filter((record) => settings.get(record.version)?.hidden !== 1)31		.map((record) => ({32			version: record.version,33			save: record.save,34			publishedAt: record.published_at,35			notes: settings.get(record.version)?.notes ?? record.notes,36			files: record.files.toSorted((a, b) => (a.platform < b.platform ? -1 : 1)),37		}))38		.sort((a, b) => compareVersions(b.version, a.version));39}4041export function compareVersions(a: string, b: string): number {42	const left = a.split(".").map((part) => Number.parseInt(part, 10) || 0);43	const right = b.split(".").map((part) => Number.parseInt(part, 10) || 0);44	for (let i = 0; i < Math.max(left.length, right.length); i++) {45		const diff = (left[i] ?? 0) - (right[i] ?? 0);46		if (diff !== 0) return diff;47	}48	// Numerically equal (`1.0` and `1.0.0`): compare the text, so the order is still total.49	return a < b ? -1 : a > b ? 1 : 0;50}5152/** §22.2's content types for a release file. */53export function releaseContentType(name: string): string {54	const lower = name.toLowerCase();55	if (lower.endsWith(".apk")) return "application/vnd.android.package-archive";56	if (lower.endsWith(".zip")) return "application/zip";57	return "application/octet-stream";58}5960export function countDownload(61	kepr: Database,62	repo: string,63	version: string,64	platform: string,65): void {66	kepr67		.query(68			`INSERT INTO downloads (repo, version, platform, count) VALUES (?, ?, ?, 1)69			ON CONFLICT DO UPDATE SET count = count + 1`,70		)71		.run(repo, version, platform);72}7374/** A Nix tarball fetch (§22.2). */75export function countInstall(kepr: Database, repo: string): void {76	kepr77		.query(78			"INSERT INTO installs (repo, count) VALUES (?, 1) ON CONFLICT DO UPDATE SET count = count + 1",79		)80		.run(repo);81}8283/** Download counts of a repo's files, keyed `<version>/<platform>`. */84export function downloadCounts(kepr: Database, repo: string): Map<string, number> {85	const rows = kepr86		.query<{ version: string; platform: string; count: number }, [string]>(87			"SELECT version, platform, count FROM downloads WHERE repo = ?",88		)89		.all(repo);90	return new Map(rows.map((row) => [`${row.version}/${row.platform}`, row.count]));91}