使用 Spring Boot + MinIO 实现文件的分片上传、秒传、续传功能开发

embedded/2024/10/9 2:59:31/

使用 Spring Boot + MinIO 实现文件的分片上传、秒传、续传功能开发

在当今的互联网应用中,文件上传是一个常见的功能需求。然而,传统的文件上传方式在面对大文件或不稳定的网络环境时,可能会出现性能瓶颈和上传失败的问题。为了解决这些问题,分片上传、秒传和续传技术应运而生。本文将详细介绍如何使用 Spring Boot 结合 MinIO 来实现这些高级的文件上传功能。

技术选型

  1. Spring Boot:一个快速开发框架,简化了 Spring 应用的搭建和配置。

  2. MinIO:一个高性能的对象存储服务器,支持 S3 协议。

分片上传、秒传和续传原理说明

分片上传:

  • 原理:将大文件分割成多个较小的片段(称为分片),然后分别上传这些分片。这样可以避免一次性传输大文件导致的超时、网络不稳定等问题。每个分片可以独立上传,并且在服务器端可以根据一定的规则重新组合成完整的文件。

  • 优点:提高上传的成功率和稳定性,尤其在网络状况不佳的情况下。可以并行上传多个分片,提高上传速度。

秒传:

  • 原理:在上传文件之前,先计算文件的唯一标识,通常是通过计算文件的哈希值(如 MD5)。服务器端会检查是否已经存在具有相同哈希值的文件。如果存在,则直接认为文件已上传成功,无需再次传输实际的文件内容。

  • 优点:节省上传时间和带宽,对于重复的文件无需再次上传。

续传:

  • 原理:在上传过程中断后,记录已经上传的分片信息。当下次继续上传时,客户端告知服务器已经上传的部分,服务器根据这些信息从上次中断的位置继续接收分片,而不是重新开始上传。

  • 优点:在网络不稳定或其他原因导致上传中断时,无需从头开始上传,提高上传效率。

