minio 文件批量下载

发布于:2025-09-10 ⋅ 阅读:(17) ⋅ 点赞:(0)

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 后端控制器接口
// 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 服务层:流式打包与进度提示
// 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 前端下载与进度条
<!-- 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 推送“已写原始字节数”,前端以此计算更准确进度。

网站公告

今日签到

点亮在社区的每一天
去签到