大文件分片上传与断点续传

280 阅读2分钟

一、核心流程设计

  1. 分片上传

    • 前端分片切割:使用 File.slice() 将文件按固定大小(如 1MB)切割为多个 Blob 分片。
    • 生成唯一标识:通过 SparkMD5 等库计算文件内容的哈希值,作为文件唯一 ID。
    • 并发控制:通过 Promise.all 或队列管理控制同时上传的分片数(如 3-5 个并发)。
  2. 断点续传

    • 记录上传状态:前端通过 LocalStorage 或 IndexedDB 保存文件哈希与已上传分片索引。
    • 服务端校验:上传前先查询服务端,获取已上传分片列表,跳过已传部分。
  3. 分片合并

    • 服务端按序合并:收到合并请求后,按分片索引顺序将临时分片合并为完整文件。
    • 清理临时文件:合并完成后删除临时分片,释放存储空间。

二、前端实现关键代码

// 计算文件哈希(Web Worker 优化)
async function calculateHash(file) {
  return new Promise((resolve) => {
    const spark = new SparkMD5.ArrayBuffer();
    const reader = new FileReader();
    reader.readAsArrayBuffer(file);
    reader.onload = (e) => {
      spark.append(e.target.result);
      resolve(spark.end());
    };
  });
}

// 分片上传逻辑
async function uploadFile(file) {
  const chunkSize = 1 * 1024 * 1024; // 1MB
  const totalChunks = Math.ceil(file.size / chunkSize);
  const fileHash = await calculateHash(file);
  
  // 查询已上传分片
  const { uploaded } = await axios.get(`/api/check?hash=${fileHash}`);
  
  // 创建分片上传任务
  const tasks = [];
  for (let i = 0; i < totalChunks; i++) {
    if (uploaded.includes(i)) continue;
    const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);
    const formData = new FormData();
    formData.append('chunk', chunk);
    formData.append('hash', fileHash);
    formData.append('index', i);
    tasks.push(axios.post('/api/upload', formData));
  }

  // 并发控制(例如每次上传3个分片)
  while (tasks.length > 0) {
    const batch = tasks.splice(0, 3);
    await Promise.all(batch);
    updateProgress(); // 更新进度条
  }

  // 合并请求
  await axios.post(`/api/merge?hash=${fileHash}&name=${file.name}`);
}

三、服务端实现要点

  1. API 设计

    • GET /api/check:根据文件哈希返回已上传分片列表。
    • POST /api/upload:接收分片并保存到临时目录。
    • POST /api/merge:合并所有分片并保存最终文件。
  2. 分片存储

    // Node.js 示例(使用 Express)
    const UPLOAD_DIR = path.resolve(__dirname, 'temp');
    
    app.post('/api/upload', (req, res) => {
      const { chunk, hash, index } = req.files;
      const chunkDir = path.resolve(UPLOAD_DIR, hash);
      if (!fs.existsSync(chunkDir)) fs.mkdirSync(chunkDir);
      fs.renameSync(chunk.path, path.resolve(chunkDir, index)); // 保存分片
      res.send({ code: 0 });
    });
    
  3. 分片合并

    app.post('/api/merge', async (req, res) => {
      const { hash, name } = req.query;
      const chunkDir = path.resolve(UPLOAD_DIR, hash);
      const chunks = fs.readdirSync(chunkDir);
      
      // 按索引排序后合并
      chunks.sort((a, b) => a - b).forEach(chunk => {
        fs.appendFileSync(path.resolve(UPLOAD_DIR, name), 
          fs.readFileSync(path.resolve(chunkDir, chunk)));
      });
      
      // 清理临时文件
      fs.rmdirSync(chunkDir, { recursive: true });
      res.send({ code: 0 });
    });
    

四、优化与异常处理

  1. 哈希计算优化

    • 抽样哈希:对大文件取头尾和中间部分计算哈希,减少耗时。
    • Web Worker:防止主线程阻塞。
  2. 断点续传增强

    • 服务端记录分片哈希,避免分片被篡改。
    • 客户端异常退出后,重新选择文件时自动恢复进度。
  3. 错误重试机制

    • 分片上传失败时自动重试(如最多 3 次)。
    • 网络中断后提示用户手动恢复上传。

五、安全性考虑

  • 身份验证:上传接口需校验用户权限(如 JWT)。
  • 分片校验:服务端校验分片 MD5,防止数据损坏。
  • 防重复上传:相同哈希文件直接返回现有地址。