掘金 后端 ( ) • 2024-04-06 12:03

title: 大文件上传(0):计算文件唯一标识 author: NanYanBeiYu desc: 大文件分片上传之计算文件唯一标识

大文件上传(0):计算文件唯一标识

前言:前端使用vue3+element-plus 文件上传是一个在开发中常见的一个部分。通过这个技术用户可以将自己的文件上传到服务器中。

当我们需要上传较大的文件的时候就容易碰到一些问题。例如:

  1. 上传的时间长
  2. 如果在上传的过程之中出现错误就会重新上传

这些问题的存在会极大的影响用户的体验,所以就有了文件的分片上传和断点续传。

文件分片原理:

文件切片顾名思义,就是将大文件给切分成一个个小段再上传,上传后由后端将这些小段合并成一个大文件,流程如下:

  1. 前端将大文件并进行分片
  2. 将每个分片都进行上传
  3. 后端接收到所有的分片后创建一个文件夹存储这些分片
  4. 将此文件夹中的所有分片合并为一个完整的文件。

01文件分片原理.png

文件分片:

demo.vue

先拿到file的文件对象

<script setup>
import { ref } from 'vue'
const formFile = ref()
const upload = (file) => {
  console.log(file.file)
}
</script>
<template>
    <el-form :model="formFile">
      <el-upload action="" :http-request="upload">
        <el-button type="primary">Click to upload</el-button>
      </el-upload>
    </el-form>
</template>

image-20240405192725452.png

拿到file对象后,可以通过file.slice来对文件进行切割,例如file.slice(0,100)则表示从文件开头开始切割前100个字节,遵循左闭右开原则,即: 开始索引(第一个参数)是包含在内的。 结束索引(第二个参数)是不包含在内的。

<script setup>
    const upload = (file) => {
      formFile.value = file.file
      const chunks = createChunks(formFile.value, chunkSize)
      console.log(chunks)
    }
    const chunkSize = 1024 * 1024 // 定义每个切片的大小 1M

    /**
     * 创建切片
     * @param {File} file 要上传的文件
     * @param {number} chunkSize 每个切片的大小
     * @returns {Array} 切片数组
     */
    const createChunks = (file, chunkSize) => {
      // 切片数组
      const result = []
      for (let i = 0; i < file.size; i += chunkSize) {
        result.push(file.slice(i, i + chunkSize))
      }
      return result
    }
</script>

这里可以看到已经将一个文件分成了5个分片了。

image-20240405194238760.png

不管是File,还是Blob都只是存储了文件的基本信息,并没有存储文件的内容

文件的唯一标识

文件分片完成后,我们需要给服务器上传,如果在上传的过程中突然出现了意外而导致文件分片上传被中断了,这时我们就会出现一个问题,不知道这个文件的分片我们是否全部上传完毕了,如果没有全部上传完毕,我们又如何得知哪些分片已上传了。

我们可以给文件一个唯一的标识来确认文件的上传状态。这个时候我们就可以计算文件的hash值来作为唯一的标识。但计算hash时就会出现一个问题:

  • 因为hash的计算是需要读取文件全部数据的,所以当用户传一个4k高清电影的话计算量就会非常的大。

这里就可以使用增量算法来计算hash。即:

我们先使用其中的一块数据来计算出一个结果,计算后这一块数据就不要了。然后将下一个数据与之前的结果一起计算新的结果然后这一块的数据也不要了,之后重复该步骤。

这里使用的是js-sha3 ,详情在https://github.com/emn178/js-sha3

<script setup>
import Pagecontainer from '@/components/pagecontainer.vue'
import { ref } from 'vue'
import { sha3_256 } from 'js-sha3'

const sha3256 = ref()
const formFile = ref()
const upload = (file) => {
  formFile.value = file.file
  const chunks = createChunks(formFile.value, chunkSize)
  console.log(chunks)
  hash(chunks)

}
const chunkSize = 1024 * 1024 * 20 // 定义每个切片的大小 5M

