Front-end large file slice upload, breakpoint resume upload, instant upload and other solutions, examples of use in Vue

Look at the logic first

How to slice? How to get the unique hash of a file? Interact with the backend to obtain the status of file upload, which can be used to judge the situation. Is it a second upload or a resume upload? Upload a sliced file and re-execute the failed file? Notify the backend of all uploads completed?

1. Upload all the code first, then analyze it in part 2 and use it in vue in part 3.
Related dependencies
spark-md5 is mainly used to get the md5 of the file
mitt publish and subscribe

import SparkMD5 from 'spark-md5'
import {<!-- --> getStartMultipartUpload, postUploadMultipartPart, postCompleteMultipartUpload } from '@/api/file'
import mitt from '@/utils/mitt'

class UpLoadVideo {<!-- -->
  /**
   * file stream file
   * sectionSize slice size (MB) default 5MB
   * failCount number of failed retries, default 3 times
   * concurrencyMax maximum concurrency
   * @param {*} params
   */
  constructor(params) {<!-- -->
    // file stream
    this.file = params.file
    this.sectionSize = params.sectionSize || 5
    this.failCount = params.failCount || 3
    this.concurrencyMax = params.concurrencyMax || 1
    // Whether to stop uploading
    this.isStop = false
    this._count = 0
    // Each file will return the upload ID before checking the upload interface. If the md5 is the same, the returned ID will be the same.
    this.uploadId = ''
    // md5 case
    this.md5 = {<!-- -->
      value: '',
      Progress: 0
    }
    //Upload progress
    this.upProgress = 0
    // 0 Not started 1 Slicing in progress 2 Slicing completed 3 Uploading starting 4 All uploads completed 5 Upload failed Status defined by the front end
    this.status = 0
    // Publish and subscribe
    this.mitt = mitt
    // slice list
    this.fileList = []
    //The initial number of slices
    this.multipartCount = 0
    //Video duration (milliseconds) is obtained locally
    this.duration = 0
    this.upVideo()
  }
  /**
   * Get file md5 and slices
   * And check the status of md5 whether it has been uploaded or is half uploaded
   * @returns
   */
  async upVideo() {<!-- -->
    const file = this.file
    this.mitt.emit('currentFunc', {<!-- --> msg: 'Checking file information' })
    const md5 = await this.getMd5(2)
    const duration = await this.getDuration(file)
    this.duration = duration
    const size = 1024 * 1024 * this.sectionSize // Slice size
    const fileList = []
    let index = 0 // slice sequence number
    for (let cur = 0; cur < file.size; cur + = size) {<!-- -->
      const sectionFile = file.slice(cur, cur + size)
      const partIdx = + + index
      fileList.push({<!-- -->
        partIdx,
        multipartName: file.name + '_' + partIdx,
        file: sectionFile,
        size: sectionFile.size
      })
    }
    this.multipartCount = index
    const res = await this.getDetail({<!-- -->
      MD5: md5,
      Size: file.size,
      FileName: file.name,
      MultipartCount: fileList.length
    })
    // slice array
    const newList = []
    // According to the data returned by the interface, partIdxList indicates those indexes that have been uploaded and filter out those that have not been uploaded.
    if (res.partIdxList & amp; & amp; res.partIdxList.length) {<!-- -->
      fileList.forEach(item => {<!-- -->
        if (!res.partIdxList.includes(item.partIdx)) {<!-- -->
          newList.push(item)
        }
      })
    }
    this.uploadId = res.uploadId
    // state 2 uploaded
    if (res.state === 2) {<!-- -->
      this.status = 4
      this.upEmit('currentFunc', {<!-- --> msg: 'Upload completed', ...res })
      return
    }
    // state 1 is uploading and the slice length is equal to the length of the uploaded sequence number list
    if (res.state === 1 & amp; & amp; fileList.length === res.partIdxList.length) {<!-- -->
      this.getCompleteMultipartUpload(false)
      return
    }
    this.fileList = newList.length ? newList : fileList
    this.upSection()
  }
  //Query before uploading
  async getDetail(data) {<!-- -->
    return new Promise((resolve, reject) => {<!-- -->
      getStartMultipartUpload(data).then(res => {<!-- -->
        resolve(res.data)
      })
    })
  }
  /**
   * Whether to check
   * @param {*} is
   */
  async getCompleteMultipartUpload(is = false) {<!-- -->
    const formData = new FormData()
    formData.append('UploadId', this.uploadId)
    postCompleteMultipartUpload(formData).then(res => {<!-- -->
      this.upProgress = 100
      this.status = 4
      this.upEmit('currentFunc', {<!-- --> msg: 'Upload completed', ...res.data })
    })
  }
  async upSection(fileList) {<!-- -->
    if (this._count === this.failCount) {<!-- -->
      this.upProgress = 0
      this.mitt.emit('currentFunc', {<!-- --> msg: 'The upload failed, please try uploading again. Your upload progress will be retained, and the next upload will be accelerated\ ' })
      return
    }
    fileList = fileList || this.fileList
    console.time()
    if (fileList.length === 0) {<!-- -->
      console.timeEnd()
      this.getCompleteMultipartUpload(true)
      return
    }
    const pool = []// concurrent pool
    const max = this.concurrencyMax // Maximum concurrency
    let finish = 0//Quantity completed
    const failList = []//Failed list
    const upProgress = this.upProgress
    if (!upProgress) {<!-- -->
      // Delay execution for 1 second and the number completed is greater than 0
      setTimeout(() => {<!-- -->
        if (!finish) {<!-- -->
          this.mitt.emit('currentFunc', {<!-- --> msg: 'Ready to upload...' })
        }
      }, 1000)
    }
    this.status = 3
    for (let i = 0; i < fileList.length; i + + ) {<!-- -->
      const item = fileList[i]
      //Call the interface and upload slices
      const task = this.apiFun(item).then(res => {<!-- -->
        const progress = (100 / fileList.length * finish)
        this.upProgress = Math.floor(progress * 100) / 100
        this.upEmit('currentFunc', {<!-- --> msg: 'Uploading files' + this.upProgress + '%' })
        // After the request ends, remove the Promise task from the concurrent pool
        const index = pool.findIndex(t => t === task)
        pool.splice(index)
      }).catch(_ => {<!-- -->
        //Failed ones are stored in the failure array
        failList.push(item)
      }).finally(_ => {<!-- -->
        finish++
        // After all requests are completed, check the failed array
        if (finish === fileList.length) {<!-- -->
          // Check the number of failures
          this._count++
          this.upSection(failList)
        }
      })
      pool.push(task)
      if (pool.length === max) {<!-- -->
        //End upload
        if (this.isStop) break
        // Whenever the concurrency pool finishes running a task, another task is inserted
        await Promise.allSettled(pool)
      }
    }
  }

