文件上传

如何做出亮点

  1. 上传文件格式验证
    • 验证后缀,但是无法防范恶意改名文件
    • 二进制流验证 (hexdunm) 一个可靠的文件校验
  2. 断点续传
    • 对文件进行切片,计算 md5 然后按数组上传
  3. 秒传,计算 Md5 然后进行比较
    • 大文件 使用 webwork 计算
    • fiber 架构,利用空闲时间计算
    • 对文件进行抽样数据再计算 md5
  4. 并发数控制+错误重试
    • 并发 + tcp 冷启动
  5. 思考
    • 碎片清理
    • 文件碎片存储在多个机器上
    • 文件碎片备份
    • 兼容性更好的 requestIdleCallback
    • 抽样 hash + 全量 hash 双重判断
    • websocket 推送
    • cdn

不同事件上传 + 进度条

拖拽上传

drag.addEventListener("dragover", () => {});
drag.addEventListener("dragleave", () => {});
drag.addEventListener("drop", () => {});

上传文件格式验证

  • 验证后缀,但是无法防范恶意改名文件
  • 二进制流验证 (hexdunm) 一个可靠的文件校验

断点续传

计算 hash 这种事都可以放入 Web Worker,不会阻塞渲染进程

  1. 整个文件进行 hash 计算,1.5 个 G 大概需要 20s
  2. 切片以后计算,再合并计算, 看并发数量

requestIdleCallback

时间切片

 

秒传

结合布隆过滤器 的思想,以 2M 的切片为例,取首尾各一个切片,其他切片只取首中尾各两个字节后,合并后计算 hash,1.5G 的文件大概只需要 1s

    async calculateHashSample() {
      return new Promise(resolve => {
        const spark = new SparkMD5.ArrayBuffer();
        const reader = new FileReader();
        const file = this.container.file;
        // 文件大小
        const size = this.container.file.size;
        let offset = 2 * 1024 * 1024;
 
        let chunks = [file.slice(0, offset)];
 
        // 前面100K
 
        let cur = offset;
        while (cur < size) {
          // 最后一块全部加进来
          if (cur + offset >= size) {
            chunks.push(file.slice(cur, cur + offset));
          } else {
            // 中间的 前中后去两个字节
            const mid = cur + offset / 2;
            const end = cur + offset;
            chunks.push(file.slice(cur, cur + 2));
            chunks.push(file.slice(mid, mid + 2));
            chunks.push(file.slice(end - 2, end));
          }
          // 前取两个字节
          cur += offset;
        }
        // 拼接
        reader.readAsArrayBuffer(new Blob(chunks));
        reader.onload = e => {
          spark.append(e.target.result);
 
          resolve(spark.end());
        };
      });
    }
 
 

并发数控制+错误重试

 
 
async sendRequest(forms, max=4) {
  return new Promise(resolve => {
    const len = forms.length;
    let idx = 0;
    let counter = 0;
    const start = async ()=> {
      // 有请求,有通道
      while (idx < len && max > 0) {
        max--; // 占用通道
        console.log(idx, "start");
        const form = forms[idx].form;
        const index = forms[idx].index;
        idx++
        request({
          url: '/upload',
          data: form,
          onProgress: this.createProgresshandler(this.chunks[index]),
          requestList: this.requestList
        }).then(() => {
          max++; // 释放通道
          counter++;
          if (counter === len) {
            resolve();
          } else {
            start();
          }
        });
      }
    }
    start();
  });
}
 
async uploadChunks(uploadedList = []) {
  // 这里一起上传,碰见大文件就是灾难
  // 没被hash计算打到,被一次性的tcp链接把浏览器稿挂了
  // 异步并发控制策略,我记得这个也是头条一个面试题
  // 比如并发量控制成4
  const list = this.chunks
    .filter(chunk => uploadedList.indexOf(chunk.hash) == -1)
    .map(({ chunk, hash, index }, i) => {
      const form = new FormData();
      form.append("chunk", chunk);
      form.append("hash", hash);
      form.append("filename", this.container.file.name);
      form.append("fileHash", this.container.hash);
      return { form, index };
    })
-     .map(({ form, index }) =>
-       request({
-           url: "/upload",
-         data: form,
-         onProgress: this.createProgresshandler(this.chunks[index]),
-         requestList: this.requestList
-       })
-     );
-   // 直接全量并发
-   await Promise.all(list);
     // 控制并发
+   const ret =  await this.sendRequest(list,4)
 
  if (uploadedList.length + list.length === this.chunks.length) {
    // 上传和已经存在之和 等于全部的再合并
    await this.mergeRequest();
  }
},
 

碎片清理

如果很多人传了一半就离开了,这些切片存在就没意义了,可以考虑定期清理,当然,我们可以使用node-schedule 来管理定时任务比如我们每天扫一次 target,如果文件的修改时间是一个月以前了,就直接删除把

[let start = function(UPLOAD_DIR){ // 每5秒 schedule.scheduleJob("*/5 * * * * *",function(){ console.log('开始扫描') scan(UPLOAD_DIR) }) }](<// 为了方便测试,我改成每5秒扫一次, 过期1钟的删除做演示
const fse = require('fs-extra')
const path = require('path')
const schedule = require('node-schedule')
 
// *    *    *    *    *    *
// ┬    ┬    ┬    ┬    ┬    ┬
// │    │    │    │    │    │
// │    │    │    │    │    └ day of week (0 - 7) (0 or 7 is Sun)
// │    │    │    │    └───── month (1 - 12)
// │    │    │    └────────── day of month (1 - 31)
// │    │    └─────────────── hour (0 - 23)
// │    └──────────────────── minute (0 - 59)
// └───────────────────────── second (0 - 59, OPTIONAL)
let start = function(UPLOAD_DIR){
    // 每5秒
    schedule.scheduleJob("*/5 * * * * *",function(){
        console.log('开始扫描')
        scan(UPLOAD_DIR)
    })
}
 
 

大文件切片下载

参考资料