Files
ElyOffice/site/worker/index.ts
T
ZacharyZhang-NYandClaude 0e0415fdbe Site: the download count adds Gitea's history to GitHub's
Gitea holds the downloads made before the move, so the worker sums
both hosts' asset download counts (update manifests excluded). Gitea is
read with the same five-minute cache and a five-second limit; when it
is down or answers oddly, the count is GitHub's alone. Downloads stay
on GitHub only.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-19 19:37:59 -04:00

98 lines
3.6 KiB
TypeScript

// 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<T>(env: Env, path: string): Promise<T> {
const headers: Record<string, string> = {
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<T>(path: string): Promise<T> {
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<string> {
const { tag_name: tag } = await read<Release>(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<number> {
let total = 0;
for (let page = 1; ; page++) {
const releases = await read<Release[]>(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<number> {
try {
let total = 0;
for (let page = 1; ; page++) {
const releases = await readGitea<Release[]>(`/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<Response> {
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 });
}
},
};