From 9ba97b7e2923714ba93b3b5e11b5c554b8184d56 Mon Sep 17 00:00:00 2001 From: CRT Date: Sat, 17 Oct 2026 15:59:44 +0800 Subject: [PATCH] release: candidate downloads go through Cloudflare first (the direct route to the origin was throttled per connection: one AppImage took 20+ min there, 3 MB/s through Cloudflare), the origin only as a fallback; a heartbeat logs the bytes every 30 s --- tools/forgejo-release.ts | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/tools/forgejo-release.ts b/tools/forgejo-release.ts index 352e376..b6c126f 100644 --- a/tools/forgejo-release.ts +++ b/tools/forgejo-release.ts @@ -162,15 +162,32 @@ export async function api(method: string, path: string, body?: unknown, raw = } } -/** Download one asset (drafts need the token) to a file; through the origin like uploads when it is set. */ +/** Download one asset (drafts need the token) to a file. Through Cloudflare first: its 100 MB limit is on request + * bodies (uploads), and the direct route to the origin was throttled per connection to ~30 KB/s (an 0.8.11 release + * sat 20 minutes on one AppImage while Cloudflare gave 3 MB/s). The origin is the fallback. A heartbeat logs the + * bytes every 30 s; curl's own --max-time bounds each try. */ export async function downloadAsset(url: string, dest: string): Promise { const ip = origin(); const dir = mkdtempSync(join(tmpdir(), 'forgejo-dl-')); try { writeFileSync(join(dir, 'h'), `Authorization: ${auth()}\n`, { mode: 0o600 }); const host = new URL(BASE).host; - const r = spawnSync('curl', ['-sS', '-L', '--fail', '--max-time', '1800', ...(ip ? ['--resolve', `${host}:443:${ip.includes(':') ? `[${ip}]` : ip}`] : []), '-H', `@${join(dir, 'h')}`, '-o', dest, url], { encoding: 'utf8' }); - if (r.status !== 0) fail(`download ${basename(dest)}: curl exit ${r.status}: ${r.stderr.slice(0, 300)}`); + const routes: string[][] = [[], ...(ip ? [['--resolve', `${host}:443:${ip.includes(':') ? `[${ip}]` : ip}`]] : [])]; + let last = ''; + for (const [i, route] of routes.entries()) { + const t0 = Date.now(); + const p = Bun.spawn(['curl', '-sS', '-L', '--fail', '--max-time', '1800', ...route, '-H', `@${join(dir, 'h')}`, '-o', dest, url], { stdout: 'ignore', stderr: 'pipe' }); + const beat = setInterval(() => { + let n = 0; try { n = statSync(dest).size; } catch { /* not started */ } + console.log(` download ${basename(dest)} (${i ? 'origin' : 'cloudflare'}): ${(n / 1e6).toFixed(1)} MB after ${Math.round((Date.now() - t0) / 1000)} s`); + }, 30_000); + const code = await p.exited; + clearInterval(beat); + if (code === 0) return; + last = `curl exit ${code}: ${(await new Response(p.stderr).text()).slice(0, 300)}`; + if (i + 1 < routes.length) console.log(` download ${basename(dest)}: ${last}; trying the origin`); + } + fail(`download ${basename(dest)}: ${last}`); } finally { rmSync(dir, { recursive: true, force: true }); } }