Node.js断点续传下载问题:如何从中断点恢复文件下载?
解决Node.js断点续传下载的问题
嘿,我看了你遇到的问题——明明传了已下载的字节数给transferred,但下载还是从头开始,这是因为你没告诉服务器要从断点位置继续下载,服务器默认还是会返回整个文件,自然就覆盖(或者重新从头写)之前的内容了。
要实现断点续传,核心是利用HTTP的Range请求头,让服务器返回文件的某一部分。下面是具体的解决步骤和修改后的代码:
关键要点说明
- 发送Range请求:在axios的请求配置里添加
headers: { Range:bytes=${transferred}-},告诉服务器“我已经下载了前transferred字节,给我剩下的部分”。 - 追加写入文件:创建写流的时候要设置
flags: 'a',这样新下载的内容会追加到已有文件末尾,而不是覆盖。 - 检查服务器支持情况:先通过HEAD请求确认服务器是否支持
Range(响应头包含Accept-Ranges: bytes),如果不支持,只能重新下载。 - 处理中断重试:监听下载过程中的错误(比如连接断开),捕获后重新调用下载函数,传入当前已下载的文件大小。
修改后的完整代码
const axios = require('axios'); const fs = require('fs'); const progress = require('progress-stream'); // 先获取文件的总大小(如果没传的话,通过HEAD请求获取) const getFileTotalSize = async (link) => { try { const response = await axios.head(link); return parseInt(response.headers['content-length'], 10); } catch (err) { throw new Error('无法获取文件大小:' + err.message); } }; // 获取本地已下载的文件大小 const getLocalFileSize = (localPath) => { try { const stats = fs.statSync(localPath); return stats.size; } catch (err) { // 文件不存在的话返回0 return 0; } }; const download = async (link, localPath) => { let totalSize = await getFileTotalSize(link); let transferred = getLocalFileSize(localPath); // 如果已经下载完成,直接返回 if (transferred >= totalSize) { console.log('文件已经下载完成'); return; } // 检查服务器是否支持Range请求 const headResponse = await axios.head(link); const acceptRanges = headResponse.headers['accept-ranges']; if (acceptRanges !== 'bytes') { console.warn('服务器不支持断点续传,将重新下载整个文件'); transferred = 0; } return new Promise((resolve, reject) => { const axiosConfig = { url: link, method: 'GET', responseType: 'stream', headers: transferred > 0 ? { Range: `bytes=${transferred}-` } : {} }; axios(axiosConfig) .then(response => { // 服务器返回206表示成功返回部分内容,200是返回整个文件 if (response.status !== 206 && response.status !== 200) { return reject(new Error(`服务器返回异常状态码:${response.status}`)); } // 创建追加模式的写流 const writer = fs.createWriteStream(localPath, { flags: 'a' }); // 配置进度流,注意length要减去已下载的部分(如果是续传的话) const str = progress({ time: 100, length: totalSize - transferred, transferred: 0 }); str.on('progress', (progressData) => { // 计算整体进度 const overallProgress = ((transferred + progressData.transferred) / totalSize) * 100; console.log(`下载进度:${overallProgress.toFixed(2)}%,已下载:${(transferred + progressData.transferred)/1024/1024}MB / ${totalSize/1024/1024}MB`); }); // 处理流的错误 const handleError = (err) => { console.error(`下载中断:${err.message},准备重试...`); writer.close(); // 递归调用下载函数,继续续传 download(link, localPath).then(resolve).catch(reject); }; response.data .pipe(str) .pipe(writer) .on('error', handleError); writer.on('finish', () => { console.log('下载完成!'); resolve(); }); response.data.on('error', handleError); }) .catch(err => { console.error(`请求失败:${err.message}`); // 重试间隔1秒,避免频繁请求 setTimeout(() => { download(link, localPath).then(resolve).catch(reject); }, 1000); }); }); }; // 使用示例 // download('https://example.com/large-file.zip', './local-file.zip').catch(console.error);
代码解释
getFileTotalSize:通过HEAD请求获取文件的总大小,避免GET请求下载整个文件来获取大小。getLocalFileSize:检查本地文件的大小,作为续传的起始位置。Range请求头:当transferred > 0时添加,告诉服务器返回从transferred字节开始的内容。- 追加写流:
flags: 'a'确保新内容不会覆盖已下载的部分。 - 错误重试:捕获流错误和请求错误,自动重新调用下载函数,实现断连后自动续传。
- 进度计算:把已下载的字节数和当前流的进度结合,显示整体的下载进度。
这样修改后,当连接断开重新运行脚本,或者脚本自动重试时,都会从之前的断点位置继续下载,不会从头开始了。
内容的提问来源于stack exchange,提问作者Conde
相关产品推荐
相关产品推荐

