文件切片上传,后端为Gin框架

ruby 复制代码
title :vue3+element-plus 实现大文件上传(文件分片上传,断点续传)
author:NanYanBeiYu
desc:大文件上传-分片上传

大文件上传

文件分片处理

本文是借助element-ui搭建的页面。使用el-upload组件,因为我们要自定义上传的实现,所以需要将组件的自动上传关掉:auto-upload="false"。

ini 复制代码
<template>
    <el-upload
      class="upload-demo"
      drag
      action=""
      :auto-upload="false"
      :show-file-list="false"
      :on-change="handleChange"
      multiple
    >
      <el-icon class="el-icon--upload"><upload-filled /></el-icon>
      <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
      <template #tip>
        <div class="el-upload__tip">文件大小不超过4GB</div>
      </template>
    </el-upload>
</template>

当文件状态发生变化时触发:on-change是文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用 handleChange。

获取到文件对象和切片处理

在el-upload组件中,:on-change的参数有file,但这里的file并不是File对象需要通过file.raw来获取。File由于继承了Blob中的slice方法,可以通过这个方法来将File进行切片。

获取文件对象:

javascript 复制代码
const handleChange = (file) => {
  console.log(file.raw)
}

控制台打印信息:

对获取到的File对象进行切片处理,创建切片函数并调用:

javascript 复制代码
const chunkSize = 1024 * 1024 * 2
​
/**
 * @description 当文件状态变化时触发
 * */
const handleChange = (file) => {
  console.log(file.raw)
  const fileChunks = createChunks(file.raw,chunkSize)
  console.log(fileChunks)
}
​
/**
 * @description 创建文件切片
 * @param file 文件
 * @param size 切片大小
 * @returns fileChunks 切片列表
 * */
const createChunks = (file,size) => {
  const fileChunks = []
  for(let i=0;i < file.size ;i += size){
    fileChunks.push(file.slice(i,i+size))
  }
  return fileChunks
}

观察控制台输出信息:

可以看到已经将这个File对象分片成了1885份。

根据文件内容生成SHA3-256

这里我使用的库是js-sha3 ,详情在github.com/emn178/js-s...

javascript 复制代码
const hash = async (chunks) => {
  const sha3 = sha3_256.create()
  let processedChunks = 0 // 记录已处理的chunks数量
  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)
}

控制台输出信息:

将哈希值计算出来后,我们就可以使用这个哈希值来作为文件的唯一标识了。

上传分片信息:

拿到文件的哈希值后,就可以根据服务端的要求来上传分片。

要将分片进行上传,我们就需要对分片列表进行遍历,为每一个分片添加进formData中

markdown 复制代码
/**
 * uploadFile函数负责将文件分片列表(`chunkList.value`)中的所有分片依次上传至服务器。该函数遵循以下步骤:
​
 1. **遍历分片**:
 - 使用一个`for`循环,从索引`0`开始,遍历至`chunkList.value.length - 1`。
 - 在每次迭代中,获取当前分片`chunk`及其在列表中的索引`index`。
​
 2. **构建FormData对象**:
 - 创建一个新的`FormData`实例,用于封装上传所需的各项数据。
 - 使用`append`方法向FormData对象添加以下字段:
 - `chunk`: 当前遍历到的分片数据。
 - `index`: 分片在列表中的索引。
 - `sha3256`: 文件的SHA-3256哈希值(存储在`sha3256.value`)。
 - `suffix`: 文件的后缀名(存储在`suffix.value`)。
 - `totalChunks`: 文件总分片数(存储在`totalChunks.value`)。
​
 3. **调用uploadChunkService进行上传**:
 - 使用封装好的FormData对象作为参数,调用`uploadChunkService(formData)`以发起异步请求,将当前分片上传至服务器。
​
 4. **响应处理**:
 - 对于上传成功的响应:
 - 在控制台输出一条成功消息,包含已成功上传的分片索引及响应数据。
 - 对于上传失败的响应:
 - 在控制台输出一条错误消息,包含失败的分片索引及具体的错误信息。
​
 **参数**:无
​
 **返回值**:无
​
 **依赖**:
 - `chunkList.value`: 存储文件分片数据的数组。
 - `sha3256.value`: 文件的SHA-3256哈希值。
 - `suffix.value`: 文件的后缀名。
 - `totalChunks.value`: 文件总分片数。
 - `uploadChunkService(formData)`: 异步服务函数,用于上传单个文件分片。
​
 **注意**:此函数仅负责上传分片,不涉及分片的生成或合并逻辑。实际执行时应确保相关依赖变量已正确初始化。
 */
