大文件切片上传、断点续传

因为之前在做h5音乐播放器的时候需要上传MP3文件,就想到之前面试有问过大文件的上传,所以就着手实现了一个,演示地址:https://www.zsp.cool/ls

gitee仓库:https://gitee.com/zhangshengpengBXH/vue-musec

手机扫码:大文件切片上传、断点续传

 

 

一、前后端大致工作:

  1.1 前端部分:

  • 首先前端计算出读取到的大文件的md5,然后对文件进行切片,使用md5+切片序号来命名,然后将切片上传至后端,由于大文件计算md5相当耗时,在主线程操作可能会造成页面假死,所以将md5的计算放到WebWorker线程
  • 而断点续传的实现依赖于切片的上传进度,如果上传在中途中断,则已上传成功的切片无需再次上传,所以需在每次切片上传前,检查后端是否已存在该切片,若存在则跳过;
  • 当所有切片上传完毕后,通知后端合并切片;

  1.2 后端部分:

  • 需要实现根据切片路径及名称查找该切片是否存在的接口;
  • 根据md5动态生成文件夹,将该文件的所有切片保存在该目录下;
  • 收到切片全部上传完毕的通知后,读取切生成原文件,删除切片及文件夹;

  下面开始具体实现,先说说一下前后端用到的相关工具吧

前端:vue+axios,spark-md5

后端:node+express,multer

 

  二、具体实现
 
2.1前端部分:
关于webWorker的创建,暂时没找到什么好方法,这里我直接将初始化webWorker的方法挂载到了window上,html模板文件里:
<script>
  window.worker = null
  window.createWorker = () => { window.worker = new Worker(‘./js/webWorker.js‘) }
</script>
 
把spark-md5.min.js拷贝到了public/js目录下,并在该目录创建webWorker.js,其中data为传入的文件切片数组,循环调用getMd5直至遍历完所有切片,向主线程发送计算得出的hash:
self.importScripts(‘./spark-md5.min.js‘)

self.addEventListener(‘message‘, ({ data }) => {
  const spark = new self.SparkMD5.ArrayBuffer()
  const getMd5 = files => {
    const reader = new FileReader()
    reader.readAsArrayBuffer(files.shift().file)
    reader.onload = e => {
      spark.append(e.target.result)
      if (files.length) {
        getMd5(files)
      } else {
        self.postMessage({ hash: spark.end() })
      }
    }
  }
  getMd5(data)
})
 
界面部分,先获取文件,对文件进行切片,将切片传递给webWorker,切片利用file.slice实现:
<input id="file" value="file" type="file" @change="handlefilesv-show="falseaccept="audio/mp3" multiple/>
 data () {
    return {
      filenull,
      fileListnull,
      chunckList: []
    }
  },
methods: {
createChunkList (file) {
    const fileChunkList = []
    const chunkSize = 1024 * 1024 * 40 // 切片大小为40MB,大家随意
    let cur = 0
    while (cur < file.size) {
      fileChunkList.push({ file: file.slice(curcur + chunkSize), percent0 })
      cur += chunkSize
    }
    return fileChunkList
  }
 handlefiles (e) {
window.createWorker()
 
   this.fileList = Array.from(e.target.files)
   this.file = this.fileList[0]
   this.chunckList = this.createChunkList(this.fileList.shift())
   window.worker.postMessage(this.chunckList)
}
}
 
