SpringBoot+Vue实现大文件上传(断点续传-后端控制(一))

SpringBoot+Vue实现大文件上传(断点续传)

1 环境 SpringBoot 3.2.1,Vue 2,ElementUI,spark-md5
2 问题 在前一篇文章,我们写了通过在前端控制的断点续传,但是有两个问题,第一个问题:如果上传过程中,页面意外关闭或者其他原因,导致上传者不知道该文件是否上传成功,则会重复上传;第二个问题,我们将文件分片后,如果分片较多,我们一个一个的上传文件块,效率还是比较低。
3方案 基于前面的问题分析,我们可以将部分判断改到后端。针对第一个问题,我们可以保存每个分片的信息,如果下次再上传相同的文件时发现文件已存在且分片全部上传时,则可直接跳过,存在分片未全部上传时,返回未上传的分片下标;第二个问题,我们前端不再采用异步上传,而是多个分片同时上传,可以较高提升上传速度。本文我们先看下第一个问题怎么解决。

效果图
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
这里我们计算文件MD5值用的是spark-md5,首先需要在控制台执行安装命令:npm install --save spark-md5
然后在需要用的文件里引入:import SparkMD5 from "spark-md5";

前端代码
前端主要做的就是计算文件md5值,跟后端交互查询文件是否已上传,再根据情况将未上传的分片上传。

