springboot 文件高效上传

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

1、总述

秒传

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

断点续传

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

分片上传

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

2、秒传

后端

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

3、分片上传

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

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

  • 第 0 片,从 0 开始,一共是 1024*1024 个字节。
  • 第 1 片,从 1024*1024 开始,一共是 1024*1024 个字节。
  • 第 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("上传失败");}
}

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

前端

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

<!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 = 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. 前端根据已经存在的大小,继续上传文件即可。

后端

后端检查的接口,如下:

@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/61018.shtml

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

相关文章

MySQL 中的数据排序是怎么实现的

MySQL 内部数据排序机制 1. 排序算法 MySQL 使用不同的算法来对数据进行排序&#xff0c;通常依据数据量和是否有索引来决定使用哪种排序算法。主要的排序算法包括&#xff1a; 文件排序 (File Sort)&#xff1a;这是 MySQL 默认的排序算法&#xff0c;用于无法利用索引或内…

199. 二叉树的右视图【 力扣(LeetCode) 】

文章目录 零、原题链接一、题目描述二、测试用例三、解题思路四、参考代码 零、原题链接 199. 二叉树的右视图 一、题目描述 给定一个二叉树的 根节点 root&#xff0c;想象自己站在它的右侧&#xff0c;按照从顶部到底部的顺序&#xff0c;返回从右侧所能看到的节点值。 二…

22.useNavigatorOnLine

React useNavigatorOnLine 钩子:如何实时监测用户的在线状态? 在现代 Web 应用中,实时监测用户的在线状态对于提供良好的用户体验至关重要。无论是离线功能还是网络状态提示,都需要准确地知道用户的连接状态。useNavigatorOnLine 钩子提供了一种简单而有效的方式来在 Reac…

Mongo数据库集群搭建

目录 1、Mongo集群优势 1.1 高可用性 1.2 水平扩展性 1.3 高性能 1.4 灵活的架构设计 1.5 数据安全 1.6 管理与监控 2、下载指定操作系统版本包 3、部署和验证工作 3.1 准备配置文件及依赖 3.2 启动第一个节点 3.3 部署更多的节点 3.4 初始化副本集 3.5 设置管理…

DB Type

P位 p 1时段描述符有效&#xff0c;p 0时段描述符无效 Base Base被分成了三个部分&#xff0c;按照实际拼接即可 G位 如果G 0 说明描述符中Limit的单位是字节&#xff0c;如果是G 1 &#xff0c;那么limit的描述的单位是页也就是4kb S位 S 1 表示代码段或者数据段描…

大语言模型通用能力排行榜(2024年11月8日更新)

数据来源SuperCLUE 榜单数据为通用能力排行榜 排名 模型名称 机构 总分 理科 文科 Hard 使用方式 发布日期 - o1-preview OpenAI 75.85 86.07 76.6 64.89 API 2024年11月8日 - Claude 3.5 Sonnet&#xff08;20241022&#xff09; Anthropic 70.88 82.4…

【Unity基础】对比OnCollisionEnter与OnTriggerEnter

在Unity中&#xff0c;OnCollisionEnter 和 OnTriggerEnter 是两种用于处理碰撞的回调函数&#xff0c;但它们的工作方式和使用场景有所不同&#xff1a; 1. OnCollisionEnter 触发条件&#xff1a;当一个带有 Collider 组件并且**未勾选“Is Trigger”**的物体&#xff0c;与…

利用OpenAI进行测试需求分析——从电商网站需求到测试用例的生成

在软件测试工程师的日常工作中&#xff0c;需求分析是测试工作中的关键步骤。需求文档决定了测试覆盖的范围和测试策略&#xff0c;而测试用例的编写往往依赖于需求的准确理解。传统手工分析需求耗时长&#xff0c;尤其在面对大量需求和复杂逻辑时容易遗漏细节。本文将以电商网…

vue之axios根据某个接口创建实例,并设置headers和超时时间,捕捉异常

import axiosNew from axios;//给axios起个别名//创建常量实例 const instanceNew axiosNew.create({//axios中请求配置有baseURL选项&#xff0c;表示请求URL的公共部分&#xff0c;url baseUrl requestUrlbaseURL: baseURL,//设置超时时间为20秒timeout: 20000,headers: {…

【数学二】线性代数-二次型