const uploadFile = () => {
  // 遍历分片
  for (let i = 0; i < chunkList.value.length; i++) {
    const chunk = chunkList.value[i]
    const index = i
    // 创建FormData对象并添加分片和索引
    const formData = new FormData()
    formData.append('chunk', chunk) // 添加分片
    formData.append('index', index) // 添加索引
    formData.append('sha3256', sha3256.value) // 添加sha3256
    formData.append('suffix', suffix.value) // 添加后缀名
    formData.append('totalChunks', totalChunks.value) // 添加总分片数
    uploadChunkService(formData)
      .then((res) => {
        // 成功上传分片后的处理
        console.log(`Chunk ${index} uploaded successfully`, res)
      })
      .catch((error) => {
        // 上传失败后的处理
        console.error(`Chunk ${index} upload failed`, error)
      })
  }
}

file.js

csharp 复制代码
// 分片上传
export const uploadChunkService = (params) =>
  request.post('/cloud/files/upload/chunk', params, {
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  })

axios封装

javascript 复制代码
import { useUserStore } from '@/stores/user.js'
import axios from 'axios'
import router from '@/router'
// import { ElMessage } from 'element-plus'
​
const baseURL = 'http://127.0.0.1:89/'
​
const instance = axios.create({
  baseURL,
  timeout: 100000
})
​
instance.interceptors.request.use(
  (config) => {
    const userStore = useUserStore()
    if (userStore.token) {
      config.headers.Authorization = userStore.token
    }
    return config
  },
  (err) => Promise.reject(err)
)
​
instance.interceptors.response.use(
  (res) => {
    if (res.status === 200) {
      // 添加检查HTTP状态码是否为200
      if (res.data.code === 200) {
        return res.data // 返回数据而不是整个响应对象
      }
    }
    ElMessage({ message: res.data.msg || '111', type: 'error' })
    return Promise.reject(res) // 当HTTP状态码不是200或者服务端返回错误代码时,才reject
  },
  (err) => {
    ElMessage({
      message: err.response.data.msg || '222',
      type: 'error'
    })
    console.log(err)
    if (err.response?.status === 401) {
      router.push('/login')
    }
    return Promise.reject(err)
  }
)
​
export default instance
export { baseURL }

后端实现:

这里后端使用的是Gin框架。路由这里就不放出来了,可以自行设计。

go 复制代码
// UploadChunk 分片上传
func UploadChunk(c *gin.Context) {
    // 1. 解析请求参数
    fileHash := c.PostForm("sha3256") // 文件hash
    index := c.PostForm("index")      // 分片索引
​
    // 定义一个临时存储路径
    tempDir := "./upload/temp/" + fileHash + "/"
    filePath := filepath.Join(tempDir, fileHash+"_"+index+".part")
​
    // 使用goroutine处理分片
    //创建一个waitgroup,用于等待所有协程执行完毕
    var wg sync.WaitGroup
    wg.Add(1) // 增加一个协程,表示即将启动一个goroutine
​
    go func() {
        defer wg.Done() //确保在goroutine结束时,减少一个计数器
​
        // 确保 tempDir 存在
        if _, err := os.Stat(tempDir); os.IsNotExist(err) {
            os.MkdirAll(tempDir, os.ModePerm)
        }
​
        // 打开文件以追加数据
        file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
        if err != nil {
            // 返回错误
            c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
                "code": 500,
                "msg":  "Failed to open file",
            })
            return
        }
        defer file.Close()
​
        // 读取切片数据
        part, err := c.FormFile("chunk")
        if err != nil {
            c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
                "code": 500,
                "msg":  "Failed to read part",
            })
            return
        }
        src, err := part.Open()
        if err != nil {
            c.AbortWithStatusJSON(500, gin.H{
                "code": 500,
                "msg":  "Failed to open part",
            })
            return
        }
        defer src.Close()
​
        // 将切片的内容写入文件
        _, err = io.Copy(file, src)
        if err != nil {
            c.JSON(500, gin.H{
                "code": 500,
                "msg":  "Failed to write part",
            })
            return
        }
        c.JSON(http.StatusOK, gin.H{
            "code": 200,
            "msg":  "切片上传成功",
        })
    }()
    wg.Wait() // 等待所有协程执行完毕
}

效果:

可以看到这里一个切片了31份,后端存储的切片信息:

相关推荐
xiao-xiang6 分钟前
jenkins-通过api获取所有job及最新build信息
前端·servlet·jenkins
C语言魔术师23 分钟前
【小游戏篇】三子棋游戏
前端·算法·游戏
匹马夕阳2 小时前
Vue 3中导航守卫(Navigation Guard)结合Axios实现token认证机制
前端·javascript·vue.js
你熬夜了吗?2 小时前
日历热力图,月度数据可视化图表(日活跃图、格子图)vue组件
前端·vue.js·信息可视化
桂月二二8 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062069 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb9 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角9 小时前
CSS 颜色
前端·css
九酒9 小时前
从UI稿到代码优化,看Trae AI 编辑器如何帮助开发者提效
前端·trae
浪浪山小白兔10 小时前
HTML5 新表单属性详解
前端·html·html5