handleFiles中还需要在接收到webWorler线程的md5之后,验证后端是否已有该切片,以及将未发送的切片和md5信息上传至后端:
window.worker.onmessage = ({ data }) => {
        const promiseList = []
        this.chunckList.forEach((itemindex=> {
          promiseList.push(new Promise((resolvereject=> {
            this.$axios.post(‘/chunckAlready‘, { name: data.hashhash: `${data.hash}-${index}` }).then(res => {
              if (res.data.already === false) { // 后端不存在该切片,上传切片
                const formData = new FormData()
                formData.append(‘hash‘, `${data.hash}-${index}`)
                formData.append(‘name‘, `${data.hash}`)
                formData.append(this.fileName, item.file)
                this.$upload.post(this.baseUrlformData, {
                  onUploadProgressprogressEvent => { 
                    this.chunckList[index].percent = (progressEvent.loaded / progressEvent.total * 100 | 0)
                  }
                }).then(res => { resolve(res) })
              } else {
                this.chunckList[index].percent = 100
                resolve()
              }
            })
          }))
        })
        Promise.all(promiseList).then(() => { // 全部切片上传完毕,通知后端合并切片
          this.$axios.post(‘/merge‘, { hash: data.hashfileName: `${data.hash}-${this.file.name}` }).then((res=> {
            this.$emit(‘finish‘, res.data)
            if (this.fileList.length) { // 如果选中多个文件,重复切片、上传操作
              this.file = this.fileList[0]
              this.chunckList = this.createChunkList(this.fileList.shift())
              window.worker.postMessage(this.chunckList)
            } else {
              this.showPercent = false
              this.$nextTick(() => {
                this.chunckList = []
                window.worker.terminate()
              })
            }
          })
        })
      }
 
2.2 后端部分
 
后端处理上传文件使用的multer插件,插件的使用不再介绍,下面是我的app.js,multer相关配置:
var Router = require(‘./route‘)
var multer = require(‘multer‘)
 
var storage = multer.diskStorage({
  destinationfunction (reqfilecb) {
    if (req.url === ‘/upload-chunck‘) {
      fs.stat(`./public/music/chunck/${req.body.name}`, (errstats=> {
        if(err) { // 文件夹不曾存在,创建文件夹
          fs.mkdir(`./public/music/chunck/${req.body.name}/`, () => { cb(null, `./public/music/chunck/${req.body.name}`) })
        } else if (stats.isDirectory()) { // 存在,直接使用
          cb(null, `./public/music/chunck/${req.body.name}`)
        }
        
      })
    }
    
  },
  filenamefunction (reqfilecb) {
    if (req.url === ‘/upload-chunck‘) {
      cb(null, req.body.hash)
    }
  }
})
var upload = multer({ storagestorage });

app.post(‘/upload-chunck‘, upload.single(‘music‘), Router.uploadFile// 上传切片
 
检测切片是否已存在:
app.js:
app.post(‘/chunckAlready‘Router.cheackChunck)
Router.js
exports.cheackChunck = (reqres=> {
  fs.stat(`./public/music/chunck/${req.body.name}/${req.body.hash}`, (errstats=> {
    if(err) {
      console.log(‘查找失败‘,err)
      res.send({ alreadyfalse })
    } else if (stats.isFile()) {
      res.send({ alreadytrue })
    }
  })
}
 
通知合并:
app.js
app.post(‘/merge‘Router.mergeChunck// 合并切片
Router..js
function del(path) {
  // 第一步读取文件内部的文件
  let arr = fs.readdirSync(path)
  // 遍历数组
  for (let i = 0i < arr.lengthi++) {
    // 获取文件的状态
    let stat = fs.statSync(path + ‘/‘ + arr[i]);
    // 判断是文件还是文件夹
    if (stat.isDirectory()) {
      // 说明是文件夹  递归调用
      del(path + ‘/‘ + arr[i]);
    } else {
      // 说明是文件
      fs.unlinkSync(path + ‘/‘ + arr[i]);
    }
  }
}
 
exports.mergeChunck = (reqres=> {
  fs.readdir( `./public/music/chunck/${req.body.hash}/`,async function(errfiles) {
    files.sort((ab=> { // 对切片排序
      a = a.split(‘-‘)[1]
      b = b.split(‘-‘)[1]
      return Number(a) - Number(b)
    })
    let w = fs.createWriteStream(`./public/music/${req.body.fileName}`) // 创建用于写入的管道流
    w.on(‘close‘, () => { // 关闭时删除切片文件及文件夹
      del(`./public/music/chunck/${req.body.hash}`)
      fs.rmdir(`./public/music/chunck/${req.body.hash}`, () => {})
    })
  function merge(chunckfinish) {
   return new Promise((resolverej=> {
     let r = fs.createReadStream(`./public/music/chunck/${req.body.hash}/${chunck}`)
     r.pipe(w, { endfinish })
     r.on(‘end‘resolve)
   })
}
    for (let i = 0i < files.lengthi++) {
      await merge(files[i], i === files.length -1)
    }
    res.send({ 
      name: req.body.fileName,
      src: `https://www.zsp.cool/music/${req.body.fileName}` 
    })
  })
}
 
 
 
参考文章: https://mp.weixin.qq.com/s/xabsRAsBDoPfbRytAPikGA
 欢迎联系交流: 1612977540@qq.com

大文件切片上传、断点续传

上一篇:HTTP知识1


下一篇:Flume—FLume安装步骤