<template><div class="container"><el-uploadclass="upload-demo"dragmultipleaction="/xml/fileUpload":on-change="handleChange":auto-upload="false"><i class="el-icon-upload"></i><div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div><div class="clearfix"></div><div class="el-upload__tip"><el-progress :style="{ width: percentage + '%' }" :text-inside="true":stroke-width="24":percentage="percentage" :status="uploadStatus"></el-progress></div></el-upload><el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload">上传到服务器</el-button></div>
</template><script>
import axios from "axios";
import SparkMD5 from "spark-md5";export default {name: 'App',data() {return {file: '',fileList: [],CHUNK_SIZE: 1024 * 1024 * 5,//100MBpercentage: 0,chunkNo: 0,uploadStatus: ''}},watch: {},created() {},methods: {async fileHash(file) {// 1 第一种 计算文件的md5值,可以基于文件的一些基本属性来计算,不过这个存在的问题很明显,就是如果改了内容而文件大小不变的情况下,算出来的md5是一样的,优点就是计算速度快// const fileName = file.name// const fileType = file.name.substring(file.name.lastIndexOf(".") + 1, file.name.length);// const fileSize = file.size// console.log(SparkMD5.hash(fileName + fileType + fileSize))//--------------------------------------------------------------------// 2 第二种读取文件内容来计算,这种方式的优点就是只要文件内容改了算出来的md5值就不一样,缺点就是如果文件太大,一次性读到内存中计算的话会占内存,可能造成卡顿,计算速度相对较慢,// 我们可以增量计算,先计算第一个文件块的hash值,再将这个值和第二个文件块一起计算,如此下去,最终获取整个文件的hash,这样每次只读取一个文件块到内存中//注意此处不能直接在循环里写读取文件,那样会报错:Uncaught (in promise) DOMException: Failed to execute 'readAsArrayBuffer' on 'FileReader': The object is already busy reading Blobs.// 原因:因为每次循环中很快地连续调用 readAsArrayBuffer ,可能上一次的读取还未完成,新的调用就来了,导致冲突。//下面两种写法,一种是封装成异步的,一种是递归调用const totalChunks = Math.ceil(file.size / this.CHUNK_SIZE);const spark = new SparkMD5();for (let i = 0; i < totalChunks; i++) {await new Promise((resolve) => {const start = i * this.CHUNK_SIZE;const end = Math.min(start + this.CHUNK_SIZE, file.size);const chunk = file.slice(start, end);const fileReader = new FileReader();// reader.onload 是为 FileReader 对象的 load 事件添加一个回调函数。当文件读取完成后,这个回调会被触发。// e 是事件对象。// e.target.result 获取到的就是读取文件后得到的结果数据,在这里是一个 ArrayBuffer 对象,它包含了文件的二进制数据。// 将 reader.onload 的处理逻辑写在读取文件操作之前// 这样做是为了提前定义好当文件读取完成这个事件发生时要执行的具体动作。在执行 reader.readAsArrayBuffer(file) 开始读取文件后,一旦读取完成,就会触发 onload 事件,从而执行之前定义好的回调函数。// 如果把这个处理逻辑放在后面,可能会导致在需要使用读取结果时,还没有正确地设置好处理的方式。// 先设置好回调,再触发相关操作,能确保整个流程的逻辑顺序和正确性。fileReader.onload = (e) => {//读取的字节数组const bytes = e.target.result;//增量计算spark.append(bytes);// resolve() 用于在异步操作完成时通知 Promise 状态变为已完成(fulfilled)//当文件读取的 onload 事件触发,表示当前这一块数据读取完成,此时调用 resolve() 来让等待这个 Promise 的后续代码知道可以继续进行下一步操作了。这样就实现了对异步读取过程的有序控制。resolve();};//读取文件块内容fileReader.readAsArrayBuffer(chunk);});}return spark.end()//---------------------------------------------------------------------------------//递归调用的写法// return new Promise((resolve) => {//   const spark = new SparkMD5();//   const totalChunks = Math.ceil(file.size / this.CHUNK_SIZE);////   function hash(index, CHUNK_SIZE) {//     if (index >= totalChunks) {//       //返回最终的结果 在调用的地方获取值:const result = await hash(); result 就是最后的md5值//       resolve(spark.end());//       return//     }//     const start = index * CHUNK_SIZE;//     const end = Math.min(start + CHUNK_SIZE, file.size);//     const chunk = file.slice(start, end);//     const read = new FileReader();//     read.onload = (e) => {//       //读取的字节数组//       const bytes = e.target.result//       spark.append(bytes)//       //递归调用//       hash(index + 1,)//     }//     read.readAsArrayBuffer(chunk)//   }////   //开始第一次计算//   hash(0, this.CHUNK_SIZE)// })},async submitUpload() {//获取上传的文件信息const file = this.fileList[0].raw//生成md5值const md5 = await this.fileHash(file)console.log("文件MD5值:" + md5)const totalChunks = Math.ceil(file.size / this.CHUNK_SIZE);let startIndex = 0;const res = await axios.get('/xml/checkMD5?md5='+md5+'&totalChunks='+totalChunks)if(res.data.code === 200){if(res.data.data.startIndex<0){this.percentage = 100this.$message({message: '文件已上传!',type: 'warning'});return}startIndex = res.data.data.startIndexthis.percentage = Math.ceil(startIndex / totalChunks * 100)}else {this.$message({message: '上传失败,请重试!',type: 'error'});return}//分片this.uploadStatus = 'success'for (let i = startIndex; i < totalChunks; i++) {const start = i * this.CHUNK_SIZE;const end = Math.min(start + this.CHUNK_SIZE, file.size);//将文件切片const chunk = file.slice(start, end);//组装参数const formData = new FormData();formData.append('file', chunk);formData.append('fileName', file.name);formData.append('md5', md5);formData.append('index', i);formData.append('status', 0);try {const res = await axios.post('/xml/bigFileUpload', formData)if (res.data.code === 200) {this.percentage = Math.ceil((i + 1) / totalChunks * 100)this.chunkNo = i + 1} else {this.$message({message: '上传失败',type: 'error'});this.errText = '失败'this.uploadStatus = 'exception'return}} catch (err) {console.log(err);this.$message.error('上传失败');this.uploadStatus = 'exception'return}}//调用合并分片请求await fetch('/xml/merge', {method: 'POST',body: JSON.stringify({fileName: file.name}),headers: {'Content-Type': 'application/json'}});this.$message({message: '文件上传成功!',type: 'success'});},handleChange(file, fileList) {this.fileList = fileList},}
}
</script><style>
.container {display: flex;
}.progress-bar {position: absolute;height: 100%;background-color: #03f80d;transition: width 0.5s ease; /* 平滑过渡效果 */
}.progress-number {position: absolute;right: 5px;top: 0;color: white;transition: opacity 0.5s ease; /* 文字的平滑过渡效果 */
}
</style>

后端代码

后端代码相较之前的多了一步,就是在分片上传时保存分片的一些信息。
如整个文件的md5值、文件名称、文件类型、分片导入时间等,可以根据需要另外增加字段。主要的逻辑就是,首先在前端计算文件的md5,然后跟后端交互,查询该文件是否已上传过,上传过多少分片,如果上传分片的条数跟前端计算的分片数一样,返回-1,否则返回上传分片的条数,前端根据这个数判断是否还需要上传、从哪个分片开始上传,这样就避免了上传过的分片重复上传。
注意:我这里采用的是通过文件内容来计算md5值,所以,如果只是修改了文件名称,计算出来的值是一样的,会认为是同一个文件。

package org.wjg.onlinexml.controller;import io.micrometer.common.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.wjg.onlinexml.po.Result;
import org.wjg.onlinexml.po.ResultData;
import org.wjg.onlinexml.po.SysFilePo;
import org.wjg.onlinexml.service.SysFileService;import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.util.Map;@RestController
public class BigFileControll {// 获取资源文件夹的路径,路径为 项目所在路径/upload/private static final String UPLOAD_DIR = System.getProperty("user.dir") + "/upload/";@Autowiredprivate SysFileService sysFileService;private static String getSuffix(String filePath) {int dotIndex = filePath.lastIndexOf('.');if (dotIndex > 0 && dotIndex < filePath.length() - 1) {return filePath.substring(dotIndex + 1);}return "";}/*** 保存分片** @param file* @param fileName* @param index* @return*/@RequestMapping("/bigFileUpload")private Result bigFileUpload(@RequestParam("file") MultipartFile file, @RequestParam("fileName") String fileName,@RequestParam("md5") String md5, @RequestParam("index") int index, @RequestParam("status") int status) {if (file.isEmpty()) {return Result.builder().code(500).msg("上传失败!").build();}File uploadDir = new File(UPLOAD_DIR);if (!uploadDir.exists()) {uploadDir.mkdirs();}File uploadFile = new File(UPLOAD_DIR + fileName + "_" + index);try {//模拟上传中断-----------------------if (status == 1) {if (index == 2) {return Result.builder().code(500).msg("上传失败").build();}}//-------------------结束------------------file.transferTo(uploadFile);SysFilePo sysFile = SysFilePo.builder().fileName(fileName).fileType(getSuffix(fileName)).md5(md5).chunkIndex(index).build();sysFileService.insert(sysFile);} catch (Exception e) {e.printStackTrace();return Result.builder().code(500).msg("上传失败").build();}return Result.builder().code(200).msg("上传成功").build();}/*** 合并分片** @param request* @return*/@PostMapping("/merge")public Result mergeChunks(@RequestBody Map<String, String> request) {String filename = request.get("fileName");File mergedFile = new File(UPLOAD_DIR + filename);try (FileOutputStream fos = new FileOutputStream(mergedFile)) {//循环获取分片,直到分片不存在为止for (int i = 0; ; i++) {File chunkFile = new File(UPLOAD_DIR + filename + "_" + i);if (!chunkFile.exists()) {break;}//将分片复制到一个文件中,这种方法会追加Files.copy(chunkFile.toPath(), fos);//删除分片chunkFile.delete();}} catch (Exception e) {return Result.builder().code(500).msg("合并失败").build();}return Result.builder().code(200).msg("合并成功").build();}@GetMapping("/checkMD5")public Result mergeChunks(@RequestParam("md5") String md5, @RequestParam("totalChunks") int totalChunks) {if (StringUtils.isBlank(md5)) {return Result.builder().code(500).msg("上传的文件md5值为空!").build();}try {int startIndex = sysFileService.checkMD5(md5, totalChunks);return Result.builder().code(200).msg("MD5校验成功").data(ResultData.builder().startIndex(startIndex).build()).build();} catch (Exception e) {e.printStackTrace();return Result.builder().code(500).msg("校验文件md5值出错!").build();}}
}

Result 类

@Data
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
public class Result {private int code;private String msg;private ResultData data;
}

ResultData 类

@Data
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
public class ResultData {private int startIndex;
}

SysFilePo类

@Data
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
public class SysFilePo {private String md5;private String fileName;private String fileType;private int chunkIndex;
}
    <select id="checkMD5" resultType="java.lang.Integer">select count(0) from sys_file<where><if test="md5!=null and md5 !=''">md5 = #{md5,jdbcType=VARCHAR}</if></where></select><insert id="insert">insert into sys_file(md5, file_name, file_type, chunk_index, insert_time, update_time)values (#{md5}, #{fileName}, #{fileType}, #{chunkIndex}, SYSDATE(), SYSDATE())</insert>

数据库脚本
可根据自身情况修改及增加主键。

CREATE TABLE `sys_file` (`md5` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '文件md5值',`file_name` varchar(255) DEFAULT NULL COMMENT '文件名称',`file_type` varchar(255) DEFAULT NULL COMMENT '文件类型',`chunk_index` int(11) DEFAULT NULL COMMENT '分片下标',`insert_time` datetime DEFAULT NULL COMMENT '插入时间',`update_time` datetime DEFAULT NULL COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

总结:本篇相较于上篇,将上传的分片信息保存在数据库中了,这样页面刷新或者上传相同的文件时,可避免重复上传已经上传过的。其实本篇的写法对于一般情况已经满足,不过如果是超大文件,好几个g或者几十g的文件,有点不适用。问题一,计算md5会耗时较长,我们可以使用 webWorker 单独开线程去计算;问题二,就是一开始提到的,分片太多的时候,我们一个个上传太耗时,效率低,我们需要使用并发请求,这两个解决方案会放到下片文章。再次强调,前后端代码写的逻辑性不强,只为展示基础的用法,在实际使用时,可基于实际需求加以优化。后端代码部分为测试所用,已标注,请注意删除!!!

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AUTOSAR Adaptive与智能汽车E/E架构发展趋势

AUTOSAR Adaptive是一个面向现代汽车应用需求的标准&#xff0c;特别适用于那些需要高计算能力和灵活性的应用。以下是AUTOSAR Adaptive的典型特性&#xff1a; 高计算能力&#xff1a;AUTOSAR Adaptive支持使用MPU&#xff08;微处理器&#xff09;&#xff0c;这些处理器的性…

嵌入式开发学习路线(25届校招学习) 嵌入式学习路线七年规划:从大一小白到校招大佬 (学习路线汇总)

嵌入式开发学习路线&#xff08;25届校招可以参考&#xff09; 嵌入式系统作为当前最热门且最有发展前途的IT应用领域之一&#xff0c;吸引了大量有志于从事该行业的学习者。为了系统地掌握嵌入式开发技能&#xff0c;以下是一条详细的学习路线&#xff0c;旨在帮助初学者逐步…

CodeSys中动态切换3D模型

文章目录 需求研究结果 需求 在前面的【CodeSys开发3d机械臂显示控件】中&#xff0c;我们已经实现了一个可以显示3d模型的控件。但是这个控件是和使用的3d模型绑定死的&#xff0c;在安装这个控件时就已经将模型文件于控件一起安装到codesys中。 假如我想在不同的工程中&…

智能家居系统(基于STM32F103C8T6标准库+FreeRTOS+Qt串口开发实现)

视频演示&#xff1a;基于STM32F103C8T6标准库FreeRTOSQt串口开发实现的智能家居项目_哔哩哔哩_bilibili 基于STM32F103C8T6标准库FreeRTOSQt串口开发实现的智能家居项目: https://pan.baidu.com/s/1f41gAfOOnlcQoKoMx3o84A?pwd6j2g 提取码: 6j2g 注&#xff1a;本项目为学习完…

Meta关闭Spark AR平台:未来规划与影响分析

Meta宣布将关闭其移动AR创作平台Spark AR&#xff0c;这一消息在业界引起了广泛关注。尽管Snap和TikTok在AR滤镜领域取得了巨大成功&#xff0c;但Meta却选择了另一条发展道路。本文将探讨这一决策背后的可能原因及其对未来的影响。 关闭Spark AR平台的背后 硬件为主&#xff…

计算机网络(三) —— 简单Udp网络程序

目录 一&#xff0c;初始化服务器 1.0 辅助文件 1.1 socket函数 1.2 填充sockaddr结构体 1.3 bind绑定函数 1.4 字符串IP和整数IP的转换 二&#xff0c;运行服务器 2.1 接收 2.2 处理 2.3 返回 三&#xff0c;客户端实现 3.1 UdpClient.cc 实现 3.2 Main.cc 实现 …

【Mysql】系统服务启动访问报错问题处理:this is incompatible with sql_mode=only_full_group_by

一、背景&#xff1a; 本来已经正常运行的平台&#xff0c;突然有一天由于对服务器进行部分操作迁移&#xff0c;发现jar可以正常启动&#xff0c;但是访问功能一直报错&#xff0c;监控后台日志后&#xff0c;发现了问题&#xff1a; 报错的具体信息如下&#xff1a; Caused…

Linux编译器--gcc/g++使用

目录 一、预编译指令 1.1预处理功能 1.2指令 1.3问题扩展 二、编译&#xff08;生成汇编&#xff09; 三、汇编&#xff08;生成二进制机器语言&#xff09; 四、链接&#xff08;生成可执行文件或库文件&#xff09; 4.1库文件 4.2目标文件和库的链接 4.3动态库和静态…

【Django-Minio-Storage 使用教程】

Django-Minio-Storage 使用教程 安装 Django-Minio-Storage配置 Django 项目官方文档 安装 Django-Minio-Storage 使用 pip 安装 Django-Minio-Storage pip install django-minio-storage配置 Django 项目 在 Django 项目的 settings.py 文件中进行以下配置 INSTALLED_APPS…

【mysql】mysql修改sql_mode之后无法启动

现象&#xff1a;修改后mysql无法启动&#xff0c;不报错 原因&#xff1a;MySQL在8以后sql_mode已经取消了NO_AUTO_CREATE_USER这个关键字。去掉这个关键字后&#xff0c;启动就可以了 修改前&#xff1a; sql_modeSTRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR…

Bootstrap前端框架Glyphicons字体图标

115工具网收集提供Bootstrap前端框架Glyphicons字体图标库对照表​​​​​​​&#xff0c;Bootstrap前端UI,Glyphicons字体图标调用,Bootstrap按钮字体图标对照表,包括250多个来自Glyphicon Halflings的字体图标.项目中引用Bootstrap相关文件后即可直接调用下列图标class&quo…

Linux CentOS安装PySpark3.5(单机版)详细教程及机器学习实战

目录 一、安装须知 二、安装Spark 1、下载安装包 2、修改配置文件spark-env.sh 3、验证Spark是否安装成功 三、安装py4j 四、配置环境变量 五、基于PySpark的机器学习实战 1、将数据文件上传HDFS 2、创建代码文件 3、提交应用程序 一、安装须知 前置依赖&#xff1…

Acrobat Pro DC 2023 for Mac/Win:全能型PDF编辑器深度解析

Adobe Acrobat Pro DC 2023作为一款跨平台的PDF编辑器&#xff0c;无论是对于Mac还是Windows用户&#xff0c;都提供了极为全面且强大的PDF处理功能。该软件凭借其卓越的性能和丰富的特性&#xff0c;成为了全球范围内用户处理PDF文档的首选工具。 一、强大的编辑功能 Acroba…

【2024高教社杯全国大学生数学建模竞赛】ABCDEF题 问题分析、模型建立、参考文献及实现代码

【2024高教社杯全国大学生数学建模竞赛】ABCDEF题 问题分析、模型建立、参考文献及实现代码 1 比赛时间 北京时间&#xff1a;2024年9月5日 18:00-2024年9月8日20:00 2 思路内容 2.1 往届比赛资料 【2022高教社杯数学建模】C题&#xff1a;古代玻璃制品的成分分析与鉴别方案…

HBase 部署及shell操作

HBase 数据库 一、HBase 概述1.1 HBase 是什么HBase 的特点 二、HBase 模型及架构2.1 HBase 逻辑模型2.2 HBase 数据模型2.3 HBase 物理模型2.3.1 列簇物理模型2.3.2 Rowkey 字段排序2.3.3 Region 存储到不同节点2.3.4 Region 结构 2.4 HBase 基本架构 三、搭建 HBase 分布式集…

Claude的小白入门指南

要想快速上手Claude AI&#xff0c;其实并没有那么复杂。作为新一代的AI助手&#xff0c;Claude致力于为用户提供高效、无害、透明的交互体验。这篇入门指南将从Claude AI的特点、主要功能和如何实际操作等几个方面为大家做一个详细的介绍。 Claude AI是什么&#xff1f; Claud…

【SRC挖掘】越权漏洞——burp插件被动检测越权漏洞,一个插件让挖洞效率翻倍!Autorize

越权与未授权漏洞 越权漏洞什么是越权漏洞&#xff1f;Autorize插件安装使用步骤拦截过滤器 越权漏洞 什么是越权漏洞&#xff1f; 越权漏洞是指应用程序未对当前用户操作的身份权限进行严格校验&#xff0c;导致用户可以操作超出自己管理权限范围的功能&#xff0c;从而操作…

大模型笔记01--基于ollama和open-webui快速部署chatgpt

大模型笔记01--基于ollama和open-webui快速部署chatgpt 介绍部署&测试安装ollama运行open-webui测试 注意事项说明 介绍 近年来AI大模型得到快速发展&#xff0c;各种大模型如雨后春笋一样涌出&#xff0c;逐步融入各行各业。与之相关的各类开源大模型系统工具也得到了快速…

UnityShader自定义属性特性

前言&#xff1a; 在编写UnityShader时&#xff0c;我们常常会使用特性来更换材质球面板的属性外观&#xff0c;除此之外&#xff0c;还可以使用自定义的扩展脚本来实现自定义的材质球界面&#xff0c;参考我之前的文章UnityShaderUI编辑器扩展 但是自定义扩展每次都要单独写…

性能测试经典案例解析——远程培训系统

各位好&#xff0c;我是 道普云 一站式云测试SaaS平台。一个在软件测试道路上不断折腾十余年的萌新。 欢迎关注我的专栏和我的主页 道普云 文章内容具有一定门槛&#xff0c;建议先赞再收藏慢慢学习&#xff0c;有不懂的问题欢迎私聊我。 希望这篇文章对想提高软件测试水平…