minio 文件批量下载

MinIO 批量下载功能说明

1. 功能描述

前端勾选多个对象文件后,一次性将这些对象从 MinIO 拉取并打包成 ZIP,通过浏览器直接下载。整体特性:

  • 支持跨桶批量下载(不同 bucket 的对象可同时下载)。
  • 服务端采用流式压缩边读边写,内存占用低,适合大文件与多文件场景。
  • 前端使用 XMLHttpRequest 的 onprogress 实时展示下载进度:
    • 若服务器返回响应体长度或提供总大小提示,显示确定进度百分比。
    • 否则显示不确定进度(动效)。

2. 时序图(时间序列图)

用户浏览器 前端页面 FileController MinioUtil MinIO服务器 ZIP流 1. 文件列表加载阶段 打开批量下载页面 1 GET /file/listAllFile 2 listAllFile() 3 获取所有存储桶 4 返回桶列表 5 获取桶内文件 6 返回文件列表 7 为文件设置bucket信息 8 loop 遍历每个桶 返回完整文件列表 9 返回JSON响应 10 渲染文件列表 11 2. 批量下载阶段 选择文件并点击下载 12 验证选择,创建FormData 13 POST /file/batchDownload 14 batchDownload(objectNames, buckets, response) 15 3. 服务端处理阶段 统计所有文件总大小 16 设置响应头 X-Total-Bytes 17 创建ZIP输出流 18 下载文件流 19 返回文件数据 20 写入ZIP条目 21 flush()刷新数据 22 触发progress事件 23 更新进度条 24 loop 处理每个文件 关闭ZIP流 25 下载完成 26 响应完成 27 4. 前端下载阶段 创建Blob对象 28 生成下载链接 29 自动触发下载 30 清理资源 31 用户浏览器 前端页面 FileController MinioUtil MinIO服务器 ZIP流

3. 关键代码与说明

3.1 后端控制器接口
java 复制代码
// File: com/wangfugui/apprentice/controller/FileController.java
@ApiOperation("批量下载文件")
@PostMapping("/batchDownload")
public void batchDownload(@RequestParam String[] objectNames,
                         @RequestParam String[] buckets,
                         HttpServletResponse response) throws Exception {
    if (objectNames.length != buckets.length) {
        throw new IllegalArgumentException("文件对象名和存储桶数量不匹配");
    }
    minioUtil.batchDownload(objectNames, buckets, response);
}
  • 参数 objectNames[]buckets[] 按序对应,每个元素指向一个要下载的对象及其桶。
  • 直接将响应流交由 MinioUtil.batchDownload 写入 ZIP 内容。
3.2 服务层:流式打包与进度提示
java 复制代码
// File: com/wangfugui/apprentice/common/util/MinioUtil.java
public void batchDownload(String[] objectNames, String[] buckets, HttpServletResponse response) throws Exception {
    // 统计总字节:以各对象原始大小之和作为估算(ZIP压缩后大小可能不同)
    long totalBytes = 0L;
    for (int i = 0; i < objectNames.length; i++) {
        try {
            totalBytes += minioClient
                .statObject(StatObjectArgs.builder().bucket(buckets[i]).object(objectNames[i]).build())
                .size();
        } catch (Exception e) {
            log.warn("统计对象大小失败,跳过: {}/{} - {}", buckets[i], objectNames[i], e.getMessage());
        }
    }

    response.setContentType("application/zip");
    response.setCharacterEncoding("UTF-8");
    response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("批量下载文件.zip", "UTF-8"));
    response.setHeader("X-Total-Bytes", String.valueOf(totalBytes)); // 前端估算进度的总大小提示
    response.setHeader("Cache-Control", "no-store");

    ServletOutputStream outputStream = response.getOutputStream();
    ZipOutputStream zipOut = new ZipOutputStream(outputStream);

    try {
        for (int i = 0; i < objectNames.length; i++) {
            String objectName = objectNames[i];
            String bucket = buckets[i];
            try (InputStream fileStream = download(bucket, objectName)) {
                String fileName = objectName.substring(objectName.lastIndexOf("/") + 1);
                zipOut.putNextEntry(new ZipEntry(fileName));

                byte[] buffer = new byte[8192];
                int bytesRead;
                while ((bytesRead = fileStream.read(buffer)) != -1) {
                    zipOut.write(buffer, 0, bytesRead);
                    zipOut.flush();
                    response.flushBuffer(); // 关键:推动数据到浏览器,触发前端 onprogress
                }
                zipOut.closeEntry();
                log.info("成功添加文件到ZIP: {}/{}", bucket, objectName);
            } catch (Exception e) {
                log.error("添加文件到ZIP失败: {}/{}, 错误: {}", bucket, objectName, e.getMessage());
                // 忽略单文件错误,继续其他对象
            }
        }
    } finally {
        zipOut.close();
        outputStream.close();
    }
}

关键点说明:

  • 使用 ZipOutputStream 边读边写,避免大文件占用大量内存。
  • 通过 response.setHeader("X-Total-Bytes", ...) 提示前端总大小,提升无法 Content-Length 时的进度可用性。
  • 每次写入后 zipOut.flush()response.flushBuffer(),推动数据尽快到达浏览器,保证 onprogress 事件高频触发。
