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

使用 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:9000
  accessKey: accessKey
  secretKey: secretKey
  bucketName: 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 {

    @Autowired
    private 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];

            // 计算文件 MD5
            let 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 哈希值)来判断文件是否已存在,避免重复上传;续传则能够在上传中断后继续从上次的位置进行上传。

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

相关推荐
不惑_17 分钟前
小白入门 · 腾讯云轻量服务器部署 Hadoop 3.3.6
服务器·hadoop·腾讯云
阿甘知识库36 分钟前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
星河梦瑾1 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
机器之心2 小时前
图学习新突破:一个统一框架连接空域和频域
人工智能·后端
计算机学长felix2 小时前
基于SpringBoot的“交流互动系统”的设计与实现(源码+数据库+文档+PPT)
spring boot·毕业设计
.生产的驴2 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
zhou周大哥2 小时前
linux 安装 ffmpeg 视频转换
linux·运维·服务器
顽疲3 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端
机器之心3 小时前
AAAI 2025|时间序列演进也是种扩散过程?基于移动自回归的时序扩散预测模型
人工智能·后端
撒呼呼3 小时前
# 起步专用 - 哔哩哔哩全模块超还原设计!(内含接口文档、数据库设计)
数据库·spring boot·spring·mvc·springboot