// ============================================================ // download.ts — 文件下载工具函数 // // 职责: // - 通过 Electron net 模块下载文件到磁盘 // - 支持重定向、超时、自定义请求头 // // 设计: // - net 基于 Chromium 网络栈,自动携带浏览器标准头,不受 CORS 限制 // - 不能用 netRequest(它 chunk.toString() 会损坏二进制数据) // - 不能用 Node.js https(缺少浏览器头导致 CDN 返回 400) // ============================================================ import { net } from 'electron'; import fs from 'node:fs'; import path from 'node:path'; import { ensureDir } from './path'; /** * 通过 Electron net 模块下载文件到磁盘。 * * @param url - 下载地址 * @param destPath - 目标文件路径 * @param headers - 可选的自定义请求头 */ export function downloadToFile( url: string, destPath: string, headers?: Record, ): Promise { return new Promise((resolve, reject) => { const req = net.request({ method: 'GET', url }); // 自定义请求头 if (headers) { for (const [key, value] of Object.entries(headers)) { req.setHeader(key, value); } } // 超时 120s let timedOut = false; const timer = setTimeout(() => { timedOut = true; req.abort(); reject(new Error('下载超时(120s)')); }, 120_000); req.on('response', (response) => { clearTimeout(timer); const status = response.statusCode; // 3xx 重定向 if (status >= 300 && status < 400) { const location = response.headers['location']?.[0]; if (location && location !== url) { downloadToFile(location, destPath, headers).then(resolve).catch(reject); } else { reject(new Error('重定向循环或缺少 Location 头')); } response.on('data', () => {}); // 消费响应体 return; } if (status >= 400) { reject(new Error(`HTTP ${status}`)); response.on('data', () => {}); return; } // 收集二进制 chunks → 写入文件 const chunks: Buffer[] = []; response.on('data', (chunk: Buffer) => chunks.push(chunk)); response.on('end', () => { try { ensureDir(path.dirname(destPath)); const buffer = Buffer.concat(chunks); fs.writeFileSync(destPath, buffer); resolve(); } catch (err) { reject(err); } }); response.on('error', (err) => { reject(new Error(`响应读取失败: ${err.message}`)); }); }); req.on('error', (err) => { clearTimeout(timer); if (timedOut) return; reject(new Error(`网络请求失败: ${err.message}`)); }); req.end(); }); }