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.mzph.cn/pingmian/53143.shtml

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

相关文章

铁打的程序员轻易“不哭”-我的大模型创业近2年来的感悟

楔子 2022年11月&#xff0c;GPT-3发布那一刻&#xff0c;我被AI的强大能力所震撼&#xff0c;意识到“超级个体”时代的来临。自那时起&#xff0c;我开始全心投入创业&#xff0c;经历了许多苦乐交织的时光。 2023年6月&#xff0c;我尝试将AI应用于智能营销导购&#xff0…

云原生架构概念

云原生架构概念 云原生架构&#xff08;Cloud Native Architechtrue&#xff09;作为一种现代软件开发的革新力量&#xff0c;正在逐渐改变企业构建、部署和管理应用程序的方式。它的核心优势在于支持微服务架构&#xff0c;使得应用程序能够分解为独立、松耦合的服务&#xf…

window系统怎么设置闹钟提醒?分享一个桌面提醒设置办法

在日常工作和生活中&#xff0c;我们常常会因忙碌而遗忘一些重要事项。对于很多使用电脑办公的用户来说&#xff0c;如果能在桌面上设置闹钟提醒&#xff0c;无疑会大大提高工作效率&#xff0c;减少遗漏。那么&#xff0c;如何设置这样的闹钟提醒呢&#xff1f; 这时&#xf…

ElementUI实现el-table组件的合并行功能

前言 有时遇到一些需求&#xff0c;需要实现ElementUI中&#xff0c;el-tabled组件合并单元格的功能&#xff0c;稍微了解一下它的数据格式&#xff0c;不难可以写出比合并方法。但是在鼠标经过单元行时&#xff0c;会出现高亮的行与鼠标经过的行不一致的BUG。因此还需要实现c…

8月刷题笔记