考试要求 1、了解二次型的概念, 会用矩阵形式表示二次型,了解合同变换与合同矩阵的概念. 2、了解二次型的秩的概念,了解二次型的标准形、规范形等概念,了解惯性定理,会用正交变换和配方法化二次型为标准形。 3、理解正定二次型、正定矩阵的概念,并掌握其判别法. 二次型…

Qt 5.6.3 手动配置 mingw 环境

- 安装 qt 5.6.3 mingw 版 - 打开 qt creator - 找到选项 工具 - 选项- 构建和运行 - 找到 “编译器” 选项卡 ,点击 "添加" “编译器路径” 设置为 qt 安装目录下&#xff0c; tool 文件夹内的 g.exe 设置完成后&#xff0c;点击 "apply" ,使选项生…

pytorch tensor在CPU和GPU之间转换,numpy之间的转换

# input input.cpu().numpy() input input.cpu().detach().numpy() # 有gradCPU tensor转GPU tensor&#xff1a; cpu_imgs.cuda()GPU tensor 转CPU tensor&#xff1a; gpu_imgs.cpu()numpy转为CPU tensor&#xff1a; torch.from_numpy( imgs )4.CPU tensor转为numpy数…

k8s上部署redis高可用集群

介绍&#xff1a; Redis Cluster通过分片&#xff08;sharding&#xff09;来实现数据的分布式存储&#xff0c;每个master节点都负责一部分数据槽&#xff08;slot&#xff09;。 当一个master节点出现故障时&#xff0c;Redis Cluster能够自动将故障节点的数据槽转移到其他健…

抖音热门素材去哪找?优质抖音视频素材网站推荐!

是不是和我一样&#xff0c;刷抖音刷到停不下来&#xff1f;越来越多的朋友希望在抖音上创作出爆款视频&#xff0c;但苦于没有好素材。今天就来推荐几个超级实用的抖音视频素材网站&#xff0c;让你的视频内容立刻变得高大上&#xff01;这篇满是干货&#xff0c;直接上重点&a…

PHP 语法基础

PHP 语法基础 PHP&#xff08;Hypertext Preprocessor&#xff09;是一种广泛使用的开源服务器端脚本语言&#xff0c;特别适用于网页开发&#xff0c;并且可以嵌入HTML中使用。PHP的语法混合了C、Java、Perl以及PHP自创的语法&#xff0c;易于学习和使用。本文将详细介绍PHP的…

Dify 通过导入 DSL 文件创建 Workflow 过程及实现

本文使用 Dify v0.9.2 版本&#xff0c;主要介绍 Dify 通过导入 DSL&#xff08;或 URL&#xff09;文件创建&#xff08;或导出&#xff09;Workflow 的操作过程及源码分析实现过程。Dify通过导入DSL文件创建Workflow过程及实现&#xff1a;https://z0yrmerhgi8.feishu.cn/wik…

代码随想录第46期 单调栈

这道题主要是单调栈的简单应用 class Solution { public:vector<int> dailyTemperatures(vector<int>& T) {vector<int> result(T.size(),0);stack<int> st;st.push(0);for(int i1;i<T.size();i){if(T[i]<T[st.top()]){st.push(i);}else{wh…

自制C++游戏头文件:C++自己的游戏头文件!!!(后续会更新)

引言 在这个数字时代&#xff0c;计算机游戏已经成为人们生活中不可或缺的一部分。它们不仅为我们带来了无尽的乐趣&#xff0c;还激发了我们的创造力和解决问题的能力。今天&#xff0c;我们将深入探讨一个特别的头文件——CPPgame.h&#xff0c;它包含了多个结构体和函数&am…

【项目开发】理解SSL延迟:为何HTTPS比HTTP慢?

未经许可,不得转载。 文章目录 前言HTTP与HTTPS的耗时差异TCP握手HTTPS的额外步骤:SSL握手使用curl测量SSL延迟性能与安全的权衡前言 在互联网发展的早期阶段,Netscape公司设计了SSL(Secure Sockets Layer)协议,为网络通信提供加密和安全性。有人曾提出一个大胆的设想:…

3步实现贪吃蛇

方法很简单&#xff0c;打开页面&#xff0c;复制&#xff0c;粘贴 一.整体思维架构 我们根据游戏的开始&#xff0c;运行&#xff0c;结束&#xff0c;将整个游戏划分成三个部分。在每个部分下面又划分出多个功能&#xff0c;接下来我们就根据模块一一实现功能。 二.Gamesta…