项目创建及依赖配置(pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.0.0</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.example</groupId><artifactId>file-upload-with-minio</artifactId><version>0.0.1-SNAPSHOT</version><name>File Upload with MinIO</name><properties><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.4.4</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>

配置文件(application.yml)

minio:endpoint: http://localhost:9000accessKey: accessKeysecretKey: secretKeybucketName: your-bucket-name

MinIO 服务类

package com.example.service;import io.minio.MinioClient;
import io.minio.Result;
import io.minio.errors.*;
import io.minio.messages.Item;import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;public class MinIOService {private final MinioClient minioClient;private final String bucketName;public MinIOService(String endpoint, String accessKey, String secretKey, String bucketName) {this.minioClient = MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();this.bucketName = bucketName;}// 检查桶是否存在,如果不存在则创建public void ensureBucketExists() throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException {boolean found = minioClient.bucketExists(bucketName);if (!found) {minioClient.makeBucket(bucketName);}}// 分片上传public void uploadChunk(String objectName, int chunkNumber, InputStream inputStream) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException {minioClient.putObject(bucketName, objectName + "_chunk_" + chunkNumber, inputStream, inputStream.available(), "application/octet-stream");}// 合并分片public void mergeChunks(String objectName, int totalChunks) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException {try (OutputStream outputStream = new ByteArrayOutputStream()) {for (int i = 1; i <= totalChunks; i++) {InputStream chunkInputStream = minioClient.getObject(bucketName, objectName + "_chunk_" + i);byte[] buffer = new byte[1024];int bytesRead;while ((bytesRead = chunkInputStream.read(buffer))!= -1) {outputStream.write(buffer, 0, bytesRead);}chunkInputStream.close();}minioClient.putObject(bucketName, objectName, new ByteArrayInputStream(((ByteArrayOutputStream) outputStream).toByteArray()), ((ByteArrayOutputStream) outputStream).size(), "application/octet-stream");} catch (Exception e) {e.printStackTrace();throw new RuntimeException("合并分片失败", e);}}// 秒传判断public boolean isInstantUpload(String objectName, long fileSize, String md5Hash) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException {try {// 检查文件是否已存在boolean exists = minioClient.statObject(bucketName, objectName).isPresent();if (exists) {// 获取已存在文件的大小和 MD5 哈希值StatObjectResponse statResponse = minioClient.statObject(bucketName, objectName);long existingFileSize = statResponse.size();String existingMd5Hash = statResponse.etag();// 比较大小和 MD5 哈希值if (existingFileSize == fileSize && existingMd5Hash.equals(md5Hash)) {return true;}}return false;} catch (Exception e) {e.printStackTrace();return false;}}// 续传判断public boolean isResumeUpload(String objectName, int uploadedChunks) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException {try {// 获取已上传的分片数量ListObjectsArgs listArgs = ListObjectsArgs.builder().bucket(bucketName).prefix(objectName + "_chunk_").build();Iterable<Result<Item>> results = minioClient.listObjects(listArgs);int existingChunks = 0;for (Result<Item> result : results) {String name = result.get().objectName();if (name.matches(objectName + "_chunk_\\d+")) {existingChunks++;}}// 比较已上传的分片数量return existingChunks == uploadedChunks;} catch (Exception e) {e.printStackTrace();return false;}}}
}

控制器类

package com.example.controller;import com.example.service.MinIOService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;@RestController
public class FileUploadController {@Autowiredprivate MinIOService minIOService;@PostMapping("/uploadChunk")public String uploadChunk(@RequestParam("fileName") String fileName,@RequestParam("chunkNumber") int chunkNumber,@RequestParam("file") MultipartFile file) {try {minIOService.uploadChunk(fileName, chunkNumber, file.getInputStream());return "Chunk uploaded successfully";} catch (Exception e) {e.printStackTrace();return "Upload failed";}}@PostMapping("/mergeChunks")public String mergeChunks(@RequestParam("fileName") String fileName,@RequestParam("totalChunks") int totalChunks) {try {minIOService.mergeChunks(fileName, totalChunks);return "Chunks merged successfully";} catch (Exception e) {e.printStackTrace();return "Merge failed";}}@PostMapping("/checkInstantUpload")public boolean checkInstantUpload(@RequestParam("fileName") String fileName,@RequestParam("fileSize") long fileSize,@RequestParam("md5Hash") String md5Hash) {try {return minIOService.isInstantUpload(fileName, fileSize, md5Hash);} catch (Exception e) {e.printStackTrace();return false;}}@PostMapping("/checkResumeUpload")public boolean checkResumeUpload(@RequestParam("fileName") String fileName,@RequestParam("uploadedChunks") int uploadedChunks) {try {return minIOService.isResumeUpload(fileName, uploadedChunks);} catch (Exception e) {e.printStackTrace();return false;}}
}

前端页面(HTML + JavaScript)

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><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>function startUpload() {// 获取文件let file = document.getElementById('fileInput').files[0];// 计算文件 MD5let reader = new FileReader();reader.readAsArrayBuffer(file);reader.onload = function (e) {let arrayBuffer = e.target.result;let sparkMD5 = new SparkMD5.ArrayBuffer();sparkMD5.append(arrayBuffer);let md5Hash = sparkMD5.end();// 分片大小let chunkSize = 1024 * 1024; // 1MB// 总片数let totalChunks = Math.ceil(file.size / chunkSize);// 分片上传逻辑for (let i = 0; i < totalChunks; i++) {let start = i * chunkSize;let end = Math.min((i + 1) * chunkSize, file.size);let chunk = file.slice(start, end);let formData = new FormData();formData.append('fileName', file.name);formData.append('chunkNumber', i + 1);formData.append('file', chunk);fetch('/uploadChunk', {method: 'POST',body: formData}).then(response => response.text()).then(result => console.log(result)).catch(error => console.error(error));}};}</script>
</body></html>

总结

通过以上的代码实现,我们成功地利用 Spring Boot 和 MinIO 搭建了一个支持分片上传、秒传和续传的文件上传系统。分片上传能够有效地解决大文件上传的问题,提高上传的稳定性和效率;秒传通过文件的唯一标识(如 MD5 哈希值)来判断文件是否已存在,避免重复上传;续传则能够在上传中断后继续从上次的位置进行上传。

在实际应用中,还需要根据具体的需求对代码进行优化和完善,例如处理并发上传、错误处理、文件的权限管理等。希望本文能够为大家在实现类似的文件上传功能时提供有价值的参考和帮助。


http://www.ppmy.cn/embedded/108473.html

相关文章

1.ASRPRO天问--开发板介绍及第一次使用--开发板挖掘系列

目录 1. 前言 2. 正文 2.1 开发板 2.2 简介 2.3 有趣的实验 3. 备注 1. 前言 时光不问赶路人&#xff0c;一切尽在不言中&#xff0c;大家好&#xff0c;我是繁花&#xff0c;oh&#xff0c;不对&#xff0c;是繁华的地方不一定留下你的脚印。开学季的到来&#xff0c;也…

Kafka命令

版本&#xff1a;3.6.0 1.kafka-topics.sh Create, delete, describe, or change a topic 创建、删除、描述或更改主题 查看所有topic kafka-topics.sh --bootstrap-server centos701:9092,centos702:9092,centos704:9092 --list 描述topic详情 kafka-topics.sh --boots…

TeeChart助力科研软件:高效实现数据可视化

在当今的科学研究中&#xff0c;数据可视化已经成为理解和传播复杂信息的关键工具。尤其是在物理研究领域&#xff0c;科学家们经常需要处理大量的数据&#xff0c;并通过可视化将这些数据转化为更易理解的形式。TeeChart作为一个强大且灵活的图形展示工具&#xff0c;能够帮助…

构建数据恢复的硬件基础:MySQL中的硬件要求详解

在企业数据管理中&#xff0c;数据恢复的硬件要求是确保数据安全和业务连续性的关键环节。MySQL作为广泛使用的数据库系统&#xff0c;其数据恢复的硬件要求对于实现有效的备份策略至关重要。本文将深入探讨如何在MySQL中实现数据恢复的硬件要求&#xff0c;包括硬件选择、硬件…

【leetcode详解】爬楼梯:DP入门典例(附DP通用思路 同类进阶练习)

实战总结&#xff1a; vector常用方法&#xff1a; 创建一个长为n的vector&#xff0c;并将所有元素初始化为某一定值x vector<int> vec(len, x) 代码执行过程中将所有元素更新为某一值x fill(vec.begin(), vec.end(), x) // 更多实战方法欢迎参考文章&#xff1a;…

Qt项目使用Inno Setup打包(关于打包中文乱码的解决)

​ 关于打包好的文件乱码解决方法 打包好的文件中文乱码&#xff0c;就是编码格式出现了问题&#xff0c;更改一下中文脚本编码格式&#xff0c;在官网Inno Setup Translations下载好中文脚本 点击下载&#xff0c;然后另存为 得到ChineseSimplified.isl.txt文件后&#…

运维工程师面试题--Linux加分项

1、安装centos步骤 # 创建新虚拟机&#xff0c;指定处理器和内存 # 添加硬盘&#xff0c;设置硬盘大小 # 设置虚拟机从光盘/USB启动 # 启动虚拟机并进入CentOS安装界面# 选择语言和键盘布局 # 存储设备自动分区或手动分区 # 配置网络接口&#xff08;DHCP/静态IP&#xff09; …

linux 检查cpu 内存命令

检查内存 free -lh 检查cpu 数量 cat /proc/cpuinfo | grep architecture 检查磁盘 fdisk -l | grep Disk