3.3 前端下载与进度条
html 复制代码
<!-- File: src/main/resources/static/batch-download.html (片段) -->
<div class="section download-section" id="downloadSection" style="display: none;">
  <h3>下载进度</h3>
  <div class="progress-bar" id="downloadProgressBar">
    <div class="progress-fill" id="downloadProgressFill"></div>
  </div>
  <div class="progress-text" id="downloadProgressText">0%</div>
  <!-- 确定/不确定进度两种模式:
       - 有总长度或总大小提示时显示百分比
       - 否则显示不确定动画 -->
  <style>
    .progress-bar.indeterminate .progress-fill { width: 30%; position: absolute; left: -30%; animation: indeterminate-move 1.2s linear infinite; }
    @keyframes indeterminate-move { 0% { left: -30%; } 100% { left: 100%; } }
  </style>
</div>

<script>
// 发送下载请求并实时刷新进度
const xhr = new XMLHttpRequest();
xhr.open('POST', '/file/batchDownload', true);
xhr.responseType = 'blob';

let totalHint = 0; // 后端给的总大小提示
xhr.onreadystatechange = function () {
  if (xhr.readyState === 2) { // HEADERS_RECEIVED
    const headerVal = xhr.getResponseHeader('X-Total-Bytes');
    totalHint = headerVal ? parseInt(headerVal, 10) : 0;
  }
};

xhr.onprogress = function (e) {
  if (e.lengthComputable) {
    setIndeterminate(false);
    setDownloadProgress(Math.floor((e.loaded / e.total) * 100));
    return;
  }
  if (totalHint > 0) {
    setIndeterminate(false);
    setDownloadProgress(Math.min(Math.floor((e.loaded / totalHint) * 100), 99));
  } else {
    setIndeterminate(true);
  }
};

xhr.onload = function () {
  setIndeterminate(false);
  setDownloadProgress(100);
  if (xhr.status >= 200 && xhr.status < 300) {
    const url = window.URL.createObjectURL(xhr.response);
    const a = document.createElement('a');
    a.href = url;
    a.download = '批量下载文件.zip';
    document.body.appendChild(a);
    a.click();
    window.URL.revokeObjectURL(url);
    document.body.removeChild(a);
  }
};

function setDownloadProgress(p) {
  const fill = document.getElementById('downloadProgressFill');
  const text = document.getElementById('downloadProgressText');
  fill.style.width = Math.max(0, Math.min(100, p)) + '%';
  text.textContent = Math.max(0, Math.min(100, p)) + '%';
}
function setIndeterminate(on) {
  const bar = document.getElementById('downloadProgressBar');
  const text = document.getElementById('downloadProgressText');
  if (on) { bar.classList.add('indeterminate'); text.textContent = '正在下载...'; }
  else { bar.classList.remove('indeterminate'); }
}
</script>

要点说明:

  • 采用 XMLHttpRequest(而非 fetch)以便使用 onprogress 事件。
  • 优先使用浏览器提供的 lengthComputable,否则退化为基于 X-Total-Bytes 的估算。
  • 使用两种进度模式:确定百分比与不确定动效,提升不同后端/网络环境下的体验一致性。

4. 小结与建议

  • 生产环境中建议:
    • 若对象较多,可限制每次最大文件数或总字节阈值,避免超大 ZIP 影响响应时延或者内存不足报错。
    • 若需更精确进度:服务端可在每写入 N 字节时通过 SSE/WebSocket 推送"已写原始字节数",前端以此计算更准确进度。
相关推荐
分布式存储与RustFS3 天前
RustFS S3 Table 开源后,我重新梳理了一下 Iceberg 数据湖的选型思路
人工智能·开源·minio·dpu·rustfs·ai存储·s3 table
sg_knight16 天前
负载均衡配置:使用 Nginx 反向代理 MinIO 集群的实战教程
运维·nginx·负载均衡·文件管理·minio·cos·oss
遇见火星1 个月前
MinIO使用笔记
笔记·minio
分布式存储与RustFS2 个月前
Helm在Kubernetes上部署RustFS生产环境指南
分布式·零基础·picgo·对象存储·minio·rustfs
johnny2332 个月前
MinIO分片上传完整实现
minio
sg_knight2 个月前
MinIO 进阶:文件下载、批量获取与打包压缩全攻略
文件管理·minio·ftp·cos·oss·文件服务器
分布式存储与RustFS2 个月前
MinIO迎来“恶龙”?RustFS这款开源存储简直“不讲武德”
架构·rust·开源·对象存储·minio·企业存储·rustfs
sg_knight2 个月前
如何实现“秒传”与“断点续传”?MinIO + Java 实战进阶篇
java·开发语言·文件管理·minio·ftp·oss·文件传输
分布式存储与RustFS2 个月前
AI 数据湖最佳实践:RustFS 支撑大模型训练的存储架构与性能优化
人工智能·性能优化·架构·对象存储·minio·企业存储·rustfs
分布式存储与RustFS2 个月前
Windows原生版RustFS:无需Docker,1分钟本地对象存储环境搭建
windows·docker·容器·对象存储·minio·企业存储·rustfs