  // Slice request upload
  apiFun(item) {<!-- -->
    return new Promise((resolve, reject) => {<!-- -->
      const formData = new FormData()
      formData.append('UploadId', this.uploadId)
      formData.append('MultipartName', item.multipartName)
      formData.append('PartIdx', item.partIdx)
      formData.append('Size', item.size)
      formData.append('File', item.file)
      postUploadMultipartPart(formData).then(res => {<!-- -->
        resolve(item.partIdx)
      }).catch(err => {<!-- -->
        reject(err)
      })
    })
  }
  upEmit(event = 'currentFunc', data) {<!-- -->
    this.mitt.emit(event, {<!-- --> ...data, md5: this.md5, upProgress: this.upProgress, status: this.status, duration: this.duration })
  }
  /**
   *
   * @param {*} computeCount number of calculations, if not passed, the default is to calculate all slices
   * @returns
   */
  getMd5(computeCount) {<!-- -->
    const that = this
    return new Promise((resolve, reject) => {<!-- -->
      // compatible
      const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
      //Slice calculation size
      const chunkSize = 2097152 // Read in chunks of 2MB
      // current file stream
      const file = that.file
      //How many slices can be divided currently?
      let chunks = Math.ceil(file.size / chunkSize)
      // If the number of times passed in is less than the number of slices, use the number passed in
      if (computeCount & amp; & amp; computeCount < chunks) {<!-- -->
        chunks = computeCount
      }
      let currentChunk = 0
      //Library method for calculating md5
      const spark = new SparkMD5.ArrayBuffer()
      //Create FileReader
      const fileReader = new FileReader()
      //Write to slice stream
      function loadNext() {<!-- -->
        const start = currentChunk * chunkSize
        const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize
        fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
      }
      loadNext()
      // Calculate md5 after writing is completed
      fileReader.onload = function(e) {<!-- -->
        spark.append(e.target.result) //Append array buffer
        currentChunk + +
        if (currentChunk < chunks) {<!-- -->
          const progress = ((100 / chunks) * currentChunk)
          this.status = 1
          that.md5 = {<!-- -->
            value: '',
            progress: Math.floor(progress * 100) / 100
          }
          that.upEmit('currentFunc', {<!-- --> msg: 'Checking file information' + that.md5.progress + '%' })
          loadNext()
        } else {<!-- -->
          const md5 = {<!-- -->
            value: spark.end(),
            Progress: 100
          }
          that.md5 = md5
          this.status = 2
          that.upEmit('currentFunc', {<!-- --> msg: 'Check file information 100%' })
          resolve(md5.value)
        }
      }

      fileReader.onerror = function() {<!-- -->
        that.upEmit('currentFunc', {<!-- --> msg: 'Failed to check file information, please try again' })
      }
    })
  }
  /**
   * Get video length
   * @param {*} file
   * @returns
   */
  getDuration(file) {<!-- -->
    return new Promise((resolve, reject) => {<!-- -->
      const video = document.createElement('video')
      video.preload = 'metadata'
      video.src = URL.createObjectURL(file)
      video.onloadedmetadata = function() {<!-- -->
        window.URL.revokeObjectURL(video.src)
        const duration = video.duration
        resolve(Math.floor(duration * 1000))
      }
    })
  }

