Express文件下载:大文件下载和断点续传

Express文件下载:大文件下载和断点续传

【免费下载链接】express Fast, unopinionated, minimalist web framework for node. 【免费下载链接】express 项目地址: https://gitcode.com/GitHub_Trending/ex/express

痛点场景:为什么需要专业的文件下载方案?

在日常Web开发中,文件下载看似简单,但当遇到大文件、网络不稳定、需要断点续传等复杂场景时,简单的下载方案往往力不从心。你是否遇到过:

  • 大文件下载中途失败,需要重新开始?
  • 用户网络不稳定,下载体验差?
  • 需要支持多线程下载提升速度?
  • 服务器带宽被大文件下载占满?

Express框架提供了强大的文件下载能力,本文将深入解析如何实现专业级的大文件下载和断点续传功能。

Express基础下载功能

1. 基本文件下载

Express通过res.download()方法提供简单的文件下载功能:

const express = require('express');
const path = require('path');
const app = express();

// 基本文件下载
app.get('/download/simple', (req, res) => {
  const filePath = path.join(__dirname, 'files', 'document.pdf');
  res.download(filePath);
});

// 自定义文件名下载
app.get('/download/custom-name', (req, res) => {
  const filePath = path.join(__dirname, 'files', 'report.pdf');
  res.download(filePath, '季度报告.pdf');
});

// 带选项的下载
app.get('/download/with-options', (req, res) => {
  const filePath = path.join(__dirname, 'files', 'large-file.zip');
  res.download(filePath, {
    maxAge: '1h', // 缓存1小时
    headers: {
      'X-File-Type': 'archive'
    }
  });
});

2. 下载功能核心原理

Express的下载功能基于send模块实现,支持以下特性:

mermaid

大文件下载优化策略

1. 分块传输(Chunked Transfer)

对于大文件,分块传输是必须的:

app.get('/download/large-file', (req, res) => {
  const filePath = path.join(__dirname, 'files', 'large-video.mp4');
  const stat = fs.statSync(filePath);
  const fileSize = stat.size;
  
  // 获取Range头信息
  const range = req.headers.range;
  
  if (range) {
    // 解析Range头
    const parts = range.replace(/bytes=/, "").split("-");
    const start = parseInt(parts[0], 10);
    const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
    const chunksize = (end - start) + 1;
    
    // 创建文件读取流
    const file = fs.createReadStream(filePath, { start, end });
    
    // 设置响应头
    res.writeHead(206, {
      'Content-Range': `bytes ${start}-${end}/${fileSize}`,
      'Accept-Ranges': 'bytes',
      'Content-Length': chunksize,
      'Content-Type': 'video/mp4',
      'Content-Disposition': 'attachment; filename="large-video.mp4"'
    });
    
    // 管道传输
    file.pipe(res);
  } else {
    // 完整文件下载
    res.setHeader('Content-Length', fileSize);
    res.setHeader('Content-Type', 'video/mp4');
    res.setHeader('Content-Disposition', 'attachment; filename="large-video.mp4"');
    
    fs.createReadStream(filePath).pipe(res);
  }
});

2. 流量控制和限速

防止服务器带宽被耗尽:

const throttle = require('throttle');

app.get('/download/limited', (req, res) => {
  const filePath = path.join(__dirname, 'files', 'large-file.iso');
  const stat = fs.statSync(filePath);
  
  // 限速1MB/s
  const throttleStream = new throttle(1024 * 1024);
  
  res.setHeader('Content-Length', stat.size);
  res.setHeader('Content-Type', 'application/octet-stream');
  res.setHeader('Content-Disposition', 'attachment; filename="large-file.iso"');
  
  fs.createReadStream(filePath)
    .pipe(throttleStream)
    .pipe(res);
});

断点续传完整实现

1. 服务端断点续传支持

