// Answers /release.json with the newest release and downloads. const REPO = "https://api.github.com/repos/ELY-Open-Source/ElyOffice"; // The old home keeps the downloads made before the move. const GITEA = "https://git.zacharyzhang.com/api/v1/repos/ZacharyZhang-NY/ElyOffice"; const PAGE = 100; const GITEA_PAGE = 50; // A slow Gitea must not hold up the page. const GITEA_WAIT = 5000; type Cached = RequestInit & { cf: { cacheTtl: number; cacheEverything: boolean } }; type Asset = { name: string; download_count: number }; type Release = { tag_name: string; assets: Asset[] }; // A token lifts GitHub's limit for the shared egress addresses: `wrangler secret put GITHUB_TOKEN`. type Env = { GITHUB_TOKEN?: string }; const cf = { cacheTtl: 300, cacheEverything: true }; async function read(env: Env, path: string): Promise { const headers: Record = { accept: "application/vnd.github+json", "user-agent": "ely-office-site", "x-github-api-version": "2022-11-28", }; if (env.GITHUB_TOKEN) headers.authorization = `Bearer ${env.GITHUB_TOKEN}`; const init: Cached = { headers, cf }; const res = await fetch(`${REPO}${path}`, init); if (!res.ok) throw new Error(`GitHub answered ${res.status} for ${path}`); return (await res.json()) as T; } async function readGitea(path: string): Promise { const init: Cached = { headers: { accept: "application/json", "user-agent": "ely-office-site" }, cf, signal: AbortSignal.timeout(GITEA_WAIT), }; const res = await fetch(`${GITEA}${path}`, init); if (!res.ok) throw new Error(`Gitea answered ${res.status} for ${path}`); return (await res.json()) as T; } /** The newest release's version, without the v. */ async function version(env: Env): Promise { const { tag_name: tag } = await read(env, "/releases/latest"); if (!/^v\d+\.\d+\.\d+$/.test(tag)) throw new Error(`GitHub's latest tag is ${JSON.stringify(tag)}`); return tag.slice(1); } /** Downloads of `releases`; update manifests are checks, not downloads. */ function counted(releases: Release[]): number { let total = 0; for (const { assets } of releases) { for (const a of assets) { if (!Number.isInteger(a.download_count)) throw new Error(`${a.name} has no download count`); if (!a.name.endsWith(".json")) total += a.download_count; } } return total; } /** Downloads of every GitHub release. */ async function downloads(env: Env): Promise { let total = 0; for (let page = 1; ; page++) { const releases = await read(env, `/releases?per_page=${PAGE}&page=${page}`); total += counted(releases); if (releases.length < PAGE) return total; } } /** Downloads of every Gitea release; 0 when Gitea is down, slow or odd. */ async function giteaDownloads(): Promise { try { let total = 0; for (let page = 1; ; page++) { const releases = await readGitea(`/releases?limit=${GITEA_PAGE}&page=${page}`); if (!Array.isArray(releases)) throw new Error("Gitea listed no releases"); total += counted(releases); if (releases.length < GITEA_PAGE) return total; } } catch (e) { // The page shows GitHub's count alone. console.warn(`no Gitea downloads: ${e}`); return 0; } } export default { async fetch(_request: Request, env: Env): Promise { try { const [v, n, old] = await Promise.all([version(env), downloads(env), giteaDownloads()]); return Response.json({ version: v, downloads: n + old }, { headers: { "cache-control": "public, max-age=300" } }); } catch (e) { return new Response(String(e), { status: 502 }); } }, };