  /**
   *Stop uploading
   */
  stopUpLoad() {<!-- -->
    this.upProgress = 0
    this.status = 5
    this.isStop = true
    this.mitt.emit('currentFunc', {<!-- --> msg: 'Upload failed, please try again' })
    this.destroy()
  }

  destroy() {<!-- -->
    this.file = null
    this.mitt.clear()
  }
}

export default UpLoadVideo

2 Code analysis (function)
The entire upload was done using Alioss.
The backend encapsulates 3 APIs. The first is query before uploading, the second is to upload video fragments, and the third is to complete the upload.

import {<!-- --> getStartMultipartUpload, postUploadMultipartPart, postCompleteMultipartUpload } from '@/api/file'

2.1 getMd5 is used to get the md5 of the file. I only take the first few slices as the global md5, because in the case of large files, getting the md5 of the file is also a time-consuming operation.
2.2 getDuration gets the video duration
2.3 UpVideo processing of the size of each slice and query before uploading, mainly processing whether it has been uploaded
2.3.1 Processing of slice size

const size = 1024 * 1024 * this.sectionSize // Slice size
const fileList = []
let index = 0 // slice sequence number
for (let cur = 0; cur < file.size; cur + = size) {<!-- -->
  const sectionFile = file.slice(cur, cur + size)
  const partIdx = + + index
  fileList.push({<!-- -->
    partIdx, // Used later to determine which slice has been uploaded
    multipartName: file.name + '_' + partIdx,
    file: sectionFile,
    size: sectionFile.size
  })
}

2.3.2 Query before uploading

const res = await this.getDetail({<!-- -->
      MD5: md5,
      Size: file.size,
      FileName: file.name,
      MultipartCount: fileList.length
    })
    // slice array
    const newList = []
    // According to the data returned by the interface, partIdxList indicates those indexes that have been uploaded and filter out those that have not been uploaded.
    if (res.partIdxList & amp; & amp; res.partIdxList.length) {<!-- -->
      fileList.forEach(item => {<!-- -->
        if (!res.partIdxList.includes(item.partIdx)) {<!-- -->
          newList.push(item)
        }
      })
    }
    this.uploadId = res.uploadId
    // state 2 has been uploaded and returns the result directly
    if (res.state === 2) {<!-- -->
      this.status = 4
      this.upEmit('currentFunc', {<!-- --> msg: 'Upload completed', ...res })
      return
    }
