大文件分片上传、断点续传、秒传
小文件上传
后端:SpringBoot+JDK17
前端:JavaScript+spark+md5.min.js
一、依赖
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.1.2</version><relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>uploadDemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>uploadDemo</name>
<description>uploadDemo</description>
<properties><java.version>17</java.version>
</properties>
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
</dependencies>
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins>
</build>
二、业务代码
@RestController
public class UploadController{//上传路径public static final String UPLOAD_PATH = "D:\\upload";@RequestMapping("/upload")public ResponseEntity<Map<String,String>> upload(@RequestParam MultipartFile file) throws IOException {File dstFile = new File(UPLOAD_PATH,String.format("%s.%s", UUID.randomUUID(), StringUtils.getFilename(file.getOriginalFilename())));file.transferTo(dstFile);return ResponseEntity.ok(Map.of("path",dstFile.getAbsolutePath()));}
}
三、前端显示
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>upload</title>
</head>
<body>
upload<form enctype="multipart/form-data"><input type="file" name="fileInput" id="fileInput"><input type="button" value="上传" onclick="uploadFile()">
</form>上传结果
<span id="uploadResult"></span><script>var uploadResult=document.getElementById("uploadResult")function uploadFile() {var fileInput = document.getElementById('fileInput');var file = fileInput.files[0];if (!file) return; // 没有选择文件var xhr = new XMLHttpRequest();// 处理上传进度xhr.upload.onprogress = function(event) {var percent = 100 * event.loaded / event.total;uploadResult.innerHTML='上传进度:' + percent + '%';};// 当上传完成时调用xhr.onload = function() {if (xhr.status === 200) {uploadResult.innerHTML='上传成功'+ xhr.responseText;}}xhr.onerror = function() {uploadResult.innerHTML='上传失败';}// 发送请求xhr.open('POST', '/upload', true);var formData = new FormData();formData.append('file', file);xhr.send(formData);}
</script></body>
</html>
【注意事项】
在上传过程会报文件大小限制错误,主要有三个参数需要设置:
org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException: the request was rejected because its size (46302921) exceeds the configured maximum (10485760)
需在springboot的application.properties 或者application.yml中添加:
max-file-size
max-request-size
默认大小分别是1M和10M,因此需要重新设定
spring.servlet.multipart.max-file-size=1024MB
spring.servlet.multipart.max-request-size=1024MB
如果使用nginx报 413状态码413 Request Entity Too Large,Nginx默认最大上传1MB文件,需要在nginx.conf配置文件中的 http{ }添加配置项:client_max_body_size 1024m
大文件上传
一、前端分片
计算文件MD5值用了spark-md5这个库
因为文件在传输写入过程中可能会出现错误,导致最终合成的文件可能和原文件不一样,所以要对比一下前端计算的MD5和后端计算的MD5是不是一样,保证上传数据的一致性
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>分片上传</title><script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
</head>
<body>
分片上传<form enctype="multipart/form-data"><input type="file" name="fileInput" id="fileInput"><input type="button" value="计算文件MD5" onclick="calculateFileMD5()"><input type="button" value="上传" onclick="uploadFile()"><input type="button" value="检测文件完整性" onclick="checkFile()">
</form><p>文件MD5:<span id="fileMd5"></span>
</p>
<p>上传结果:<span id="uploadResult"></span>
</p>
<p>检测文件完整性:<span id="checkFileRes"></span>
</p><script>//每片的大小var chunkSize = 1 * 1024 * 1024;var uploadResult = document.getElementById("uploadResult")var fileMd5Span = document.getElementById("fileMd5")var checkFileRes = document.getElementById("checkFileRes")var fileMd5;function calculateFileMD5(){var fileInput = document.getElementById('fileInput');var file = fileInput.files[0];getFileMd5(file).then((md5) => {console.info(md5)fileMd5=md5;fileMd5Span.innerHTML=md5;})}function uploadFile() {var fileInput = document.getElementById('fileInput');var file = fileInput.files[0];if (!file) return;if (!fileMd5) return;//获取到文件let fileArr = this.sliceFile(file);//保存文件名称let fileName = file.name;fileArr.forEach((e, i) => {//创建formdata对象let data = new FormData();data.append("totalNumber", fileArr.length)data.append("chunkSize", chunkSize)data.append("chunkNumber", i)data.append("md5", fileMd5)data.append("file", new File([e],fileName));upload(data);})}/*** 计算文件md5值*/function getFileMd5(file) {return new Promise((resolve, reject) => {let fileReader = new FileReader()fileReader.onload = function (event) {let fileMd5 = SparkMD5.ArrayBuffer.hash(event.target.result)resolve(fileMd5)}fileReader.readAsArrayBuffer(file)})}function upload(data) {var xhr = new XMLHttpRequest();// 当上传完成时调用xhr.onload = function () {if (xhr.status === 200) {uploadResult.append( '上传成功分片:' +data.get("chunkNumber")+'\t' ) ;}}xhr.onerror = function () {uploadResult.innerHTML = '上传失败';}// 发送请求xhr.open('POST', '/uploadBig', true);xhr.send(data);}function checkFile() {var xhr = new XMLHttpRequest();// 当上传完成时调用xhr.onload = function () {if (xhr.status === 200) {checkFileRes.innerHTML = '检测文件完整性成功:' + xhr.responseText;}}xhr.onerror = function () {checkFileRes.innerHTML = '检测文件完整性失败';}// 发送请求xhr.open('POST', '/checkFile', true);let data = new FormData();data.append("md5", fileMd5)xhr.send(data);}function sliceFile(file) {const chunks = [];let start = 0;let end;while (start < file.size) {end = Math.min(start + chunkSize, file.size);chunks.push(file.slice(start, end));start = end;}return chunks;}</script></body>
</html>
二、后端
两个接口/uploadBig用于每一片文件的上传和/checkFile检测文件的MD5
FileChannel fileChannel = randomAccessFile.getChannel();
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, chunkNumber * chunkSize, fileData.length);
mappedByteBuffer.put(fileData);
@RestController
public class UploadController {public static final String UPLOAD_PATH = "D:\\upload\\";/*** @param chunkSize 每个分片大小* @param chunkNumber 当前分片* @param md5 文件总MD5* @param file 当前分片文件数据* @return* @throws IOException*/@RequestMapping("/uploadBig")public ResponseEntity<Map<String, String>> uploadBig(@RequestParam Long chunkSize, @RequestParam Integer totalNumber, @RequestParam Long chunkNumber, @RequestParam String md5, @RequestParam MultipartFile file) throws IOException {//文件存放位置String dstFile = String.format("%s\\%s\\%s.%s", UPLOAD_PATH, md5, md5, StringUtils.getFilenameExtension(file.getOriginalFilename()));//上传分片信息存放位置String confFile = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5);//第一次创建分片记录文件//创建目录File dir = new File(dstFile).getParentFile();if (!dir.exists()) {dir.mkdir();//所有分片状态设置为0byte[] bytes = new byte[totalNumber];Files.write(Path.of(confFile), bytes);}//随机分片写入文件try (RandomAccessFile randomAccessFile = new RandomAccessFile(dstFile, "rw");RandomAccessFile randomAccessConfFile = new RandomAccessFile(confFile, "rw");InputStream inputStream = file.getInputStream()) {//定位到该分片的偏移量(可以将光标移到文件指定位置开始写数据,每一个文件每将上传分片编号chunkNumber都是不一样的,所以各自写自己文件块,多线程写同一个文件不会出现线程安全问题)randomAccessFile.seek(chunkNumber * chunkSize);//写入该分片数据大文件写入时用RandomAccessFile可能比较慢,可以使用MappedByteBuffer内存映射来加速大文件写入,不过使用MappedByteBuffer如果要删除文件可能会存在删除不掉,因为删除了磁盘上的文件,内存的文件还是存在的randomAccessFile.write(inputStream.readAllBytes());//定位到当前分片状态位置randomAccessConfFile.seek(chunkNumber);//设置当前分片上传状态为1randomAccessConfFile.write(1);}return ResponseEntity.ok(Map.of("path", dstFile));}/*** 获取文件分片状态,检测文件MD5合法性** @param md5* @return* @throws Exception*/@RequestMapping("/checkFile")public ResponseEntity<Map<String, String>> uploadBig(@RequestParam String md5) throws Exception {String uploadPath = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5);Path path = Path.of(uploadPath);//MD5目录不存在文件从未上传过if (!Files.exists(path.getParent())) {return ResponseEntity.ok(Map.of("msg", "文件未上传"));}//判断文件是否上传成功StringBuilder stringBuilder = new StringBuilder();byte[] bytes = Files.readAllBytes(path);for (byte b : bytes) {stringBuilder.append(String.valueOf(b));}//所有分片上传完成计算文件MD5if (!stringBuilder.toString().contains("0")) {File file = new File(String.format("%s\\%s\\", UPLOAD_PATH, md5));File[] files = file.listFiles();String filePath = "";for (File f : files) {//计算文件MD5是否相等if (!f.getName().contains("conf")) {filePath = f.getAbsolutePath();try (InputStream inputStream = new FileInputStream(f)) {String md5pwd = DigestUtils.md5DigestAsHex(inputStream);if (!md5pwd.equalsIgnoreCase(md5)) {return ResponseEntity.ok(Map.of("msg", "文件上传失败"));}}}}return ResponseEntity.ok(Map.of("path", filePath));} else {//文件未上传完成,反回每个分片状态,前端将未上传的分片继续上传return ResponseEntity.ok(Map.of("chucks", stringBuilder.toString()));}}}
断点续传
用/checkFile接口,文件里如果有未完成上传的分片,接口返回chunks字段对就的位置值为0,前端将未上传的分片继续上传,完成后再调用/checkFile就完成了断点续传
秒传
只要修改前端代码流程就好了,比如张三上传了一个文件,然后李四又上传了同样内容的文件,同一文件的MD5值可以认为是一样的(虽然会存在不同文件的MD5一样,不过概率很小,可以认为MD5一样文件就是一样)李四调用/checkFile接口后,后端直接返回了李四上传的文件路径,李四就完成了秒传。大部分云盘秒传的思路应该也是这样,只不过计算文件HASH算法更为复杂,返回给用户文件路径也更为安全,要防止被别人算出文件路径了
相关文章:

大文件分片上传、断点续传、秒传
小文件上传 后端:SpringBootJDK17 前端:JavaScriptsparkmd5.min.js 一、依赖 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.1.2</ve…...

DAY53 1143.最长公共子序列 + 1035.不相交的线 + 53. 最大子序和
1143.最长公共子序列 题目要求:给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。 一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删…...

短剧App开发:个性化的内容推荐
随着移动互联网的普及和用户需求的多样化,短剧App作为一种新兴的内容消费模式,受到了越来越多用户的青睐。在短剧App开发中,个性化的内容推荐是一个重要的功能,它能够根据用户的兴趣偏好和行为数据,为他们提供更精准、…...

互斥量保护资源
一、概念 在多数情况下,互斥型信号量和二值型信号量非常相似,但是从功能上二值型信号量用于同步, 而互斥型信号量用于资源保护。 互斥型信号量和二值型信号量还有一个最大的区别,互斥型信号量可以有效解决优先级反转现 象。 …...

天机学堂-1、项目搭建,微服务架构设计
1.学习背景 各位同学大家好,经过前面的学习我们已经掌握了《微服务架构》的核心技术栈。相信大家也体会到了微服务架构相对于项目一的单体架构要复杂很多,你的脑袋里也会有很多的问号: 微服务架构该如何拆分? 到了公司中我需要自…...

windows 电脑删除不了.TTF的文件
出现这个问题,首先检查,你的.ttf文件是不是在哪个软件中打开了。 如果是,先关掉,然后在删一遍试试。 如果这个还是不行试着打开控制面板>外观和个性化> 字体 > 字体设置>还原默认字体设置勾选,然后重启一下…...

C#多线程的操作
文章目录 1 使用线程意义2 C#线程开启的四种方式2.1 异步委托开启线程2.2 通过Thread类开启线程2.3 通过线程池开启线程2.4 通过任务Task开启线程 3 前台线程和后台线程简述3.1 前台线程3.2 后台线程 4 简述Thread和Task开启线程的区别4.1 Thread效果展示4.2 Task效果展示4.3 区…...

MyBatis Plus—CRUD 接口
Service CRUD 接口 说明: 通用 Service CRUD 封装IService (opens new window)接口,进一步封装 CRUD 采用 get 查询单行 remove 删除 list 查询集合 page 分页 前缀命名方式区分 Mapper 层避免混淆,泛型 T 为任意实体对象建议如果存在自定义通用 Servi…...

火焰图:链路追踪分析的可视化利器
什么是火焰图? 火焰图用于可视化分布式链路追踪,通过使用持续时间和不同颜色的水平条形来表示请求执行路径中的每个服务调用。分布式跟踪的火焰图包括错误、延迟数据等详情,帮助开发人员识别和解决应用程序中的瓶颈问题。 链路追踪与 Span …...

中睿天下Coremail | 2023年Q3企业邮箱安全态势观察报告
10月25日,北京中睿天下信息技术有限公司联合Coremail邮件安全发布《2023年第三季度企业邮箱安全性研究报告》。2023年第三季度企业邮箱安全呈现出何种态势?作为邮箱管理员,我们又该如何做好防护? 以下为精华版阅读,如需…...

HBuilderX vue项目打包上传到服务器
完成后有个’dist’目录,把真个目录通过FTP 上传到服务器,Mac电脑使用cyberduck 上传 服务器使用‘宝塔’进行一件部署,基本上就是傻瓜式的点击下一步...

2656. K 个元素的最大和 --力扣 --JAVA
题目 给你一个下标从 0 开始的整数数组 nums 和一个整数 k 。你需要执行以下操作 恰好 k 次,最大化你的得分: 从 nums 中选择一个元素 m 。 将选中的元素 m 从数组中删除。 将新元素 m 1 添加到数组中。 你的得分增加 m 。 请你返回执行以上操作恰好 k …...

从HTTP到Tomcat:揭秘Web应用的底层协议与高性能容器
WEB服务器 1. HTTP协议1.1 HTTP-概述1.1.1 介绍1.2.2 特点 2.2 HTTP-请求协议2.3 HTTP-响应协议2.3.1 格式介绍2.3.2 响应状态码 2.4 HTTP-协议解析 2. WEB服务器-Tomcat2.1 简介2.1.1 服务器概述2.1.2 Web服务器2.1.3 Tomcat 2.2 基本使用2.2.1 下载2.2.2 安装与卸载2.2.3 启动…...

百度搜索智能化算力调控分配方法
作者 | 泰来 导读 随着近年深度学习技术的发展,搜索算法复杂度不断上升,算力供给需求出现了爆发式的增长。伴随着AI技术逐步走到深水区,算法红利在逐步消失,边际效应日益显著,算力效能的提升尤为重要,同时随…...

如何搭建接口自动化测试框架?
经过了一年多的接口测试工作,旧的框架也做了一些新的调整,删除了很多冗余的功能,只保留了最基本的接口结构验证、接口回归测试、线上定时巡检功能。 一、框架的演进 界面 UI 做了优化,整个框架的画风突然不一样了(人靠…...

ubuntu 20.04+ORB_SLAM3 安装配库教程
目录 安装ros(如果只是运行ORB-SLAM3,可以跳过安装)0. ros 安装教程1. 安装opencv2. 安装Pangolin3. 安装Eigen34.安装Python & libssl-dev5.安装boost库6.安装ceres库(不必须)7.安装Sophus库(不必须)8. 安装g20库…...

Poly风格模型的创建与使用_unity基础开发教程
Poly风格模型的创建与使用 安装Poly相关组件Poly模型的创建Poly模型编辑 安装Poly相关组件 打开资源包管理器Package Manager 在弹出的窗口左上角Packages选择Unity Registry 搜索框搜索 Poly 搜索结果点击Polybrush 点击右下角 Install 同时也别忘了导入一下模型示例&#…...

终于有人把VMware虚拟机三种网络模式讲清楚了!
前段时间VMware更新了,你用上最新版了吗? 有几个网工在操作中遇到过各种各样的问题。 比如说由于公司服务器重启导致出现下面的问题:在Xshell里连接虚拟机映射时连接失败;能够连接上虚拟机的映射地址,但git pull时报…...

Flutter实践二:repository模式
1.repository 几乎所有的APP,从简单的到最复杂的,在它们的架构里几乎都包括状态管理和数据源这两部分。状态管理常见的有Bloc、Cubit、Provider、ViewModel等,数据源则是一些直接和数据库或者网络客户端进行交互,取得相应的数据&…...

交换机Vlan和端口配置(H3C)
交换机Vlan配置(H3C) 配置VLAN配置VLAN接口的IP地址开启ARP网关保护功能,配置被保护的网关IP地址 配置VLAN Vlan物理端口3GigabitEthernet 1/0/1 ~ GigabitEthernet 1/0/14 ;GigabitEthernet 2/0/1 ~ GigabitEthernet 2/0/1450Gi…...

vue自定义指令控制权限
1、在main.js中注册全局指令 import Vue from vue;// 按钮权限控制指令 Vue.directive(permission, {inserted: (el, binding)>{const { value } binding;// 判断当前用户是否拥有该按钮权限if (!checkPermission(value)) {el.parentNode.removeChild(el);}} }); // 判断…...

jquery获取url参数
使用jquery获取url以及使用jquery获取url参数是我们经常要用到的操作,下面通过文字说明加代码分析的形式给大家解析,具体详情请看下文。 1、jquery获取url很简单,代码如下: window.location.href; 其实只是用到了javascript的基…...

SpringBoot和Spring源码下载
1.下载:在一个空的干净地创建一个文件夹叫springsourcecode,其实叫什么都行的。 git clone https://github.com/spring-projects/spring-framework.git 2.JDK要和gradle匹配 我们要21的,今天为止2023年11月13日,idea是2023.2。 …...

【算法练习Day49】每日温度下一个更大元素 I
📝个人主页:Sherry的成长之路 🏠学习社区:Sherry的成长之路(个人社区) 📖专栏链接:练题 🎯长路漫漫浩浩,万事皆有期待 文章目录 每日温度下一个更大元素 I总…...

Maven打包时跳过测试代码
Maven 打包时会把一些用于测试的类或文件也一起打包,无疑增加了打包失败的风险,也加剧了文件占用磁盘的大小。 所以本次写一下如何跳过测试类。 命令行方式跳过测试 我们可以用两种命令来跳过测试 mvn clean package -DskipTestsmvn clean package -D…...

2023-2024 年适用于 Windows 电脑的顶级视频录制软件
想捕捉您正在在线观看的视频吗?使用网络摄像头录制视频会议以供日后参考。正在寻找可以完成这些任务的视频捕捉软件?这篇文章说明了一切。以下是一些适用于 Windows PC 的最佳视频录制工具。 什么是视频录制软件? 顾名思义,视频捕…...

2023-11-14 mysql-主从复制-重置主从连接-记录
摘要: mysql的主从复制, 当从库执行binlog出错后, 会中止主从复制. 此时需要重置主从连接, 以重建主从关系. 主库操作: 一. 清理同步的数据库 drop database test;二. 重置主库状态 reset master;reset slave all;三. 检测主库状态 show master status;mysql> show master…...

go语言学习之旅之安装sdk环境,hello world!
学无止境 为什么学习Go语言 高效编程: Go语言被设计为一门高效的编程语言。其编译速度快,执行速度也相对较快,适合用于构建高性能的应用程序。 并发支持: Go语言天生支持并发编程,通过goroutine和channel提供了简单而…...

《Linux从练气到飞升》No.28 Linux中的线程同步
🕺作者: 主页 我的专栏C语言从0到1探秘C数据结构从0到1探秘Linux菜鸟刷题集 😘欢迎关注:👍点赞🙌收藏✍️留言 🏇码字不易,你的👍点赞🙌收藏❤️关注对我真的…...

爬取动态网页内容的库
爬取动态网页内容时,传统的 Python 爬虫库(如 Requests、BeautifulSoup)可能无法直接获取 JavaScript 动态生成的内容。为了处理这种情况,你可以使用一些特别设计的库,它们能够模拟浏览器行为,执行 JavaScr…...