写点什么

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

作者:江南一点雨
  • 2024-11-12
    广东
  • 本文字数:5223 字

    阅读完需:约 17 分钟

文件上传功能几乎是每个 Web 应用不可或缺的一部分。无论是个人博客中的图片上传,还是企业级应用中的文档管理,文件上传都扮演着至关重要的角色。今天,松哥和大家来聊聊文件上传中的几个高级玩法——秒传、断点续传和分片上传。

一 文件上传的常见场景

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

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

秒传

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

断点续传

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

分片上传

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

三 秒传实战

后端实现

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


@RestController@RequestMapping("/file")public class FileController {    @Autowired    FileService 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 片,从 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; // 每个分片1MB const 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; // 每个分片1MB const 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>
复制代码


这个案例实际上是一个断点续传+分片上传的案例,相关知识点并不难,小伙伴们可以自行体会下。

六 总结

好了,以上就是关于文件上传中秒传、断点续传和分片上传的实战分享。通过这些技术的应用,我们可以极大地提升文件上传的效率和稳定性,改善用户体验。希望各位小伙伴在自己的项目中也能灵活运用这些技巧,解决实际问题。


本文完整案例:https://github.com/lenve/springboot3-samples/tree/main/file_upload

发布于: 刚刚阅读数: 5
用户头像

技术宅 2019-04-09 加入

Java猿

评论

发布
暂无评论
SpringBoot 实战:文件上传之秒传、断点续传、分片上传_江南一点雨_InfoQ写作社区