Electron下载文件时如何校验完整性避免损坏且节省用户流量?
现有代码的核心问题
- 采用
fetch + blob全量加载模式,文件整体存入内存后再写入磁盘,200MB以上文件会占用过高内存,网络中断时所有已下载内容直接作废,必须重下整个文件 - 路径拼接硬编码Windows反斜杠,无法兼容macOS、Linux系统
- 同步读写API会阻塞渲染进程,界面可能出现卡顿
大文件下载优化方案
1. 流式下载 + 断点续传(核心解决流量浪费问题)
放弃fetch全量下载逻辑,改用Node.js https 模块实现流式下载,边下载边写入磁盘,无需占用全量内存。同时支持断点续传,网络中断后只需下载未完成的部分:
- 先发起HEAD请求获取文件总大小,校验服务端是否支持
Range断点续传(响应头包含Accept-Ranges: bytes) - 每次下载前先读取本地已下载的临时文件大小,请求时携带
Range: bytes=已下载大小-头,从断点位置继续下载 - 临时文件下载完成后再重命名为正式文件名,避免中途退出产生损坏的正式文件
- 如果你不想自己实现完整的下载逻辑,也可以直接调用Electron内置的
session.downloadAPI,原生支持断点续传、进度监听、取消暂停等能力,稳定性更高
2. 完整性校验方案
- 单文件MD5校验完全能覆盖普通下载损坏的场景,可靠性足够。建议让服务端将对应文件的MD5值放在响应头的自定义字段(例如
X-File-MD5)中,下载完成后本地计算文件MD5与响应头值比对即可,无需额外请求接口 - 若需要进一步节省重传流量,可以要求服务端按固定块大小(例如10MB/块)计算每块的哈希值并返回哈希列表,你每下载完一个块就校验对应哈希,仅重传校验失败的块即可,不需要重下整个文件
优化后的核心代码示例
const https = require('https'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); async function downloadFile(downloadURL, downloadDir, filename) { // 跨平台拼接路径 const fullPath = path.join(downloadDir, filename); const tempPath = fullPath + '.tmp'; // 创建目录 if (!fs.existsSync(downloadDir)) { fs.mkdirSync(downloadDir, { recursive: true }); } // 读取已下载的临时文件大小 let downloadedSize = 0; if (fs.existsSync(tempPath)) { downloadedSize = fs.statSync(tempPath).size; } return new Promise((resolve, reject) => { const options = new URL(downloadURL); // 断点续传头 options.headers = downloadedSize > 0 ? { Range: `bytes=${downloadedSize}-` } : {}; https.get(options, (res) => { // 206是断点续传成功响应码,200是全量下载成功响应码 if (res.statusCode !== 200 && res.statusCode !== 206) { return reject(new Error(`下载失败,响应码:${res.statusCode}`)); } // 读取服务端返回的文件MD5 const serverMD5 = res.headers['x-file-md5']; // 以追加模式写入临时文件 const writeStream = fs.createWriteStream(tempPath, { flags: downloadedSize > 0 ? 'a' : 'w' }); const hash = crypto.createHash('md5'); res.pipe(writeStream); // 边下载边计算MD5 res.on('data', (chunk) => hash.update(chunk)); writeStream.on('finish', () => { writeStream.close(); // 校验文件完整性 const localMD5 = hash.digest('hex'); if (serverMD5 && localMD5 !== serverMD5) { fs.unlinkSync(tempPath); return reject(new Error('文件校验失败,已删除损坏文件')); } // 校验通过后重命名为正式文件 fs.renameSync(tempPath, fullPath); resolve(fullPath); }); writeStream.on('error', (err) => { writeStream.close(); reject(err); }); }).on('error', reject); }); } // 调用示例 downloadFile("https://sample.com/test.zip", "SOME_DIRECTORY_HERE", "test.zip") .then(() => console.log("下载完成")) .catch(console.error);
内容的提问来源于stack exchange,提问作者Polar
相关产品推荐
相关产品推荐

