SpringBoot文件上传之秒传、断点续传、分片上传

一 文件上传的常见场景

在日常开发中,文件上传的场景多种多样。比如,在线教育平台上的视频资源上传,社交平台上的图片分享,以及企业内部的知识文档管理等。这些场景对文件上传的要求也各不相同,有的追求速度,有的注重稳定性,还有的需要考虑文件大小和安全性。因此,针对不同需求,我们有了秒传、断点续传和分片上传等解决方案。

二 秒传、断点上传与分片上传

秒传
秒传,顾名思义,就是几乎瞬间完成文件上传的过程。其实现原理是通过计算文件的哈希值(如 MD5 或 SHA-1),然后将这个唯一的标识符发送给服务器。如果服务器上已经存在相同的文件,则直接返回成功信息,避免了重复上传。这种方式不仅节省了带宽,也大大提高了用户体验。
断点续传
断点续传是指在网络不稳定或者用户主动中断上传后,能够从上次中断的地方继续上传,而不需要重新开始整个过程。这对于大文件上传尤为重要,因为它可以有效防止因网络问题导致的上传失败,同时也能节约用户的流量和时间。
分片上传
分片上传则是将一个大文件分割成多个小块分别上传,最后再由服务器合并成完整的文件。这种做法的好处是可以并行处理多个小文件,提高上传效率;同时,如果某一部分上传失败,只需要重传这一部分,不影响其他部分。

三 秒传实现

后端实现:在 SpringBoot 中,我们可以使用 MessageDigest 类来计算文件的 MD5 值,然后检查数据库中是否存在该文件。