app.get('/download/resumable/:filename', (req, res) => {
  const filename = req.params.filename;
  const filePath = path.join(__dirname, 'files', filename);
  
  try {
    const stat = fs.statSync(filePath);
    const fileSize = stat.size;
    
    // 获取Range头
    const range = req.headers.range;
    
    if (range) {
      // 解析Range头
      const parts = range.replace(/bytes=/, "").split("-");
      const start = parseInt(parts[0], 10);
      const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
      
      if (start >= fileSize || end >= fileSize) {
        // 无效的Range请求
        res.status(416).header('Content-Range', `bytes */${fileSize}`).end();
        return;
      }
      
      const chunksize = (end - start) + 1;
      const file = fs.createReadStream(filePath, { start, end });
      
      res.status(206);
      res.setHeader('Content-Range', `bytes ${start}-${end}/${fileSize}`);
      res.setHeader('Accept-Ranges', 'bytes');
      res.setHeader('Content-Length', chunksize);
      res.setHeader('Content-Type', getContentType(filename));
      res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
      
      file.pipe(res);
    } else {
      // 完整文件下载
      res.setHeader('Content-Length', fileSize);
      res.setHeader('Content-Type', getContentType(filename));
      res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
      res.setHeader('Accept-Ranges', 'bytes');
      
      fs.createReadStream(filePath).pipe(res);
    }
  } catch (error) {
    if (error.code === 'ENOENT') {
      res.status(404).send('File not found');
    } else {
      res.status(500).send('Server error');
    }
  }
});

function getContentType(filename) {
  const ext = path.extname(filename).toLowerCase();
  const contentTypes = {
    '.pdf': 'application/pdf',
    '.mp4': 'video/mp4',
    '.zip': 'application/zip',
    '.iso': 'application/octet-stream',
    '.txt': 'text/plain'
  };
  return contentTypes[ext] || 'application/octet-stream';
}

2. 客户端断点续传实现

<!DOCTYPE html>
<html>
<head>
    <title>断点续传下载器</title>
    <script src="https://cdn.jsdelivr.net/npm/axios@1.6.0/dist/axios.min.js"></script>
</head>
<body>
    <h1>大文件断点续传下载</h1>
    <button onclick="startDownload()">开始下载</button>
    <button onclick="pauseDownload()">暂停下载</button>
    <button onclick="resumeDownload()">继续下载</button>
    <div id="progress">进度: 0%</div>

    <script>
        let downloadController = null;
        let downloadedBytes = 0;
        let totalBytes = 0;
        let isPaused = false;

        async function startDownload() {
            try {
                const response = await axios.head('/download/resumable/large-file.zip');
                totalBytes = parseInt(response.headers['content-length'], 10);
                
                await downloadChunk();
            } catch (error) {
                console.error('开始下载失败:', error);
            }
        }

        async function downloadChunk() {
            if (isPaused) return;
            
            const chunkSize = 1024 * 1024; // 1MB chunks
            const end = Math.min(downloadedBytes + chunkSize - 1, totalBytes - 1);
            
            try {
                downloadController = new AbortController();
                
                const response = await fetch('/download/resumable/large-file.zip', {
                    headers: {
                        'Range': `bytes=${downloadedBytes}-${end}`
                    },
                    signal: downloadController.signal
                });

                if (response.status === 206) {
                    const blob = await response.blob();
                    await saveChunk(blob);
                    
                    downloadedBytes = end + 1;
                    updateProgress();
                    
                    if (downloadedBytes < totalBytes) {
                        await downloadChunk();
                    } else {
                        console.log('下载完成!');
                    }
                }
            } catch (error) {
                if (error.name !== 'AbortError') {
                    console.error('下载错误:', error);
                }
            }
        }

        async function saveChunk(blob) {
            // 这里实现文件保存逻辑,可以使用FileSystem API或IndexedDB
            console.log('保存数据块:', blob.size, 'bytes');
        }

        function pauseDownload() {
            isPaused = true;
            if (downloadController) {
                downloadController.abort();
            }
        }

        function resumeDownload() {
            if (isPaused) {
                isPaused = false;
                downloadChunk();
            }
        }

        function updateProgress() {
            const progress = (downloadedBytes / totalBytes * 100).toFixed(2);
            document.getElementById('progress').textContent = `进度: ${progress}%`;
        }
    </script>
</body>
</html>

高级特性与最佳实践

1. 多线程下载支持

app.get('/download/multithread/:filename', async (req, res) => {
  const filename = req.params.filename;
  const filePath = path.join(__dirname, 'files', filename);
  const threads = parseInt(req.query.threads) || 4;
  
  try {
    const stat = fs.statSync(filePath);
    const fileSize = stat.size;
    const chunkSize = Math.ceil(fileSize / threads);
    
    res.setHeader('Content-Type', 'application/json');
    res.json({
      filename,
      fileSize,
      chunkSize,
      threads,
      chunks: Array.from({ length: threads }, (_, i) => ({
        start: i * chunkSize,
        end: Math.min((i + 1) * chunkSize - 1, fileSize - 1)
      }))
    });
  } catch (error) {
    res.status(404).json({ error: 'File not found' });
  }
});

2. 下载状态管理

class DownloadManager {
  constructor() {
    this.downloads = new Map();
  }

