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

基于Node.js Express的支持浏览器下载管理器断点续传的大文件下载服务

基于Node.js Express的大文件下载服务实现与完善

需求概述

  • 基于Node.js Express开发下载服务,支持最大20GB的大文件传输
  • 用户访问https://my-url.com/api/download即可触发下载,需兼容浏览器内置下载管理器的暂停、恢复、取消操作:
    • 暂停:服务器停止发送文件数据
    • 恢复:从上次中断的进度位置继续传输
    • 取消:服务器立即终止下载流程
  • 支持多用户同时下载,参考Ubuntu桌面版下载的实现逻辑

现有代码的问题分析

原代码的核心逻辑已具备Range请求处理能力,但存在以下影响稳定性与兼容性的问题:

  1. 同步IO阻塞:使用fs.statSync()查询大文件状态会阻塞Event Loop,降低并发处理能力
  2. 错误处理缺失:未处理文件不存在、非法Range请求、文件读取失败等异常场景
  3. 进度计算错误:用当前请求的已发送字节除以整个文件大小计算进度,逻辑不符合实际
  4. 响应头不完整:缺少Content-Disposition头,浏览器可能不会触发下载弹窗;未处理非Range请求的200状态码
  5. 流处理不严谨:手动调用res.write(chunk)未处理背压,大文件传输时可能导致内存溢出
  6. Range参数未校验:未验证start/end值的合法性,可能引发无效的文件读取

完善后的代码

const express = require('express');
const router = express.Router();
const path = require('path');
const fs = require('fs').promises;
const { createReadStream } = require('fs');

router.get('/api/download', async (req, res) => {
    const rootPath = path.join(__dirname, '../', 'download');
    const filePath = path.join(rootPath, 'big-file.zip');
    let fileStat;

    // 异步获取文件信息,避免阻塞Event Loop
    try {
        fileStat = await fs.stat(filePath);
    } catch (err) {
        if (err.code === 'ENOENT') {
            return res.status(404).send('文件不存在');
        }
        return res.status(500).send('服务器内部错误');
    }

    const fileSize = fileStat.size;
    let start = 0;
    let end = fileSize - 1;

    // 处理Range请求并校验参数合法性
    const range = req.headers.range;
    if (range) {
        const parts = range.replace(/bytes=/, '').split('-');
        start = parseInt(parts[0], 10);
        
        if (isNaN(start) || start >= fileSize) {
            return res.status(416).send('请求的范围超出文件大小');
        }
        
        end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
        end = Math.min(end, fileSize - 1);
    }

    const chunkSize = end - start + 1;
    const headers = {
        'Accept-Ranges': 'bytes',
        'Content-Length': chunkSize,
        'Content-Type': 'application/zip',
        // 触发浏览器下载弹窗,指定文件名
        'Content-Disposition': 'attachment; filename="big-file.zip"'
    };

    // 根据请求类型设置响应状态码
    if (range) {
        headers['Content-Range'] = `bytes ${start}-${end}/${fileSize}`;
        res.writeHead(206, headers);
    } else {
        res.writeHead(200, headers);
    }

    // 创建文件可读流
    const fileStream = createReadStream(filePath, { start, end });

    // 处理文件读取错误
    fileStream.on('error', (err) => {
        console.error('文件读取异常:', err);
        res.status(500).end();
    });

    // 使用pipe自动处理背压,替代手动write
    fileStream.pipe(res);

    // 监听请求关闭事件,终止文件流(对应暂停/取消操作)
    req.on('close', () => {
        console.log('下载已暂停或取消,终止文件流');
        fileStream.destroy();
    });

    // 监听流结束事件
    fileStream.on('end', () => {
        console.log('文件传输完成');
        res.end();
    });
});

module.exports = router;

核心改进说明

  • 异步IO优化:使用fs.promises.stat()替代同步方法,避免阻塞Event Loop,提升多用户并发处理能力
  • 完整错误处理:覆盖文件不存在、非法Range请求、文件读取失败等场景,返回标准HTTP状态码
  • 规范响应头:添加Content-Disposition确保浏览器触发下载弹窗;区分Range请求(206)与普通请求(200)的状态码
  • 流稳定性优化:用pipe()处理文件流,自动管理背压,防止大文件传输时内存溢出
  • 参数合法性校验:严格验证Range请求的start/end值,避免无效的文件读取操作
  • 兼容浏览器操作:通过监听req.close事件,在用户暂停/取消下载时立即终止文件流,符合浏览器下载管理器的交互逻辑

内容的提问来源于stack exchange,提问作者fw1983

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 17:32:47