springboot3+vue3实现大文件分片上传和断点续传

大文件分片上传和断点续传

大文件分片上传是一种将大文件切分成小片段进行上传的策略。这种上传方式有以下几个主要原因和优势:

  1. 网络稳定性:大文件的上传需要较长时间,而网络连接可能会不稳定或中断。通过将文件切分成小片段进行上传,如果某个片段上传失败,只需要重新上传该片段,而不需要重新上传整个文件。
  2. 断点续传:由于网络的不稳定性,上传过程中可能会中断,导致上传失败。使用分片上传,可以记录已成功上传的片段,当上传中断后再次恢复时,可以跳过已上传的片段,只需上传剩余的片段,从而实现断点续传。
  3. 容错性:在大文件上传过程中,由于各种原因(例如网络中断、服务器故障等),可能会导致部分片段上传失败。通过分片上传,即使部分片段上传失败,也能够保留已成功上传的片段,减小上传失败的影响,提高上传的可靠性和容错性。
  4. 服务器资源管理:大文件上传可能会占用服务器大量的内存和网络带宽资源。通过分片上传,可以将服务器的资源分配给不同的上传任务,避免单个上传任务占用过多资源,提高服务器的可扩展性和资源利用率。

根据上面的概述, 总体就涉及到了两大概念: 分片上传断点续传 , 下面就分别介绍这两大概念.

演示如下图:

r3v7t-bckax

分片上传

​ 分片如图所示:

image-20231228161211118

​ 简单来说就如下几个步骤:

  1. 首先获取到选择文件的唯一标识符, 请求服务端查询该文件是否已经上传,如已经上传过返回文件地址, 结束上传功能**(这也就是文件秒传的原理)**
  2. 将需要上传的文件按照一定的分割规则,分割成小的切片;
  3. 循环上传每个分片数据(包含:分片数据, 分片索引, 分片唯一标识, 分片大小等),返回本次分片上传的索引;
  4. 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件并返回文件访问地址。

通过以上几个步骤从而化解大文件上传响应慢,服务器带宽地址, 服务器需要更大的资源去接收等弊端.

断点续传

​ 在上传大的文件时, 如果出现了网络问题, 整个请求都会断掉, 在下一次上传时就会从头又开始上传, 有可能就会陷入死循环大的文件永远都无法上传成功.

​ 能实现断点续传功能, 它是离不开上面讲的分片上传功能的, 分片上传在网络异常时, 它只会中断后面未上传成功的部分, 在网络恢复重新上传时, 它就只会接着上次上传中断时的索引接着传, 从而节约上传时间, 提高速度.

代码实现

前端核心代码:

前置要求:

需要获取文件的唯一标识符, 这里使用的是md5:

yarn add spark-md5
yarn add @types/spark-md5

工具函数

import { Ref, ref } from "vue";
import { checkFileApi, uploadFileApi } from "../api/upload.ts";
import { ResultData } from "./request.ts";
import { CheckFileRes } from "../api/types.ts";
import { ElMessage } from "element-plus";
import SparkMD5 from "spark-md5";

export interface UploadFile {
    name: string,
    size: number,
    parsePercentage: Ref,
    uploadPercentage: any,
    uploadSpeed: string,
    chunkList?: number[],
    file: File,
    uploadingStop: boolean,
    md5?: string,
    needUpload?: boolean,
    fileName?: string
}