  startDownload(sessionId, filePath) {
    const stat = fs.statSync(filePath);
    const downloadInfo = {
      sessionId,
      filePath,
      totalSize: stat.size,
      downloaded: 0,
      chunks: [],
      startTime: Date.now()
    };
    
    this.downloads.set(sessionId, downloadInfo);
    return downloadInfo;
  }

  updateProgress(sessionId, bytesDownloaded) {
    const info = this.downloads.get(sessionId);
    if (info) {
      info.downloaded += bytesDownloaded;
      info.chunks.push({
        time: Date.now(),
        bytes: bytesDownloaded
      });
    }
  }

  getProgress(sessionId) {
    const info = this.downloads.get(sessionId);
    if (!info) return null;
    
    const elapsed = (Date.now() - info.startTime) / 1000;
    const speed = info.downloaded / elapsed;
    const remaining = (info.totalSize - info.downloaded) / speed;
    
    return {
      progress: (info.downloaded / info.totalSize * 100).toFixed(2),
      downloaded: info.downloaded,
      total: info.totalSize,
      speed: this.formatSpeed(speed),
      remaining: this.formatTime(remaining)
    };
  }

  formatSpeed(bytesPerSecond) {
    const units = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
    let speed = bytesPerSecond;
    let unitIndex = 0;
    
    while (speed >= 1024 && unitIndex < units.length - 1) {
      speed /= 1024;
      unitIndex++;
    }
    
    return `${speed.toFixed(2)} ${units[unitIndex]}`;
  }

  formatTime(seconds) {
    const hours = Math.floor(seconds / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    const secs = Math.floor(seconds % 60);
    
    return `${hours}h ${minutes}m ${secs}s`;
  }
}

// 使用示例
const downloadManager = new DownloadManager();

app.get('/download/status/:sessionId', (req, res) => {
  const status = downloadManager.getProgress(req.params.sessionId);
  res.json(status || { error: 'Session not found' });
});

3. 安全性考虑

// 文件类型白名单
const allowedExtensions = ['.pdf', '.txt', '.zip', '.mp4', '.jpg', '.png'];

app.get('/download/secure/:filename', (req, res) => {
  const filename = req.params.filename;
  const ext = path.extname(filename).toLowerCase();
  
  // 检查文件类型
  if (!allowedExtensions.includes(ext)) {
    return res.status(403).send('File type not allowed');
  }
  
  // 防止路径遍历攻击
  const safeFilename = path.basename(filename);
  const filePath = path.join(__dirname, 'files', safeFilename);
  
  // 检查文件是否存在
  if (!fs.existsSync(filePath)) {
    return res.status(404).send('File not found');
  }
  
  // 设置下载限制
  const downloadLimit = 1024 * 1024 * 100; // 100MB限制
  
  res.download(filePath, {
    headers: {
      'X-Content-Type-Options': 'nosniff',
      'X-Frame-Options': 'DENY'
    },
    maxAge: '1h',
    dotfiles: 'deny'
  });
});

性能优化表格

优化策略实现方式适用场景性能提升
分块传输Range头支持大文件下载减少内存占用,支持断点续传
流量控制throttle限流带宽限制环境防止服务器过载
多线程下载分片并行下载高速网络环境提升下载速度2-4倍
缓存策略设置Cache-Control重复下载减少服务器压力
压缩传输gzip/brotli压缩文本文件减少传输数据量

完整示例项目结构

project/
├── app.js                 # 主应用文件
├── package.json
├── files/                 # 下载文件目录
│   ├── document.pdf
│   ├── large-video.mp4
│   └── software.zip
├── middleware/
│   └── download.js       # 下载中间件
├── utils/
│   └── fileManager.js    # 文件管理工具
└── public/
    └── download.html     # 下载页面

总结

Express框架提供了强大的文件下载能力,通过合理利用分块传输、断点续传、流量控制等高级特性,可以构建出专业级的大文件下载解决方案。关键要点包括:

  1. 充分利用HTTP Range头实现分块下载和断点续传
  2. 实施流量控制防止服务器带宽被耗尽
  3. 加强安全措施防止路径遍历和非法文件访问
  4. 提供进度反馈增强用户体验
  5. 支持多线程下载提升大文件下载效率

通过本文的实践方案,你可以轻松构建出稳定、高效、安全的大文件下载服务,满足各种复杂场景下的文件传输需求。

【免费下载链接】express Fast, unopinionated, minimalist web framework for node. 【免费下载链接】express 项目地址: https://gitcode.com/GitHub_Trending/ex/express

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值