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>
This commit is contained in:
3 files changed
+116
-11
No files matched your search
@@ -180,7 +180,7 @@ tests/scripts/windows-installer.sh 假应用的安装程序:所选目录原
|
||||
tests/scripts/types.sh packaging/types.tsv 的文件类型在 Linux 桌面项、Windows 安装程序与 macOS 文档类型里一致,从不设为默认
|
||||
tests/scripts/layout.sh 真窗口:六个应用在 640、800、1100 宽下无元素越出窗口左右,留截图(需辅助功能与屏幕录制权限)
|
||||
tests/scripts/ooxml.py <用例> <文件> 不经 LibreOffice 读 DOCX、XLSX、PPTX 并按自算的预期核对(往返用例调用)
|
||||
cd site && pnpm deploy 官网构建并经 wrangler 发到 Cloudflare Workers(静态资源,另有 `site/worker` 只答 `/release.json`:GitHub API 的最新版本与全部发布的下载总数(不计更新清单 `.json`),缓存 5 分钟;下载链接指向 `releases/latest`),地址 https://ely-office.zhangyanghaha0407.workers.dev
|
||||
cd site && pnpm deploy 官网构建并经 wrangler 发到 Cloudflare Workers(静态资源,另有 `site/worker` 只答 `/release.json`:GitHub API 的最新版本与全部发布的下载总数,加上 Gitea 上迁移前的下载(Gitea 不通时只计 GitHub;都不计更新清单 `.json`),缓存 5 分钟;下载链接指向 `releases/latest`),地址 https://ely-office.zhangyanghaha0407.workers.dev
|
||||
cd site && pnpm test && pnpm e2e 官网单测(几何、道路连通)与 Playwright 端到端(悬停、聚焦、按压、命中归属)
|
||||
cd site/video/scenes && npm run verify 发布视频:群像与离场两个渲染器在切场处屏幕角点一致,离场全程各屏播自己的录屏帧(真页面渲染)
|
||||
cargo check --workspace
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { afterEach, test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import worker from "../worker/index.ts";
|
||||
|
||||
const real = globalThis.fetch;
|
||||
afterEach(() => {
|
||||
globalThis.fetch = real;
|
||||
});
|
||||
|
||||
const github = {
|
||||
tag_name: "v0.1.9",
|
||||
assets: [
|
||||
{ name: "ElyDoc-0.1.9-arm64.dmg", download_count: 3 },
|
||||
{ name: "ely-doc.json", download_count: 50 },
|
||||
],
|
||||
};
|
||||
const gitea = [
|
||||
{ tag_name: "v0.1.8", assets: [{ name: "ElyDoc-0.1.8-arm64.dmg", download_count: 40 }, { name: "ely-doc.json", download_count: 900 }] },
|
||||
{ tag_name: "v0.1.7", assets: [{ name: "ElyDoc-0.1.7-arm64.dmg", download_count: 7 }] },
|
||||
];
|
||||
|
||||
/** Answers GitHub with `github` and Gitea with `old`. */
|
||||
function hosts(old: () => Response) {
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url.startsWith("https://git.zacharyzhang.com/")) return old();
|
||||
if (url.endsWith("/releases/latest")) return Response.json(github);
|
||||
return Response.json([github]);
|
||||
}) as typeof fetch;
|
||||
}
|
||||
|
||||
async function answer() {
|
||||
const warn = console.warn;
|
||||
console.warn = () => {};
|
||||
try {
|
||||
const res = await worker.fetch(new Request("https://office.elygpui.com/release.json"), {});
|
||||
assert.equal(res.status, 200);
|
||||
return await res.json();
|
||||
} finally {
|
||||
console.warn = warn;
|
||||
}
|
||||
}
|
||||
|
||||
test("downloads add Gitea's history to GitHub's, without manifests", async () => {
|
||||
hosts(() => Response.json(gitea));
|
||||
assert.deepEqual(await answer(), { version: "0.1.9", downloads: 3 + 40 + 7 });
|
||||
});
|
||||
|
||||
test("an unreachable, failing or odd Gitea leaves GitHub's count", async () => {
|
||||
const cases: (() => Response)[] = [
|
||||
() => {
|
||||
throw new TypeError("fetch failed");
|
||||
},
|
||||
() => new Response("down", { status: 502 }),
|
||||
() => new Response("<html>", { status: 200 }),
|
||||
() => Response.json({ message: "not found" }),
|
||||
() => Response.json([{ tag_name: "v0.1.8", assets: [{ name: "ElyDoc.dmg" }] }]),
|
||||
];
|
||||
for (const old of cases) {
|
||||
hosts(old);
|
||||
assert.deepEqual(await answer(), { version: "0.1.9", downloads: 3 });
|
||||
}
|
||||
});
|
||||
+52
-10
@@ -1,6 +1,11 @@
|
||||
// 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 };
|
||||
@@ -8,6 +13,8 @@ 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",
|
||||
@@ -15,12 +22,23 @@ async function read<T>(env: Env, path: string): Promise<T> {
|
||||
"x-github-api-version": "2022-11-28",
|
||||
};
|
||||
if (env.GITHUB_TOKEN) headers.authorization = `Bearer ${env.GITHUB_TOKEN}`;
|
||||
const init: Cached = { headers, cf: { cacheTtl: 300, cacheEverything: true } };
|
||||
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");
|
||||
@@ -28,26 +46,50 @@ async function version(env: Env): Promise<string> {
|
||||
return tag.slice(1);
|
||||
}
|
||||
|
||||
/** Downloads of every release; update manifests are checks, not downloads. */
|
||||
/** 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}`);
|
||||
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;
|
||||
}
|
||||
}
|
||||
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] = await Promise.all([version(env), downloads(env)]);
|
||||
return Response.json({ version: v, downloads: n }, { headers: { "cache-control": "public, max-age=300" } });
|
||||
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 });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user