| 10 | } |
| 11 | |
| 12 | export async function downloadFile( |
| 13 | url: string, |
| 14 | destPath: string, |
| 15 | opts?: DownloadOpts, |
| 16 | ): Promise<{ size: number }> { |
| 17 | // Fix: Alibaba Cloud OSS US East blocks HTTP from certain regions. |
| 18 | // Force HTTPS to ensure reliable downloads. |
| 19 | const downloadUrl = url.startsWith('http://') ? url.replace('http://', 'https://') : url; |
| 20 | const maxRetries = opts?.retries ?? 3; |
| 21 | const baseDelay = opts?.retryDelayMs ?? 1000; |
| 22 | let lastError: Error | undefined; |
| 23 | |
| 24 | for (let attempt = 0; attempt <= maxRetries; attempt++) { |
| 25 | try { |
| 26 | if (attempt > 0) { |
| 27 | const delay = baseDelay * Math.pow(2, attempt - 1); |
| 28 | if (!opts?.quiet) { |
| 29 | process.stderr.write(`\n Retry ${attempt}/${maxRetries} in ${delay}ms...\n`); |
| 30 | } |
| 31 | await new Promise(r => setTimeout(r, delay)); |
| 32 | } |
| 33 | |
| 34 | const res = await fetch(downloadUrl); |
| 35 | |
| 36 | if (!res.ok) { |
| 37 | throw new CLIError(`Download failed: HTTP ${res.status}`, ExitCode.GENERAL); |
| 38 | } |
| 39 | |
| 40 | const contentLength = Number(res.headers.get('content-length') || 0); |
| 41 | const reader = res.body?.getReader(); |
| 42 | if (!reader) throw new CLIError('No response body', ExitCode.GENERAL); |
| 43 | |
| 44 | const tmpPath = `${destPath}.tmp-${process.pid}-${Date.now()}-${attempt}-${Math.random().toString(36).slice(2)}`; |
| 45 | const writer = createWriteStream(tmpPath); |
| 46 | const progress = contentLength > 0 && !opts?.quiet |
| 47 | ? createProgressBar(contentLength, 'Downloading') |
| 48 | : null; |
| 49 | |
| 50 | let received = 0; |
| 51 | let readComplete = false; |
| 52 | let completed = false; |
| 53 | |
| 54 | try { |
| 55 | const writeError = new Promise<never>((_, reject) => { |
| 56 | writer.on('error', reject); |
| 57 | }); |
| 58 | |
| 59 | while (true) { |
| 60 | const { done, value } = await Promise.race([ |
| 61 | reader.read(), |
| 62 | writeError, |
| 63 | ]) as ReadableStreamReadResult<Uint8Array>; |
| 64 | if (done) break; |
| 65 | |
| 66 | const ok = writer.write(value); |
| 67 | if (!ok) await new Promise(r => writer.once('drain', r)); |
| 68 | |
| 69 | received += value.byteLength; |