export const initChunk = () => {

    const currentUploadFile = ref<UploadFile>()!


    const checkFile = async (file: File, back: Function) => {
        const uploadFile: UploadFile = {
            name: file.name,
            size: file.size,
            parsePercentage: ref<number>(0),
            uploadPercentage: ref<number>(0),
            uploadingStop: false,
            uploadSpeed: '0 M/s',
            chunkList: [],
            file: file,
        }
        back(uploadFile)
        currentUploadFile.value = uploadFile;
        const md5: string = await computeMd5(file, uploadFile)
        if (!md5) {
            console.log("转md5失败")
            return
        }
        uploadFile.md5 = md5;
        const res: ResultData<CheckFileRes> = await checkFileApi(md5);
        if (!res.data?.uploaded) {
            uploadFile.chunkList = res.data?.chunkList;
            uploadFile.needUpload = true;
        } else {
            uploadFile.needUpload = false;
            uploadFile.uploadPercentage.value = 100;
            uploadFile.fileName = res.data.fileName
            console.log("文件已秒传");
            ElMessage({
                showClose: true,
                message: "文件已秒传",
                type: "warning",
            });
        }
    }

    const uploadFile = async (file: File) => {
        const uploadParam = currentUploadFile.value;
        if (!uploadParam) {
            throw Error('请先调用 [checkFile] 方法')
        }
        currentUploadFile.value = uploadParam;
        if (uploadParam?.needUpload) {
            // 分片上传文件
            // 确定分片的大小
            await uploadChunk(file, 1, uploadParam);
            clear()
        }

    }

    const changeUploadingStop = async (uploadFile: UploadFile) => {
        uploadFile.uploadingStop = !uploadFile.uploadingStop;
        if (!uploadFile.uploadingStop) {
            await uploadChunk(uploadFile.file, 1, uploadFile);
        }
    }

    const clear = () => {
        currentUploadFile.value = undefined
    }
    return {checkFile, uploadFile, changeUploadingStop};
}

const uploadChunk = async (file: File, index: number, uploadFile: UploadFile) => {
    const chunkSize = 1024 * 1024 * 10; //10mb
    const chunkTotal = Math.ceil(file.size / chunkSize);
    if (index <= chunkTotal) {
        // 根据是否暂停,确定是否继续上传

        // console.log("4.上传分片");

        const startTime = new Date().valueOf();

        const exit = uploadFile?.chunkList?.includes(index);
        if (!exit) {
            if (!uploadFile.uploadingStop) {
                // 分片上传,同时计算进度条和上传速度
                const form = new FormData();
                const start = (index - 1) * chunkSize;
                let end =
                    index * chunkSize >= file.size ? file.size : index * chunkSize;
                let chunk = file.slice(start, end);

                form.append("chunk", chunk);
                form.append("index", index + "");
                form.append("chunkTotal", chunkTotal + "");
                form.append("chunkSize", chunkSize + "");
                form.append("md5", uploadFile.md5!);
                form.append("fileSize", file.size + "");
                form.append("fileName", file.name);

                const res = await uploadFileApi(form)
                if (res.code === 200) {
                    uploadFile.fileName = res.data as string
                }
                const endTime = new Date().valueOf();
                const timeDif = (endTime - startTime) / 1000;
                uploadFile.uploadSpeed = (10 / timeDif).toFixed(1) + " M/s";

                uploadFile.chunkList?.push(index);
                uploadFile.uploadPercentage = parseInt(String((uploadFile.chunkList!.length / chunkTotal) * 100));
                await uploadChunk(file, index + 1, uploadFile);

            }
        } else {
            uploadFile.uploadPercentage = parseInt(String((uploadFile.chunkList!.length / chunkTotal) * 100));
            await uploadChunk(file, index + 1, uploadFile);
        }
    }
}

function computeMd5(file: File, uploadFile: UploadFile): Promise<string> {
    return new Promise((resolve, _reject) => {
        //分片读取并计算md5
        const chunkTotal = 100; //分片数
        const chunkSize = Math.ceil(file.size / chunkTotal);
        const fileReader = new FileReader();
        const md5 = new SparkMD5();
        let index = 0;
        const loadFile = (uploadFile: UploadFile) => {
            uploadFile.parsePercentage.value = parseInt(((index / file.size) * 100) + '');
            const slice: Blob = file.slice(index, index + chunkSize);

            fileReader.readAsBinaryString(slice);
        };
        loadFile(uploadFile);
        fileReader.onload = (e) => {
            md5.appendBinary(e.target?.result as string);
            if (index < file.size) {
                index += chunkSize;
                loadFile(uploadFile);
            } else {
                resolve(md5.end());
            }
        };
    });
}

vue文件

<script setup lang="ts">