@RestController
@RequestMapping("/file")
public class FileController {@AutowiredFileService fileService;@PostMapping("/upload1")public ResponseEntity<String> secondUpload(@RequestParam(value = "file",required = false) MultipartFile file,@RequestParam(required = false,value = "md5") String md5) {try {// 检查数据库中是否已存在该文件if (fileService.existsByMd5(md5)) {return ResponseEntity.ok("文件已存在");}// 保存文件到服务器file.transferTo(new File("/path/to/save/" + file.getOriginalFilename()));// 保存文件信息到数据库fileService.save(new FileInfo(file.getOriginalFilename(), DigestUtils.md5DigestAsHex(file.getInputStream())));return ResponseEntity.ok("上传成功");} catch (Exception e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("上传失败");}}
}

前端调用:通过 JavaScript 的 FileReader API 读取文件内容,通过 spark-md5 计算 MD5 值,然后发送给后端进行校验。

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>秒传</title><script src="spark-md5.js"></script>
</head>
<body>
<input type="file" id="fileInput" />
<button onclick="startUpload()">开始上传</button>
<hr>
<script>async function startUpload() {const fileInput = document.getElementById('fileInput');const file = fileInput.files[0];if (!file) {alert("请选择文件");return;}const md5 = await calculateMd5(file);const formData = new FormData();formData.append('md5', md5);const response = await fetch('/file/upload1', {method: 'POST',body: formData});const result = await response.text();if (response.ok) {if (result != "文件已存在") {// 开始上传文件}} else {console.error("上传失败: " + result);}}function calculateMd5(file) {return new Promise((resolve, reject) => {const reader = new FileReader();reader.onloadend = () => {const spark = new SparkMD5.ArrayBuffer();spark.append(reader.result);resolve(spark.end());};reader.onerror = () => reject(reader.error);reader.readAsArrayBuffer(file);});}
</script>
</body>
</html>

前端分为两个步骤:
1.计算文件的 MD5 值,计算之后发送给服务端确定文件是否存在。
2.如果文件已经存在,则不需要继续上传文件;如果文件不存在,则开始上传文件,上传文件和 MD5 校验请求类似。

四 分片上传实例

分片上传关键是在前端对文件切片,比如一个 10MB 的文件切为 10 份,每份 1MB。每次上传的时候,需要多一个参数记录当前上传的文件切片的起始位置。
比如一个 10MB 的文件,切为 10 份,每份 1MB,那么:

  • 第 0 片,从 0 开始,一共是 1024*1024 个字节。
  • 第 1 片,从 10241024 开始,一共是 10241024 个字节。
  • 第 2 片…

后端实现

private static final String UPLOAD_DIR = System.getProperty("user.home") + "/uploads/";
/*** 上传文件到指定位置** @param file 上传的文件* @param start 文件开始上传的位置* @return ResponseEntity<String> 上传结果*/
@PostMapping("/upload2")
public ResponseEntity<String> resumeUpload(@RequestParam("file") MultipartFile file, @RequestParam("start") long start,@RequestParam("fileName") String fileName) {try {File directory = new File(UPLOAD_DIR);if (!directory.exists()) {directory.mkdirs();}File targetFile = new File(UPLOAD_DIR + fileName);RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "rw");FileChannel channel = randomAccessFile.getChannel();channel.position(start);channel.transferFrom(file.getResource().readableChannel(), start, file.getSize());channel.close();randomAccessFile.close();return ResponseEntity.ok("上传成功");} catch (Exception e) {System.out.println("上传失败: "+e.getMessage());return ResponseEntity.status(500).body("上传失败");}
}

后端每次处理的时候,需要先设置文件的起始位置。

前端调用
前端需要将文件切分成多个小块,然后依次上传。

<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>分片示例</title>
</head>
<body><input type="file" id="fileInput" /><button onclick="startUpload()">开始上传</button><script>async function startUpload() {const fileInput = document.getElementById('fileInput');const file = fileInput.files[0];if (!file) {alert("请选择文件");return;}const filename = file.name;let start = 0;uploadFile(file, start);}async function uploadFile(file, start) {const chunkSize = 1024 * 1024; // 每个分片1MBconst total = Math.ceil(file.size / chunkSize);for (let i = 0; i < total; i++) {const chunkStart = start + i * chunkSize;const chunkEnd = Math.min(chunkStart + chunkSize, file.size);const chunk = file.slice(chunkStart, chunkEnd);const formData = new FormData();formData.append('file', chunk);formData.append('start', chunkStart);formData.append('fileName', file.name);const response = await fetch('/file/upload2', {method: 'POST',body: formData});const result = await response.text();if (response.ok) {console.log(`分片 ${i + 1}/${total} 上传成功`);} else {console.error(`分片 ${i + 1}/${total} 上传失败: ${result}`);break;}}}</script>
</body>
</html>

五 断点续传实例

断点续传的技术原理类似于分片上传。
当文件已经上传了一部分之后,断了需要重新开始上传。
那么我们的思路是这样的:

  1. 前端先发送一个请求,检查要上传的文件在服务端是否已经存在,如果存在,目前大小是多少。
  2. 前端根据已经存在的大小,继续上传文件即可。

后端案例

@GetMapping("/check")
public ResponseEntity<Long> checkFile(@RequestParam("filename") String filename) {File file = new File(UPLOAD_DIR + filename);if (file.exists()) {return ResponseEntity.ok(file.length());} else {return ResponseEntity.ok(0L);}
}

如果文件存在,则返回已经存在的文件大小。
如果文件不存在,则返回 0,表示前端从头开始上传该文件。
前端调用

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>断点续传示例</title>
</head>
<body>
<input type="file" id="fileInput"/>
<button onclick="startUpload()">开始上传</button><script>async function startUpload() {const fileInput = document.getElementById('fileInput');const file = fileInput.files[0];if (!file) {alert("请选择文件");return;}const filename = file.name;let start = await checkFile(filename);uploadFile(file, start);}async function checkFile(filename) {const response = await fetch(`/file/check?filename=${filename}`);const start = await response.json();return start;}async function uploadFile(file, start) {const chunkSize = 1024 * 1024; // 每个分片1MBconst total = Math.ceil((file.size - start) / chunkSize);for (let i = 0; i < total; i++) {const chunkStart = start + i * chunkSize;const chunkEnd = Math.min(chunkStart + chunkSize, file.size);const chunk = file.slice(chunkStart, chunkEnd);const formData = new FormData();formData.append('file', chunk);formData.append('start', chunkStart);formData.append('fileName', file.name);const response = await fetch('/file/upload2', {method: 'POST',body: formData});const result = await response.text();if (response.ok) {console.log(`分片 ${i + 1}/${total} 上传成功`);} else {console.error(`分片 ${i + 1}/${total} 上传失败: ${result}`);break;}}}
</script>
</body>
</html>

这个案例实际上是一个断点续传+分片上传的案例。

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

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

相关文章

力扣 最长回文字串-5

最长回文字串-5 //双指针&#xff0c;暴力解法 class Solution { public:bool is(string s, int l, int r) // 判断是否为回文{while (l < r) {if (s[l] ! s[r]) {return false;}l;r--;}return true;}string longestPalindrome(string s) {int Max 0;//用来判断找出最长字…

【算法】快速求出 n 最低位的 1

Leetcode 2438. 二的幂数组中查询范围内的乘积 先展示算法具体实现 while (n) {int lowbit n & (-n);powers.push_back(lowbit);n ^ lowbit; }这段代码的核心是通过 n & (-n) 计算出 n 的 最低位的 1&#xff08;即最右边的 1&#xff09; -n 是 n 的二进制补码表…

数据抽取平台pydatax使用案例---11个库项目使用

数据抽取平台pydatax&#xff0c;前期项目做过介绍&#xff1a; 1&#xff0c;数据抽取平台pydatax介绍--实现和项目使用 项目2&#xff1a; 客户有9个分公司&#xff0c;用的ERP有9套&#xff0c;有9个库&#xff0c;不同版本&#xff0c;抽取的同一个表字段长度有不一样&…

.NET9 - Swagger平替Scalar详解(四)

书接上回&#xff0c;上一章介绍了Swagger代替品Scalar&#xff0c;在使用中遇到不少问题&#xff0c;今天单独分享一下之前Swagger中常用的功能如何在Scalar中使用。 下面我们将围绕文档版本说明、接口分类、接口描述、参数描述、枚举类型、文件上传、JWT认证等方面详细讲解。…

shiny动态生成颜色选择器并将其用于绘图

在 Shiny 中使用 uiOutput 和 renderUI 动态生成 UI 控件是一种灵活的方法。结合 uiOutput(ns("colorSelectors")) 的用法&#xff0c;可以实现动态生成颜色选择器&#xff0c;并响应用户选择进行绘图或更新显示。 代码 library(shiny) library(colourpicker)# UI …

【单点知识】基于PyTorch进行模型部署

文章目录 0. 前言1. 模型导出1.1 TorchScript1.1.1 使用 torch.jit.trace1.1.2 使用 torch.jit.script 1.2 ONNX1.2.1 导出为 ONNX 格式 1.3 导出后的模型加载1.3.1 加载 TorchScript 模型1.3.2 加载 ONNX 模型 2. 模型优化2.1 模型量化2.2 模型剪枝 3. 服务化部署3.1 Flask 部…

‌Kotlin中的?.和!!主要区别

目录 1、?.和!!介绍 2、使用场景和最佳实践 3、代码示例和解释 1、?.和!!介绍 ‌Kotlin中的?.和!!主要区别在于它们对空指针的处理方式。‌ ‌?.&#xff08;安全调用操作符&#xff09;‌&#xff1a;当变量可能为null时&#xff0c;使用?.可以安全地调用其方法或属性…

java基础知识(常用类)

目录 一、包装类(Wrapper) (1)包装类与基本数据的转换 (2)包装类与String类型的转换 (3)Integer类和Character类常用的方法 二、String类 (1)String类介绍 1)String 对象用于保存字符串,也就是一组字符序列 2)字符串常量对象是用双引号括起的字符序列。例如:&quo…

《Hello YOLOv8从入门到精通》5,颈部网络(Neck)结构、核心源码和参数调优

YOLOv8的颈部网络&#xff08;Neck&#xff09;是目标检测模型中的关键组成部分&#xff0c;它位于骨干网络&#xff08;Backbone&#xff09;和头部网络&#xff08;Head&#xff09;之间&#xff0c;主要负责进行特征融合和增强。 在YOLOv8中&#xff0c;颈部网络采用了先进…

C#里怎么样实现单向链表?

C#里怎么样实现单向链表? 数据结构,是程序基本表示方法。 不同的数据结构,就需要采用不同的算法。 在软件开发中,使用到的链表还是比较多的。不过,目前C#语言,基本上都类库, 所以需要自己创建链表的机会,基本不存在了。 但是作为理解原理,还是学习一下吧。 下面的例…

Servlet细节

目录 1 Servlet 是否符合线程安全&#xff1f; 2 Servlet对象的创建时间&#xff1f; 3 Servlet 绑定url 的写法 3.1 一个Servlet 可以绑定多个url 3.2 在web.xml 配置文件中 url-pattern写法 1 Servlet 是否符合线程安全&#xff1f; 答案&#xff1a;不安全 判断一个线程…

对比三种UI交互界面的方案

在嵌入式系统的显示应用领域&#xff0c;如何高效、稳定地驱动TFT LCD显示屏至关重要。当下主流方案有三种&#xff1a; 单片机控制芯片屏 &#xff0c;常见的是瑞佑系列芯片单片机串口屏&#xff0c;常见迪文和大彩单片机内建LCD驱动&#xff0c;常见比如ST32F429等 这三种各…

w~视觉~3D~合集3

我自己的原文哦~ https://blog.51cto.com/whaosoft/12538137 #SIF3D 通过两种创新的注意力机制——三元意图感知注意力&#xff08;TIA&#xff09;和场景语义一致性感知注意力&#xff08;SCA&#xff09;——来识别场景中的显著点云&#xff0c;并辅助运动轨迹和姿态的预测…

fastjson不出网打法—BCEL链

前言 众所周知fastjson公开的就三条链&#xff0c;一个是TemplatesImpl链&#xff0c;但是要求太苛刻了&#xff0c;JNDI的话需要服务器出网才行&#xff0c;BCEL链就是专门应对不出网的情况。 实验环境 fastjson1.2.4 jdk8u91 dbcp 9.0.20 什么是BCEL BCEL的全名应该是…

GitLab使用操作v1.0

1.前置条件 Gitlab 项目地址&#xff1a;http://******/req Gitlab账户信息&#xff1a;例如 001/******自己的分支名称&#xff1a;例如 001-master&#xff08;注&#xff1a;master只有项目创建者有权限更新&#xff0c;我们只能更新自己分支&#xff0c;然后创建合并请求&…

MATLAB GUI设计(基础)

一、目的和要求 1、熟悉和掌握MATLAB GUI的基本控件的使用及属性设置。 2、熟悉和掌握通过GUIDE创建MATLAB GUI的方法。 3、熟悉和掌握MATLAB GUI的菜单、对话框及文件管理框的设计。 4、熟悉和掌握MATLAB GUI的M文件编写。 5、了解通过程序创建MATLAB GUI的方法。 二、内…

RabbitMQ简单应用

概念 RabbitMQ 是一种流行的开源消息代理&#xff08;Message Broker&#xff09;软件&#xff0c;它实现了高级消息队列协议&#xff08;AMQP - Advanced Message Queuing Protocol&#xff09;。RabbitMQ 通过高效的消息传递机制&#xff0c;主要应用于分布式系统中解耦应用…

第 36 章 - Go语言 服务网格

服务网格&#xff08;Service Mesh&#xff09;是一种管理服务间通信的方法&#xff0c;它允许开发人员对服务之间的交互进行抽象化处理。通过在基础设施层面上实现这一点&#xff0c;服务网格可以帮助解决微服务架构中常见的复杂性和挑战&#xff0c;比如服务发现、负载均衡、…

【es6】原生js在页面上画矩形及删除的实现方法

画一个矩形&#xff0c;可以选中高亮&#xff0c;删除自己效果的实现&#xff0c;后期会丰富下细节&#xff0c;拖动及拖动调整矩形大小 实现效果 代码实现 class Draw {constructor() {this.x 0this.y 0this.disX 0this.disY 0this.startX 0this.startY 0this.mouseDo…

【前端】JavaScript中的隐式声明及其不良影响分析

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: 前端 文章目录 &#x1f4af;前言&#x1f4af;什么是隐式声明&#xff1f;&#x1f4af;隐式声明的常见情景1. 赋值给未声明的变量2. 非严格模式下的隐式声明3. 函数中的变量漏掉声明4. for 循环中的隐式声明5. 使用…