springboot 文件高效上传

devtools/2024/11/17 11:51:08/

文件上传功能可以说对于后端服务是必须的,不同场景对文件上传的要求也各不相同,有的追求速度,有的注重稳定性,还有的需要考虑文件大小和安全性。所以便有了秒传、断点续传和分片上传等解决方案。

1、总述

秒传

秒传,顾名思义,就是几乎瞬间完成文件上传的过程。其实现原理是通过计算文件的哈希值(如 MD5 或 SHA-1),然后将这个唯一的标识符发送给服务器。如果服务器上已经存在相同的文件,则直接返回成功信息,避免了重复上传。这种方式不仅节省了带宽,也大大提高了用户体验。

断点续传

断点续传是指在网络不稳定或者用户主动中断上传后,能够从上次中断的地方继续上传,而不需要重新开始整个过程。这对于大文件上传尤为重要,因为它可以有效防止因网络问题导致的上传失败,同时也能节约用户的流量和时间。

分片上传

分片上传则是将一个大文件分割成多个小块分别上传,最后再由服务器合并成完整的文件。这种做法的好处是可以并行处理多个小文件,提高上传效率;同时,如果某一部分上传失败,只需要重传这一部分,不影响其他部分。

2、秒传

后端

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

java">@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>javascript">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 校验请求类似,上面的案例代码中我就没有重复演示了,松哥在书里和之前的课程里都多次讲过文件上传,这里不再啰嗦。

3、分片上传

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

比如:一个 10MB 的文件,切为 10 份,每份 1MB,那么:

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

把这个搞懂,后面的代码就好理解了。

后端

java">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("上传失败");}
}

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

前端

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

<!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>javascript">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>

4、断点续传

断点续传的技术原理类似于 分片上传。

当文件已经上传了一部分之后,断了需要重新开始上传。

大概思路是这样的:

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

后端

后端检查的接口,如下:

java">@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>javascript">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.ppmy.cn/devtools/134687.html

相关文章

Android fragment ,在Activity崩溃的时候,fragment碎片就会重叠,这样处理,完美

在Activity使用fragment 中&#xff0c;有时候为了减少内存分配&#xff0c;采用hide和show的方法加载&#xff0c;这样能省很多内存&#xff0c;但是在碰见意外bug时候&#xff0c;fragment会重叠&#xff1b; 这样处理&#xff1a; 在Activity 的oncreate方法中&#xff0c;…

系统思考—跳出症状看全局

在深圳圆满结束了两天的《系统思考》公开课&#xff0c;和来自不同企业的管理者们一起度过了充实又深刻的学习时光。 这两天&#xff0c;我们探讨了如何从“头痛医头”的短视思维&#xff0c;转向深层次的系统思考&#xff1b;从局部优化到全局视角&#xff0c;看清企业背后真…

Gin 中自定义控制器

1、控制器分组 当我们的项目比较大的时候有必要对我们的控制器进行分组 新建 controller/admin/NewsController.go package admin import ( "net/http" "github.com/gin-gonic/gin" )

26-ES集群搭建、身份认证配置

虚机搭建 添加es用户 elasticsearch 默认不允许root用户启动&#xff0c;所以需要创建es用户 useradd elasticsearch passwd elasticsearch 解压安装包 #解压es tar -xvzf elasticsearch-7.14.2-linux-x86_64.tar.gz 将文件夹赋予es用户权限 #将文件夹赋予es用户权限 sud…

ctfshow-web入门-SSTI(web369-web372)下

目录 1、web369 2、web370 3、web371 4、web372 1、web369 完全过滤了 request 双大括号也过滤了 包括前面的单双引号、中括号、下划线都是过滤了的 能构造出一些东西 调用属性&#xff1a; ?name{%set gl(((lipsum|string|list).pop(18))~((lipsum|string|list).pop(18…

使用React和Vite构建一个AirBnb Experiences克隆网站

这一篇文章中&#xff0c;我会教你如何做一个AirBnb Experiences的克隆网站。主要涵盖React中Props的使用。 克隆网站最终呈现的效果&#xff1a; 1. 使用vite构建基础框架 npm create vitelatestcd airbnb-project npm install npm run dev2. 构建网站的3个部分 网站从上…

在MATLAB中导入TXT文件的若干方法

这是一篇关于如何在MATLAB中导入TXT文件的文章&#xff0c;包括示例代码和详细说明 文章目录 在MATLAB中导入TXT文件1. 使用readtable函数导入TXT文件示例代码说明 2. 使用load函数导入TXT文件示例代码说明 3. 使用importdata函数导入TXT文件示例代码说明 4. 自定义导入选项示例…

设计模式-Facade(门面模式)GO语言版本

前言 个人理解Facade模式其实日常生活中已经不知不觉就在使用了&#xff0c;基本核心内容就是暴露一些简单操作的接口&#xff0c;实现上将一些内容封装起来。 如上图&#xff0c;外界使用内部子系统时&#xff0c;只需要通过调用facade接口层面的功能&#xff0c;不需要了解子…