/**
 * 创建切片
 * @param {File} file 要上传的文件
 * @param {number} chunkSize 每个切片的大小
 * @returns {Array} 切片数组
 */
const createChunks = (file, chunkSize) => {
  // 切片数组
  const result = []
  for (let i = 0; i < file.size; i += chunkSize) {
    result.push(file.slice(i, i + chunkSize))
  }
  return result
}

const hash = async (chunks) => {
  const sha3 = sha3_256.create()
  async function _read(i) {
    if (i >= chunks.length) {
      sha3256.value = sha3.hex()
      console.log("sha3256:",sha3256.value)
      return // 读取完毕
    }
    const blob = chunks[i]
    const reader = new FileReader()
    await new Promise((resolve, reject) => {
      reader.onload = e => {
        e.target.result // 读取到的字节数组

        sha3.update(e.target.result)
        resolve()
      }
      reader.onerror = reject
      reader.readAsArrayBuffer(blob)
    })
    await _read(i + 1)
  }

  await _read(0)
}

</script>

<template>
  <pagecontainer title="文件上传Demo">
    <el-form :model="formFile">
      <el-upload action="" :http-request="upload">
        <el-button type="primary">Click to upload</el-button>
      </el-upload>
    </el-form>
  </pagecontainer>
</template>

<style scoped></style>

这里又会出现一个问题,就是js语句是单线程的,在js对chunk进行计算的时候,浏览器是做不了其他事的。有2中方法解决:

  1. 适当调整chunk的大小,减少其计算的时间
  2. 利用Web Worker。

因为我不是很熟悉,所以直接使用了ChatGpt来帮我弄。这里放一篇HTML5 File API 配合 Web Worker 计算大文件 SHA3 Hash 值,可以在里面看看。

贴上代码:

demo.vue

<!-- main.js -->
<script setup>
import Pagecontainer from '@/components/pagecontainer.vue'
import { ref } from 'vue'

const formFile = ref()
const chunkSize = 1024 * 1024 * 5 // 定义每个切片的大小 5M

/**
 * 创建切片
 * @param {File} file 要上传的文件
 * @param {number} chunkSize 每个切片的大小
 * @returns {Array} 切片数组
 */
const createChunks = (file, chunkSize) => {
  // 切片数组
  const result = []
  for (let i = 0; i < file.size; i += chunkSize) {
    result.push(file.slice(i, i + chunkSize))
  }
  return result
}



// 在主线程中
const worker = new Worker('src/assets/worker/sha3worker.js')

// 上传文件
const upload = (file) => {
  formFile.value = file.file
  const chunks = createChunks(formFile.value, chunkSize)
  console.log(chunks)

  // 发送切片给Web Worker
  worker.postMessage({ type: 'chunks', data: chunks })
}

// Web Worker中的代码(worker.js)
worker.addEventListener('message', (e) => {
  if (e.data.type === 'hash') {
    const hashResult = e.data.data
    console.log("Hash result:", hashResult)
  }
})
</script>

<template>
  <pagecontainer title="文件上传Demo">
    <el-form :model="formFile">
      <el-upload action="" :http-request="upload">
        <el-button type="primary">Click to upload</el-button>
      </el-upload>
    </el-form>
  </pagecontainer>
</template>

<style scoped></style>

sha3worker.js

// sha3worker.js

importScripts('https://cdnjs.cloudflare.com/ajax/libs/js-sha3/0.8.0/sha3.min.js');

self.addEventListener('message', (e) => {
  if (e.data.type === 'chunks') {
    const chunks = e.data.data
    hash(chunks)
  }
})

// 哈希计算
const hash = async (chunks) => {
  const sha3 = sha3_256.create()
  await Promise.all(chunks.map(async (blob) => {
    const buffer = await blob.arrayBuffer()
    sha3.update(buffer)
  }))
  self.postMessage({ type: 'hash', data: sha3.hex() })
}

运行效果:

02计算结果.png