// state 1 is uploading and the slice length is equal to the length of the uploaded sequence number list. Directly call the upload completion interface.
    if (res.state === 1 & amp; & amp; fileList.length === res.partIdxList.length) {<!-- -->
      this.getCompleteMultipartUpload(false)
      return
    }
    // Start upload
this.fileList = newList.length ? newList : fileList
    this.upSection()

2.4 upSection starts multipart upload

if (this._count === this.failCount) {<!-- -->
      this.upProgress = 0
      this.mitt.emit('currentFunc', {<!-- --> msg: 'The upload failed, please try uploading again. Your upload progress will be retained, and the next upload will be accelerated\ ' })
      return
    }
    fileList = fileList || this.fileList
    console.time()
    // All uploads are completed and the completion interface is called
    if (fileList.length === 0) {<!-- -->
      console.timeEnd()
      this.getCompleteMultipartUpload(true)
      return
    }
    const pool = []// concurrent pool
    const max = this.concurrencyMax // Maximum concurrency
    let finish = 0//Quantity completed
    const failList = []//Failed list
    const upProgress = this.upProgress
    if (!upProgress) {<!-- -->
      // Delay execution for 1 second and the number completed is greater than 0
      setTimeout(() => {<!-- -->
        if (!finish) {<!-- -->
          this.mitt.emit('currentFunc', {<!-- --> msg: 'Ready to upload...' })
        }
      }, 1000)
    }
    this.status = 3
    for (let i = 0; i < fileList.length; i + + ) {<!-- -->
      const item = fileList[i]
      //Call the interface and upload slices
      const task = this.apiFun(item).then(res => {<!-- -->
        const progress = (100 / fileList.length * finish)
        this.upProgress = Math.floor(progress * 100) / 100
        this.upEmit('currentFunc', {<!-- --> msg: 'Uploading files' + this.upProgress + '%' })
        // After the request ends, remove the Promise task from the concurrent pool
        const index = pool.findIndex(t => t === task)
        pool.splice(index)
      }).catch(_ => {<!-- -->
        //Failed ones are stored in the failure array. Each time fileList is executed, the array of failList will be executed.
        failList.push(item)
      }).finally(_ => {<!-- -->
        finish++
        // After all requests are completed, check the failed array
        if (finish === fileList.length) {<!-- -->
          // Check the number of failures
          this._count++
          this.upSection(failList)
        }
      })
      pool.push(task)
      if (pool.length === max) {<!-- -->
        //End upload
        if (this.isStop) break
        // Whenever the concurrency pool finishes running a task, another task is inserted
        await Promise.allSettled(pool)
      }
    }

3. Use encapsulated js in vue
Imported into vue file

import UpLoadVideo from '@/utils/uploadVideo'

data(){<!-- -->
return {<!-- -->
uploadVideo: null
}
 }
// fileBase is a stream file
   let newUploadVideo = new UpLoadVideo({<!-- -->
        file: fileBase
   })
newUploadVideo.mitt.on('currentFunc', data => {<!-- -->
        this.loadingText = data.msg // Some uploading tips
        this.upProgress = data.upProgress // Upload progress
        // status is customized state is returned from the background
        if (data.status === 4 || data.state === 2) {<!-- -->
            console.log('%c [The data in data is returned by the interface or returned in the encapsulated js]', 'font-size:13px; background:pink; color:#bf2c9f;', data)
          this.upProgress = 0
          this.loadingText = ''
          newUploadVideo.destroy()
          newUploadVideo = null
          this.uploadVideo = null
        }
      })
      this.uploadVideo = newUploadVideo

Reference article: http://blog.ncmem.com/wordpress/2023/11/01/Front-end large file slicing upload, breakpoint resume upload, second transfer and other solutions/
Welcome to join the group to discuss