刷题笔记—8月 LCP40.心算挑战(贪心、排序) class Solution { public:int maxmiumScore(vector<int>& cards, int cnt) {//24.8.1ranges::sort(cards, greater()); //从大到小排序int s reduce(cards.begin(), cards.begin()cnt, 0);if(s%2 0) return s;auto rep…

无线麦克风哪个牌子的好,麦克风哪个好,无线麦克风品牌推荐

​在自媒体日益繁荣的当下&#xff0c;内容创作成为了许多人追求的目标。对于视频内容创作者而言&#xff0c;出色的内容是成功的基石&#xff0c;而高质量的设备则是保证作品品质的关键。为了提升视频音质&#xff0c;拥有一款专业的无线麦克风是不可或缺的。 然而&#xff0…

PHP智能匹配轻松预订自习室在线订座系统小程序源码

智能匹配&#xff0c;轻松预订——自习室在线订座系统 &#x1f4da;【开篇&#xff1a;告别排队&#xff0c;迎接智能学习新时代】&#x1f4da; 还在为找不到合适的自习室座位而烦恼吗&#xff1f;是不是每次去图书馆或自习室都要提前好久去排队占位&#xff1f;现在&#…

太速科技-1路万兆光纤SFP+和1路千兆网络 FMC子卡模块

1路万兆光纤SFP和1路千兆网络 FMC子卡模块 一、概述 该板卡是基于kc705和ml605的fmc 10g万兆光纤扩展板设计&#xff0c;提供了1路万兆光纤SFP和1路千兆网络接口。可搭配我公司开发的FPGA载卡使用。载卡可参考&#xff1a;ID204 SFP&#xff08;10 Gigabit Small…

AWS-亚马逊网络服务(基础服务)-AWS 定价计算器-概述与动手部署:

让我们来概述并亲身实践如何使用 AWS 定价计算器来计算 概述&#xff1a; AWS 定价计算器是 Amazon Web Services (AWS) 提供的基于 Web 的工具&#xff0c;可帮助用户估算其特定用例的 AWS 服务成本。欢迎来到雲闪世界。 它允许客户建模他们的基础设施并根据他们打算使用的…

Android 9.0 增加interface audio接口,解决编译报错

最近修改Android接口&#xff0c;报了一个VNDK的错误 我总结了如下几种方式&#xff1a; 1、直接关闭&#xff08;不推荐&#xff09;&#xff1a; 在BoardConfig.mk中加入如下两行&#xff0c;可以在编译的时候不去check VNDK&#xff0c;关掉这个可能会导致XTS某些测项跑不…

Burp Suite Professional 2024.8 for macOS x64 ARM64 - 领先的 Web 渗透测试软件

Burp Suite Professional 2024.8 for macOS x64 & ARM64 - 领先的 Web 渗透测试软件 世界排名第一的 Web 渗透测试工具包 请访问原文链接&#xff1a;https://sysin.org/blog/burp-suite-pro-mac/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页…

Linux内核编程(十五)网络设备驱动

本文目录 一、常见的网络协议二、传输介质三、RJ-45接口 对于网络知识不太熟悉的同学可以查看这篇文章&#xff1a;计算机网络知识点详情总结。 一、常见的网络协议 TCP、UDP协议&#xff1a;详情查看-TCP、UDP系统编程。DNS协议&#xff1a;是互联网中用于将域名&#xff08…

RocketMQ集群搭建,及RocketMQ-Dashboard部署(前RocketMQ-Console)

集群搭建 RocketMQ不支持单主机搭建主从结构集群&#xff0c;当从节点启动时&#xff0c;即使和主节点设置不同的监听端口&#xff0c;他也要去监听主节点端口&#xff0c;也就是说正常启动的从节点会监听四个端口。原因未知&#xff0c;现象后面会列举出来。 1. 准备JAVA环境…

在 Ubuntu 环境下使用 VSCode 和 PlatformIO 下载程序到 Arduino Uno

安装 VSCode 访问 VSCode 官网 下载 .deb 包使用以下命令安装&#xff1a;sudo dpkg -i <下载的文件名>.deb sudo apt-get install -f安装 PlatformIO 扩展 在 VSCode 中&#xff0c;转到扩展市场&#xff08;CtrlShiftX&#xff09;搜索 “PlatformIO IDE”点击 “安装”…

STM32 HAL CAN (TJA1050CAN模块) 通讯(一)理论

1、简介 CAN具备多个设备交互的能力,但是网上大多是两个单片机进行交互,或者单片机通过CAN收发器与上位机进行交互测试,本次通过STM32cubeMX完成CAN通讯配置,并通过多个单片机进行数据交互测试。 2、CAN简介 CAN是一种串行通讯协议,主要有低速、高速CAN两种。 低速CAN…

相亲交友系统商业开发

在快节奏的现代生活中&#xff0c;寻找真爱成为了许多人的渴望。相亲交友系统&#xff0c;作为连接心灵的桥梁&#xff0c;正逐渐成为人们寻找伴侣的首选方式。我们的团队h17711347205致力于开发一款创新的相亲交友系统&#xff0c;旨在通过智能化的匹配算法&#xff0c;为用户…

Android UID 和 userID 以及 appID

我们知道Android 操作系统是基于Linux内核的&#xff0c;所以Android 的UID 是基于 Linux UID的。 Linux UID Linux 本身就是一个多用户操作系统&#xff0c;每一个用户都会有一个UID&#xff0c;不同UID 之间的资源访问是受限的。 其中&#xff0c;Linux的DAC权限模型&#…

【UE5】控件蓝图——树视图(TreeView)的基本使用

目录 前言 效果 步骤 一、显示根节点 二、显示子节点 前言 我们在视口中添加1个方块&#xff0c;2个球体&#xff0c;5个圆柱 它们在大纲视图中的层级关系如下&#xff0c;那么如何将这种层级关系显示在树视图中是本篇文章要解决的问题。 效果 步骤 一、显示根节点 1…

[STM32]从零开始的STM32标准库环境搭建(小白向)

一、我们为什么要搭建STM32标准库开发环境 如果你对STM32有一定的了解&#xff0c;相信你已经认识了STM32的几种开发方式。基于STM32寄存器开发&#xff0c;基于ST官方的标准库开发&#xff0c;基于ST官方的HAL库开发。我们现在来了解一下这些库的优缺点。首先就是基于寄存器开…

macos USB外接键盘ctrl键绑定方法 解决外接USB键盘与mac键盘不一致问题

mac电脑外接USB键盘后我们需要修改一下 ctrl键的绑定后才符合我们的使用习惯,因为标准USB键盘和mac键盘上面的ctrl键是不一样的, mac上面的 command 键 对应我们USB键盘上面的 ctrl 键. 修改方法: 偏好设置 --> 键盘 点击修饰键 后 选择键盘里面选择 USB键盘 ,然后调换…