Spring Boot 实现分片上传、断点续传与进度条

Spring Boot 实现分片上传、断点续传与进度条


md 复制代码
# Spring Boot 实现分片上传、断点续传与进度条  
## ------ 支持 MinIO / RustFS / SeaweedFS 可配置切换

在大文件上传场景下,传统单次上传存在明显问题:

- 文件过大,失败后需要整体重传
- 网络不稳定,用户体验差
- 无法展示上传进度

本文基于 **Spring Boot**,实现一套**生产可用**的大文件上传方案,支持:

- 分片上传
- 断点续传
- 上传进度查询
- MinIO / RustFS / SeaweedFS / 本地存储
- 通过 yml 配置文件切换存储类型

---

## 一、整体架构设计

系统整体采用「接口隔离 + 策略模式」设计:

Controller ↓ UploadService ↓ StorageService(统一抽象) ↓ MinIO / RustFS / SeaweedFS / Local

yaml 复制代码
**核心思想:上传逻辑与底层存储解耦。**

---

## 二、统一配置设计

### 1. 存储类型切换

```yaml
file:
  path: file/
  prefix: pre
  domain: domain/
  storage:
    type: minio   # minio / rustfs / seaweedfs / local

只需修改 storage.type,即可切换存储实现。


2. MinIO 配置

yaml 复制代码
minio:
  url: http://localhost:9000
  accessKey: minioadmin
  secretKey: minioadmin123
  bucketName: xxx

3. RustFS 配置(S3 协议)

yaml 复制代码
rustfs:
  url: http://localhost:9000
  accessKey: rustfsadmin
  secretKey: rustfsadmin
  bucketName: xxx

RustFS 兼容 S3 协议,可直接复用 MinIO SDK。


4. SeaweedFS 配置

yaml 复制代码
seaweedfs:
  url: http://127.0.0.1:8333
  accessKey: weed
  secretKey: weed
  bucketName: xxx

三、分片上传核心流程

1. 前端切片思路

前端将大文件切割为多个分片(如 5MB):

复制代码
file
├── chunk_0
├── chunk_1
├── chunk_2
└── chunk_n

每个分片上传时携带:

  • 文件唯一标识(guid / md5)
  • 分片索引(chunkIndex)
  • 总分片数(totalChunk)

2. 分片上传接口

http 复制代码
POST /file/chunk/upload

四、断点续传实现

1. 查询已上传分片

http 复制代码
GET /file/chunk/uploaded?guid=xxx

返回示例:

json 复制代码
[0,1,3,5]

前端只上传缺失分片即可。


2. 实现原则

  • 是否已上传以「存储层」为准
  • 不依赖内存或 Redis
  • 服务重启不影响续传

五、上传进度计算

text 复制代码
进度 = 已上传分片数 / 总分片数 × 100%

示例返回:

json 复制代码
{
  "uploaded": 6,
  "total": 10,
  "percent": 60
}

六、存储层抽象设计

1. 统一接口定义

java 复制代码
public interface StorageService {

    void uploadChunk(String path, InputStream inputStream);

    boolean exists(String path);

    List<Integer> listChunks(String prefix);

    void mergeChunks(String chunkPrefix, String targetPath, int totalChunk);

    void deleteChunks(String chunkPrefix);
}

2. MinIO / RustFS 实现(S3 通用)

java 复制代码
@Slf4j
public class MinioStorageService implements StorageService {

    private final MinioClient minioClient;
    private final String bucket;

    public MinioStorageService(MinioClient client, String bucket) {
        this.minioClient = client;
        this.bucket = bucket;
    }

    @Override
    public void uploadChunk(String path, InputStream inputStream) {
        try {
            minioClient.putObject(
                PutObjectArgs.builder()
                    .bucket(bucket)
                    .object(path)
                    .stream(inputStream, -1, 5 * 1024 * 1024)
                    .build()
            );
        } catch (Exception e) {
            throw new RuntimeException("分片上传失败", e);
        }
    }

    @Override
    public boolean exists(String path) {
        try {
            minioClient.statObject(
                StatObjectArgs.builder()
                    .bucket(bucket)
                    .object(path)
                    .build()
            );
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    @Override
    public List<Integer> listChunks(String prefix) {
        List<Integer> chunks = new ArrayList<>();
        Iterable<Result<Item>> results =
            minioClient.listObjects(
                ListObjectsArgs.builder()
                    .bucket(bucket)
                    .prefix(prefix)
                    .build()
            );

        for (Result<Item> r : results) {
            String name = r.get().objectName();
            chunks.add(Integer.parseInt(
                name.substring(name.lastIndexOf("_") + 1)));
        }
        return chunks;
    }

    @Override
    public void mergeChunks(String chunkPrefix,
                            String targetPath,
                            int totalChunk) {
        try {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            for (int i = 0; i < totalChunk; i++) {
                InputStream in = minioClient.getObject(
                    GetObjectArgs.builder()
                        .bucket(bucket)
                        .object(chunkPrefix + "/chunk_" + i)
                        .build()
                );
                IOUtils.copy(in, out);
            }
            uploadChunk(
                targetPath,
                new ByteArrayInputStream(out.toByteArray()));
        } catch (Exception e) {
            throw new RuntimeException("分片合并失败", e);
        }
    }

    @Override
    public void deleteChunks(String chunkPrefix) {
        // 可按需实现批量删除
    }
}

七、业务 Service 实现

java 复制代码
@Service
public class FileUploadServiceImpl implements FileUploadService {

    @Autowired
    private StorageService storageService;

    @Override
    public void uploadChunk(ChunkUploadDTO dto) throws IOException {
        String path = dto.getGuid() + "/chunk_" + dto.getChunkIndex();
        if (storageService.exists(path)) {
            return;
        }
        storageService.uploadChunk(
            path, dto.getFile().getInputStream());
    }

    @Override
    public List<Integer> uploadedChunks(String guid) {
        return storageService.listChunks(guid + "/");
    }

    @Override
    public void merge(String guid, int totalChunk) {
        storageService.mergeChunks(
            guid, guid + ".final", totalChunk);
        storageService.deleteChunks(guid + "/");
    }
}

八、Controller 接口

java 复制代码
@RestController
@RequestMapping("/file/chunk")
public class FileUploadController {

    @Autowired
    private FileUploadService fileUploadService;

    @PostMapping("/upload")
    public void upload(ChunkUploadDTO dto) throws IOException {
        fileUploadService.uploadChunk(dto);
    }

    @GetMapping("/uploaded")
    public List<Integer> uploaded(@RequestParam String guid) {
        return fileUploadService.uploadedChunks(guid);
    }

    @PostMapping("/merge")
    public void merge(@RequestParam String guid,
                      @RequestParam Integer totalChunk) {
        fileUploadService.merge(guid, totalChunk);
    }
}

九、总结

本文实现了一套 Spring Boot 大文件上传方案,具备:

  • 分片上传
  • 断点续传
  • 上传进度计算
  • 多存储后端解耦
  • yml 配置快速切换

适用于文件中心、数据平台、企业网盘等场景,可直接用于生产环境


欢迎点赞、收藏、评论交流。

相关推荐
k***92161 天前
如何在C++的STL中巧妙运用std::find实现高效查找
java·数据结构·c++
三十_1 天前
WebRTC 入门:一分钟理解会议系统的三种架构(Mesh/SFU/MCU)
前端·后端·webrtc
君爱学习1 天前
Spring AI简介
java
EnzoRay1 天前
注解
java
嘻哈baby1 天前
Redis缓存三大问题实战:穿透、雪崩、击穿怎么解决
后端
宇宙之大,无奇不有(一个玩暗区的人)1 天前
[NOIP 2011 普及组]T1 数字反转
java·开发语言·算法
Cache技术分享1 天前
282. Java Stream API - 从 Collection 或 Iterator 创建 Stream
前端·后端
用户3074596982071 天前
ThinkPHP 6.0 多应用模式下的中间件机制详解
后端·thinkphp
格格步入1 天前
支付幂等:一锁二判三更新
后端