You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:10:13