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.fileNameconsole.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; //10mbconst 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) => {//分片读取并计算md5const 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-uploadaction="#":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-progressstripedstriped-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-progressstripedstriped-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;@Overridepublic 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);}@Overridepublic 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 {@Overridepublic 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.mzph.cn/news/590565.shtml

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

相关文章

低延时视频技术的应用场景和挑战

编者按 无线网络对人们的生活产生了巨大的影响&#xff0c;而5G技术的引入将彻底改变我们与世界互联互通的方式。在5G时代&#xff0c;实现万物互联离不开低延时技术的应用。 LiveVideoStackCon 2023 深圳站邀请到秒点科技的CEO扶凯&#xff0c;为大家分享低延时技术在物联网、…

【CF比赛记录】—— Good Bye 2023(A、B、C)

&#x1f30f;博客主页&#xff1a;PH_modest的博客主页 &#x1f6a9;当前专栏&#xff1a;CF比赛记录 &#x1f48c;其他专栏&#xff1a; &#x1f534;每日一题 &#x1f7e1; cf闯关练习 &#x1f7e2; C语言跬步积累 &#x1f308;座右铭&#xff1a;广积粮&#xff0c;缓…

Big-endian与Little-endian详尽说明

大端与小端存储详尽说明 大端与小端存储详尽说明 大端与小端存储详尽说明一. 什么是字节序二. 什么是大端存储模式三. 什么是小端存储模式四. 大小端各自的特点五. 为什么会有大小端模式之分六. 为什么要注意大小端问题六. 大小端判定程序七. 大端小端的转换1&#xff09;16位大…

详解Vue3中的鼠标事件mousedown、mouseup和contextmenu

本文主要介绍Vue3中的常见鼠标事件mousedown、mouseup和contextmenu。 目录 一、mousedown——鼠标按下事件二、mouseup——鼠标弹起事件三、contextmenu——页面菜单 下面是Vue 3中常用的鼠标事件mousedown、mouseup和contextmenu的详解。 一、mousedown——鼠标按下事件 mo…

当你的电脑在安装Windows更新后出现问题时怎么办,这里提供办法

Windows更新通常会为你的电脑带来错误修复、安全补丁和新功能,但它们也可能会带来性能下降甚至引发恐慌的数据丢失等问题,从而适得其反。如果你在安装更新后发现了一些奇怪之处,你可以将其回滚,尝试重新启动。 Windows更新主要有两种:质量更新和功能更新。高质量的更新包…

vmware安装openEuler 22.03 LTS操作系统

vmware安装openEuler 22.03 LTS操作系统 1、下载openEuler操作系统镜像文件2、安装openEuler操作系统3、配置openEuler操作系统3.1、配置静态IP地址 和 dns3.2、查看磁盘分区3.3、查看系统版本 1、下载openEuler操作系统镜像文件 官网下载链接 链接: https://www.openeuler.or…

【2023年终总结:轻舟已过万重山】

&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308; 欢迎关注公众号&#xff08;通过文章导读关注&#xff09;&#xff0c;发送【资料】可领取 深入理解 Redis 系列文章结合电商场景讲解 Redis 使用场景、中间件系列…

数据结构期末复习(fengkao课堂)

学习数据结构时&#xff0c;以下建议可能对您有所帮助&#xff1a; 理解基本概念&#xff1a;首先&#xff0c;确保您理解数据结构的基本概念&#xff0c;例如数组、链表、栈、队列、树、图等。了解它们的定义、特点和基本操作。 学习时间复杂度和空间复杂度&#xff1a;了解如…

Docker support for NVIDIA GPU Accelerated Computing on WSL 2

Docker support for NVIDIA GPU Accelerated Computing on WSL 2 0. 背景1. 安装 Docker Desktop2. 配置 Docker Desktop3. WLS Ubuntu 配置4. 安装 Docker-ce5. 安装 NVIDIA Container Toolkit6. 配置 Docker7. 运行一个 Sample Workload 0. 背景 今天尝试一下 NVIDIA GPU 在…

IoT 物联网常用协议

物联网协议是指在物联网环境中用于设备间通信和数据传输的协议。根据不同的作用&#xff0c;物联网协议可分为传输协议、通信协议和行业协议。 传输协议&#xff1a;一般负责子网内设备间的组网及通信。例如 Wi-Fi、Ethernet、NFC、 Zigbee、Bluetooth、GPRS、3G/4G/5G等。这些…