import { initChunk, UploadFile } from "./utils/chunkUpload.ts";
import { UploadRequestOptions } from "element-plus";
import { reactive } from "vue";
import { VideoPause, VideoPlay } from "@element-plus/icons-vue";

const fileList = reactive<UploadFile []>([])

const {checkFile, uploadFile, changeUploadingStop} = initChunk();
const colors = [
  {color: '#f56c6c', percentage: 20},
  {color: '#e6a23c', percentage: 40},
  {color: '#5cb87a', percentage: 60},
  {color: '#1989fa', percentage: 80},
  {color: '#6f7ad3', percentage: 100},
]

const uploadColors = [
  {color: '#f56c6c', percentage: 20},
  {color: '#e6a23c', percentage: 40},
  {color: '#5cb87a', percentage: 60},
  {color: '#08916c', percentage: 80},
  {color: '#0cf52a', percentage: 100},
]

const upload = async (data: UploadRequestOptions) => {
  const file = data.file;
  await uploadFile(file);

}

const beforeUpload = async (file: File) => {
  await checkFile(file, (uploadFile: UploadFile) => {
    fileList.push(uploadFile)
  })
}
</script>

<template>

  <div class="container">
    <div class="left">
      <div style="display: flex; justify-content: center;
      background: linear-gradient(-225deg, rgba(112,133,182,0.49) 0%, #87A7D9 50%, #DEF3F8 100%);
      ;font-size: 25px;
      ">
        <h3>文件分片上传</h3>
      </div>
      <div style="height: 100vh;display: flex;
  justify-content: center;
  align-items: center;">
        <el-upload
            action="#"
            :http-request="upload"
            :before-upload="beforeUpload"
            :show-file-list="false"
        >
          <div class="upload-pic">
            <div style="font-size: 20px">+</div>
            <div class="ant-upload-text">Upload</div>
          </div>
        </el-upload>
      </div>
    </div>
    <div class="right">
      <div style="padding: 30px">
        <h3 style="margin: 20px 0">上传文件列表</h3>
        <el-table
            :data="fileList"
            style="width: 100%"
            stripe
        >
          <el-table-column label="文件名称">
            <template #default="{row}">
              {{ row.name }}
            </template>
          </el-table-column>
          <el-table-column prop="size" label="文件大小">
            <template #default="{row}">
              {{ row.size }}
            </template>
          </el-table-column>
          <el-table-column prop="uploadSpeed" label="上传速率">
            <template #default="{row}">
              {{ row.uploadSpeed }}
            </template>
          </el-table-column>
          <el-table-column prop="parsePercentage" label="解析进度">
            <template #default="{row}">
              <div class="progress-bar">
                <el-progress
                    striped
                    striped-flow
                    :stroke-width="14"
                    :duration="20"
                    :color="colors" :percentage="row.parsePercentage" text-inside/>
              </div>
            </template>
          </el-table-column>
          <el-table-column prop="parsePercentage" label="上传进度">
            <template #default="{row}">
              <div class="progress-bar">
                <el-progress
                    striped
                    striped-flow
                    :stroke-width="14"
                    :duration="20"
                    :color="uploadColors" :percentage="row.uploadPercentage" text-inside/>
              </div>
            </template>
          </el-table-column>

          <el-table-column prop="Operation" label="操作">
            <template #default="{row}">
              <el-button circle link @click="changeUploadingStop(row)"
                         v-if="row.uploadPercentage >0 && row.uploadPercentage <100"
              >
                <el-icon size="20" v-if="row.uploadingStop === false"
                >
                  <VideoPause
                  />
                </el-icon>
                <el-icon size="20" v-else>
                  <VideoPlay/>
                </el-icon>
              </el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
    </div>
  </div>

</template>

<style scoped>
.container {
  width: 100%;
  height: 100vh;
  display: flex;
}

.left {
  flex: 1;
  background-color: #fffcfe;
}

.right {
  flex: 3;
  background-color: rgba(154, 180, 185, 0.12);
}



.progress-bar {
  width: 100%;
}


</style>

后端核心代码:

控制层

/**
* 文件上传接口
*/
@Slf4j
@RestController
@RequestMapping("/upload")
@RequiredArgsConstructor
public class UploadFIleController {

   private final WFileService wFileService;


   @GetMapping("/check")
   public AjaxResult<CheckVo> checkFile(@RequestParam("md5") String md5) {
       log.info("MD5值:" + md5);
       return wFileService.checkFile(md5);
   }

   /**
    * form-data传参时 @ModelAttribute 注解必须标记, 否则报错No primary or single unique constructor found for class
    *
    * @param dto 请求参数
    * @return 返回结果
    */
   @PostMapping("/chunk")
   public AjaxResult<Object> uploadChunk(@ModelAttribute UploadChunkDto dto) {
       return wFileService.uploadChunk(dto);
   }

}

service层

/**
 * @author wdhcr
 * 上传文件表服务层
 * @date 2023-11-22
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class WFileServiceImpl extends ServiceImpl<WFileMapper, WFile> implements WFileService {

    private final WFileChunkService wFileChunkService;

    private final WdhcrProperties wdhcrProperties;

    @Override
    public AjaxResult<CheckVo> checkFile(String md5) {
        CheckVo checkVo = new CheckVo();

        //首先检查是否有完整的文件
        WFile wFile = getOne(new LambdaQueryWrapper<WFile>()
                .eq(WFile::getMd5, md5)
                .last("limit 1"));
        if (!ObjectUtils.isEmpty(wFile)) {
            //存在,就是秒传
            checkVo.setUploaded(true);
            checkVo.setFileName(wFile.getFileName());
            return AjaxResult.success(checkVo);
        }
        List<WFileChunk> chunks = wFileChunkService.list(new LambdaQueryWrapper<WFileChunk>()
                .eq(WFileChunk::getMd5, md5));
        List<Integer> chunkIndexes = Optional.ofNullable(chunks)
                .orElseGet(ArrayList::new)
                .stream().map(WFileChunk::getChunkIndex)
                .toList();
        checkVo.setChunkList(chunkIndexes);
        return AjaxResult.success(checkVo);
    }

    @Override
    public AjaxResult<Object> uploadChunk(UploadChunkDto dto) {
        String fileName = dto.getFileName();
        MultipartFile chunk = dto.getChunk();
        Integer index = dto.getIndex();
        Long chunkSize = dto.getChunkSize();
        String md5 = dto.getMd5();
        Integer chunkTotal = dto.getChunkTotal();
        Long fileSize = dto.getFileSize();

        String[] splits = fileName.split("\\.");
        String type = splits[splits.length - 1];
        String filePath = wdhcrProperties.getFilepath();
        String resultFileName = filePath + md5 + "." + type;

        wFileChunkService.saveChunk(chunk, md5, index, chunkSize, resultFileName);
        log.info("上传分片:索引:" + index + " , 总数: " + chunkTotal + ",文件名称" + fileName + ",存储名称" + resultFileName);
        if (Objects.equals(index, chunkTotal)) {
            WFile wFile = new WFile();
            wFile.setName(fileName);
            wFile.setMd5(md5);
            wFile.setFileName(resultFileName);
            wFile.setSize(fileSize);
            save(wFile);
            wFileChunkService.remove(new LambdaQueryWrapper<WFileChunk>()
                    .eq(WFileChunk::getMd5, md5));
            return AjaxResult.success("文件上传成功", resultFileName);
        } else {
            return new AjaxResult<>(201, "文件分片上传成功", index);
        }
    }
}
/**
 * 文件分片表服务层
 *
 * @author wdhcr
 * @date 2023-11-22
 */
@Service
public class WFileChunkServiceImpl extends ServiceImpl<WFileChunkMapper, WFileChunk> implements WFileChunkService {


    @Override
    public boolean saveChunk(MultipartFile chunk, String md5, Integer index, Long chunkSize, String resultFileName) {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(resultFileName, "rw")) {


            // 偏移量
            long offset = chunkSize * (index - 1);
            // 定位到该分片的偏移量
            randomAccessFile.seek(offset);
            // 写入
            randomAccessFile.write(chunk.getBytes());

            WFileChunk wFileChunk = new WFileChunk();
            wFileChunk.setMd5(md5);
            wFileChunk.setChunkIndex(index);
            return save(wFileChunk);
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }
}
以上就是实现文件分片上传和断点续传的核心代码.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/277869.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

四阶轨迹规划,高精度电机运动控制必备

最近开始接触百纳米级别的控制精度&#xff0c;为了达到这一精度&#xff0c;必须使用高阶的轨迹规划。 四阶轨迹规划的作用是把一个阶跃信号变为一个四阶连续信号。 例如一个电机&#xff0c;想让它从a点移动到b点&#xff0c;那么给出的参考信号在位置&#xff0c;速度&…

灸哥问答:架构师如何提高自己的竞争力?

这个问题其实从我的主题【程序员如何转型架构师】所述内容中是能看到答案的&#xff0c;这里针对这个问题做一次总结性的回复&#xff1a; 1、 深入理解业务领域 充分理解你所在公司和行业的业务需求&#xff0c;运用 DDD 的业务分析思想和手段&#xff0c;构建更贴近实际业务…

悔不该用中文作为Windows的用户名啊~

前言 汉字在中华文明已经有了几千年的历史&#xff0c;小伙伴们所使用名字更是伴随了自己一生。所以小白们在拿到自己的新电脑&#xff0c;总会想着把自己的中文名字设置为电脑的用户名&#xff0c;这样更能显示出那是自己的专属电脑&#xff01; 一开始小白也是这么想的&…

【JavaScript】new原理解析

✨ 专栏介绍 在现代Web开发中&#xff0c;JavaScript已经成为了不可或缺的一部分。它不仅可以为网页增加交互性和动态性&#xff0c;还可以在后端开发中使用Node.js构建高效的服务器端应用程序。作为一种灵活且易学的脚本语言&#xff0c;JavaScript具有广泛的应用场景&#x…

Python+OpenGL绘制3D模型(七)制作3dsmax导出插件

系列文章 一、逆向工程 Sketchup 逆向工程&#xff08;一&#xff09;破解.skp文件数据结构 Sketchup 逆向工程&#xff08;二&#xff09;分析三维模型数据结构 Sketchup 逆向工程&#xff08;三&#xff09;软件逆向工程从何处入手 Sketchup 逆向工程&#xff08;四&#xf…

Zookeeper之快速入门

前言 本篇文章主要还是让人快速上手入门&#xff0c;想要深入的话可以通过书籍系统的学习。 简介 是什么 可用于协调、构建分布式应用。 本质上是一个分布式的小文件存储系统。提供基于类似于文件系统的目录树方式的数据存储&#xff0c;并且可以对树中的节点进行有效管理…

Unity中Shader裁剪空间推导(在Shader中使用)

文章目录 前言一、在Shader中使用转化矩阵1、在顶点着色器中定义转化矩阵2、用 UNITY_NEAR_CLIP_VALUE 区分平台矩阵3、定义一个枚举用于区分当前是处于什么相机 二、我们在DirectX平台下&#xff0c;看看效果1、正交相机下2、透视相机下3、最终代码 前言 在上一篇文章中&…

Linux驱动开发学习笔记6《蜂鸣器实验》

目录 一、蜂鸣器驱动原理 二、硬件原理分析 三、实验程序编写 1、 修改设备树文件 &#xff08;1&#xff09;添加pinctrl节点 &#xff08;2&#xff09;添加BEEP设备节点 &#xff08;3&#xff09;检查PIN 是否被其他外设使用 2、蜂鸣器驱动程序编写 3、编写测试AP…

【JS逆向学习】快乐学堂

逆向目标 登陆接口&#xff1a;https://www.91118.com/passport/Account/LoginPost?r0.20790763112591337&kdsyes&username13127519353&passbb3mlkFBqqo%3D&recordPwd1&ckcode5719&fscodeklxt&invite 加密参数&#xff1a; r&#xff1a;0.2079…

机器学习模型可解释性的结果分析

模型的可解释性是机器学习领域的一个重要分支&#xff0c;随着 AI 应用范围的不断扩大&#xff0c;人们越来越不满足于模型的黑盒特性&#xff0c;与此同时&#xff0c;金融、自动驾驶等领域的法律法规也对模型的可解释性提出了更高的要求&#xff0c;在可解释 AI 一文中我们已…

Linux开发工具——gdb篇

Linux下调试工具——gdb 文章目录 makefile自动化构建工具 gdb背景 gdb的使用 常用命令 总结 前言&#xff1a; 编写代码我们使用vim&#xff0c;编译代码我们使用gcc/g&#xff0c;但是我们&#xff0c;不能保证代码没问题&#xff0c;所以调试是必不可少的。与gcc/vim一样&…

Python中使用SQLite数据库的方法2-2

3.3.2 创建表单及字段 通过“3.2 创建Cursor类的对象”中创建的Cursor类的对象cur创建表单及字段&#xff0c;代码如图5所示。 图5 创建表单及字段 从图5中可以看出&#xff0c;通过Cursor类的对象cur调用了Cursor类的execute()方法来执行SQL语句。该方法的参数即为要指定的S…

在电商行业中,如何采集电商数据使用数据分析提高业务绩效

数据分析丨知识点丨电商数据采集 福利指路&#xff1a;文章底部领取《数据分析全家桶》 随着电子商务的不断发展&#xff0c;越来越多的企业开始使用数据分析来提高业务绩效。数据分析可以帮助电商企业更好地理解市场和客户&#xff0c;以制定更有针对性的营销策略和产品方案。…

ksuser.dll文件缺失怎么办?软件或游戏无法启动,一键自动修复

很多小伙伴反馈&#xff0c;自己的电脑中了病毒&#xff0c;被杀毒软件清理后&#xff0c;在打开游戏或软件的时候&#xff0c;经常会报错“提示无法找到ksuser.dll文件&#xff0c;建议重新安装软件或游戏”。自己根据提示重装后&#xff0c;还是报错&#xff0c;不知道应该怎…

两向量叉乘值为对应平行四边形面积--公式推导

两向量叉乘值为对应平行四边形面积--公式推导 介绍 介绍

[电磁学]大学物理陈秉乾老师课程笔记

主页有博主其他上万字的精品笔记,都在不断完善ing~ 第一讲 绪论,库仑定律 主要讲解了电磁学中的库伦定律和电场的相关概念&#xff0c;介绍了电荷和电磁相互作用的规律&#xff0c;并讲解了电场强度和电势的概念。 03:14 &#x1f393; 库伦定律&#xff1a;电势能与电荷的关…

【JAVA核心知识】分布式事务框架Seata

Seata 基本信息 GitHub&#xff1a;https://github.com/seata/seatastars: 20.6k 最新版本&#xff1a; v1.6.1 Dec 22, 2022 官方文档&#xff1a;http://seata.io/zh-cn/index.html 注意 官方仅仅支持同步调用。 官方在FAQ中表示对于异步框架需要自行支持。 具体的扩展思…

【Maven】<scope>provided</scope>

在Maven中&#xff0c;“provided”是一个常用的依赖范围&#xff0c;它表示某个依赖项在编译和测试阶段是必需的&#xff0c;但在运行时则由外部环境提供&#xff0c;不需要包含在最终的项目包中。下面是对Maven scope “provided”的详细解释&#xff1a; 编译和测试阶段可用…

关于2024年度PMI认证考试计划的通知

尊敬的考生&#xff1a; 经PMI和中国国际人才交流基金会研究决定&#xff0c;2024年度中国大陆地区计划举办四次PMI认证考试&#xff0c;3月、6月、8月、11月各举办一次&#xff0c;具体考试日期另行公布。如遇特殊情况需变更考试计划的&#xff0c;将提前另行通知。 PMI&#…

Ubuntu安装K8S(1.28版本,基于containrd)

原文网址&#xff1a;Ubuntu安装K8S(1.28版本&#xff0c;基于containrd&#xff09;-CSDN博客 简介 本文介绍Ubuntu安装K8S的方法。 官网文档&#xff1a;这里 1.安装K8S 1.让apt支持SSL传输 sudo apt-get update sudo apt-get -y install apt-transport-https ca-certi…