加载中...

大文件分片上传、断点续传、秒传


大文件分片上传、断点续传、秒传

前言

  1. 也是很久没有更新了,这段时间的文章都写在了飞书云里面,后续也会慢慢同步过来。
  2. 最近面试的时候被问到有关大文件分片上传的问题,是为后端工程师面的,虽然接触过一点,但并没有系统性的总结,还是有一定的作用的。
  3. 其实也是为了方便在工作中可以复制<小声bibi>,
  4. 本文章为他文总结性,

本文主要讲述了大文件的分片上传、断点续传和秒传三个功能。实际上,断点续传和秒传都是基于分片上传扩展的功能。

  • 分片上传原理:客户端将选择的文件进行切分,每一个分片都单独发送请求到服务端。

  • 断点续传 & 秒传原理:客户端发送请求询问服务端某文件的上传状态,服务端响应该文件已上传分片,客户端再将未上传分片上传即可。

    • 如果没有需要上传的分片就是秒传;
    • 如果有需要上传的分片就是断点续传。
  • 文件唯一标识:每个文件要有自己唯一的标识,这个标识就是将整个文件进行MD5加密,这是一个Hash算法,将加密后的Hash值作为文件的唯一标识。可以使用第三方工具库spark-md5。

  • 文件合并时机:当服务端确认所有分片都发送完成后,此时会发送请求通知服务端对文件进行合并操作。

前端整体流程

  1. 分片计算Hash值:首先将文件进行分片,并计算其Hash值(文件的唯一标识)。

  2. 查询上传状态:发送请求,询问服务端文件的上传状态。

  3. 根据上传状态进行操作

    • 文件已经上传过了:
      • 结束,实现秒传功能。
    • 文件存在,但分片不完整:
      • 将未上传的分片进行上传,实现断点续传功能。
    • 文件不存在:
      • 将所有分片上传。
  4. 合并文件:当文件分片全部上传后,发送请求通知服务端合并文件分片。

详细流程

上传文件

上传文件触发回调函数,同时注意需要的参数:

/**
 * @param {File} file 目标上传文件
 * @param {number} baseChunkSize 上传分块大小,单位Mb
 * @param {string} uploadUrl 上传文件的后端接口地址
 * @param {string} vertifyUrl 验证文件上传的接口地址
 * @param {string} mergeUrl 请求进行文件合并的接口地址
 * @param {Function} progress_cb 更新上传进度的回调函数
 * @returns {Promise}
 */
 
 async function uploadFile(file, baseChunkSize, uploadUrl, vertifyUrl, mergeUrl, progress_cb) {
    ...
}
  1. 将文件进行分片并计算Hash值
    得到 allChunkList—所有分片 fileHash—文件的hash值
  2. 通过fileHash请求服务端,判断文件上传状态
    得到 neededFileList—待上传文件分片
  3. 同步上传进度,针对不同文件上传状态调用 progress_cb
  4. 发送上传请求
  5. 发送文件合并请求

文件分片 & Hash计算

/**
 * 将目标文件分片 并 计算文件Hash
 * @param {File} targetFile 目标上传文件
 * @param {number} baseChunkSize 上传分块大小,单位Mb
 * @returns {chunkList:ArrayBuffer,fileHash:string}
 */
async function sliceFile(targetFile, baseChunkSize = 1) {
  return new Promise((resolve, reject) => {
    //初始化分片方法,兼容问题
    let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
    //分片大小 baseChunkSize Mb
    let chunkSize = baseChunkSize * 1024 * 1024;
    //分片数
    let targetChunkCount = targetFile && Math.ceil(targetFile.size / chunkSize);
    //当前已执行分片数
    let currentChunkCount = 0;
    //当前以收集的分片
    let chunkList = [];
    //创建sparkMD5对象
    let spark = new SparkMD5.ArrayBuffer();
    //创建文件读取对象
    let fileReader = new FileReader();
    let fileHash = null;

    //FilerReader onload事件
    fileReader.onload = (e) => {
      //当前读取的分块结果 ArrayBuffer
      const curChunk = e.target.result;
      //将当前分块追加到spark对象中
      spark.append(curChunk);
      currentChunkCount++;
      chunkList.push(curChunk);
      //判断分块是否全部读取成功
      if (currentChunkCount >= targetChunkCount) {
        //全部读取,获取文件hash
        fileHash = spark.end();
        resolve({ chunkList, fileHash });
      } else {
        loadNext();
      }
    };

    //FilerReader onerror事件
    fileReader.onerror = () => {
      reject(null);
    };

    //读取下一个分块
    const loadNext = () => {
      //计算分片的起始位置和终止位置
      const start = chunkSize * currentChunkCount;
      let end = start + chunkSize;
      if (end > targetFile.size) {
        end = targetFile.size;
      }
      //读取文件,触发onLoad
      fileReader.readAsArrayBuffer(blobSlice.call(targetFile, start, end));
    };

    loadNext();
  });
}

分片上传入口完善

async function uploadFile(file, baseChunkSize, uploadUrl, vertifyUrl, mergeUrl, progress_cb) {
  const { chunkList, fileHash } = await sliceFile(file, baseChunkSize);
  //所有分片 ArrayBuffer[]
  let allChunkList = chunkList;
  //需要上传的分片序列 number[]
  let neededChunkList = [];
  //上传进度
  let progress = 0;
  //发送请求,获取文件上传状态
  if (vertifyUrl) {
      // 这里发送请求
    const { data } = await requestInstance.post(vertifyUrl, {
      fileHash,
      totalCount: allChunkList.length,
      extname: '.' + file.name.split('.').pop(),
    });
    const { neededFileList, message } = data;
    if (message) console.info(message);
    //无待上传文件,秒传
    if (!neededFileList.length) {
      progress_cb(100);
      return;
    }

    //部分上传成功,更新unUploadChunkList
    neededChunkList = neededFileList;
  }

  //同步上传进度,断点续传情况下
  progress = ((allChunkList.length - neededChunkList.length) / allChunkList.length) * 100;

  //上传
  if (allChunkList.length) {
    const requestList = allChunkList.map(async (chunk, index) => {
      if (neededChunkList.includes(index + 1)) {
        const response = await uploadChunk(chunk, index + 1, fileHash, uploadUrl);
        //更新进度
        progress += Math.ceil(100 / allChunkList.length);
        if (progress >= 100) progress = 100;
        progress_cb(progress);
        return response;
      }
    });
    Promise.all(requestList).then(() => {
      //发送请求,通知后端进行合并 //后缀名可通过其他方式获取,这里以.mp4为例
      requestInstance.post(mergeUrl, { fileHash, extname: '.mp4' });
    });
  }
}

在上传时我们调用了uploadChunk()方法,由于我们的请求不仅包含文件,还包含分片索引以及hash值,因此我们的请求体应该是formData,还有一点需要就是此时我们传入的chunk的类型是ArrayBuffer,而formData中文件的类型应该是Blob 具体代码如下:

async function uploadChunk(chunk, index, fileHash, uploadUrl) {
  let formData = new FormData();
  //注意这里chunk在之前切片之后未ArrayBuffer,而formData接收的数据类型为 blob|string
  formData.append('chunk', new Blob([chunk]));
  formData.append('index', index);
  formData.append('fileHash', fileHash);
  return requestInstance.post(uploadUrl, formData);
}

文章作者:
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 !