公司电脑文件防泄密|防止内部终端核心文件数据 \ 资料外泄

PC端访问地址&#xff1a; https://isite.baidu.com/site/wjz012xr/2eae091d-1b97-4276-90bc-6757c5dfedee 为了防止公司电脑文件泄密&#xff0c;可以采取以下措施&#xff1a; 文件加密&#xff1a;对重要文件进行加密是一种有效的防泄密方法。通过使用加密算法&#xff0c;…

Linux学习之系统编程1(关于读写系统函数)

写在前面&#xff1a; 我的Linux的学习之路非常坎坷。第一次学习Linux是在大一下的开学没多久&#xff0c;结果因为不会安装VMware就无疾而终了&#xff0c;可以说是没开始就失败了。第二次学习Linux是在大一下快放暑假&#xff08;那个时候刚刚过完考试周&#xff09;&#xf…

Pandas教程(四)—— 分层索引及数据联合

1.分层索引 分层索引就是在一个轴上拥有多个&#xff08;两个及以上&#xff09;索引级别&#xff0c;能以低维度形式处理高维度数据。 行索引有两层 1.1 分层索引的创建 1.1.1 方式一&#xff1a;直接设置 1&#xff09;在创建series、dataframe或读取文件时时&#xff0c;行…

[蓝桥杯基础题型] 图论题目

遍历 添加路障 首先答案只能是0 1 2 &#xff0c;原因&#xff1a;把出发点堵住只需要两个路障 路障为0&#xff1a;不能找到一条从出发点到终点的路 路障为1&#xff1a;能找到一条从出发点到终点的路&#xff0c;但是只有一条 路障为2&#xff1a;能找到一条从出发点到终…

Linux搭建我的世界Mohist1.20.2版服务器教程,MOD和插件服开服教程,MC开服教程

雨云游戏云VPS服务器用Linux搭建MCSM面板和Minecraft Mohist 1.20.2服务器教程&#xff0c;我的世界MOD和插件服开服教程。 本教程演示安装的MC服是Mohist 1.20.2版&#xff0c;其他版本也可以参考本教程&#xff0c;差别不大。 本教程使用Docker来运行mc服&#xff0c;可以方…

【网络面试(5)】收发数据及断开服务器(四次挥手)

前面了解到服务器和客户端在创建套接字&#xff0c;建立连接后&#xff0c;就可以进入到下一步&#xff0c;双发可以互相发送和接收数据&#xff0c;本篇博客就来学习一下这个过程。  我们印象里&#xff0c;发送数据应该是我们在浏览器输入网址&#xff0c;敲击回车的一瞬间&…

NGUI基础-三大基础组件之Panel组件

目录 Panel组件 Panel的作用&#xff1a; 注意&#xff1a; 相关关键参数讲解&#xff1a; Alpha&#xff08;透明度值&#xff09;&#xff1a; Depth&#xff08;深度&#xff09;&#xff1a; Clippinng&#xff08;裁剪&#xff09;&#xff1a; ​编辑 None Tex…

Redis(Linux版本7.2.3)

1、停止Redis服务器 [roottssvr1-c1 sysconfig]# ps -ef | grep redis root 322 1 0 10月30 ? 02:58:53 ./bin/redis-server 0.0.0.0:6379 root 32664 12498 0 14:45 pts/0 00:00:00 grep --colorauto redis [roottssvr1-c1 sysconfig]# [roottssvr…

VMware虚拟机和Centos7镜像安装

文章目录 安装VMware虚拟机1、下载2、激活 安装Centos7镜像启动虚拟机 安装VMware虚拟机 1、下载 建议还是安装16版本 VMware16下载 https://www.123pan.com/s/HQeA-aX1Sh VMware15 链接&#xff1a;https://pan.baidu.com/s/11UD1hb6IydbxNNPxmh-MqA?pwd0630 提取码&am…

PiflowX组件-JDBCRead

JDBCRead组件 组件说明 使用JDBC驱动向任意类型的关系型数据库读取数据。 计算引擎 flink 有界性 Scan Source: Bounded Lookup Source: Sync Mode 组件分组 Jdbc 端口 Inport&#xff1a;默认端口 outport&#xff1a;默认端口 组件属性 名称展示名称默认值允许…