当前位置: 首页 > java >正文

Spring Boot 实现多种来源的 Zip 多层目录打包下载(本地文件HTTP混合)

需要将一批文件(可能分布在不同目录、不同来源)打包成Zip格式,按目录结构导出给用户下载。


1. 核心思路

  • 支持将本地服务器上的文件(如/data/upload/xxx.jpg)打包进Zip,保持原有目录结构。
  • 支持通过HTTP下载远程文件写入Zip。
  • 所有写入Zip的目录名、文件名均需安全处理。
  • 统一使用流式IO,适合大文件/大量文件导出,防止内存溢出。
  • 目录下无文件时写入empty.txt标识。

2. 代码实现

2.1 工具类:本地&HTTP两种方式写入Zip

package com.example.xiaoshitou.utils;import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;/**** @title* @author shijiangyong* @date 2025/4/28 16:34**/
public class ZipDownloadUtils {private static final String SUFFIX_ZIP = ".zip";private static final String UNNAMED = "未命名";/*** 安全处理文件名/目录名* @param name* @return*/public static String safeName(String name) {if (name == null) return "null";return name.replaceAll("[\\\\/:*?\"<>|]", "_");}/*** HTTP下载写入Zip* @param zipOut* @param fileUrl* @param zipEntryName* @throws IOException*/public static void writeHttpFileToZip(ZipArchiveOutputStream zipOut, String fileUrl, String zipEntryName) throws IOException {ZipArchiveEntry entry = new ZipArchiveEntry(zipEntryName);zipOut.putArchiveEntry(entry);try (InputStream in = openHttpStream(fileUrl, 8000, 20000)) {byte[] buffer = new byte[4096];int len;while ((len = in.read(buffer)) != -1) {zipOut.write(buffer, 0, len);}} catch (Exception e) {zipOut.write(("下载失败: " + fileUrl).getBytes(StandardCharsets.UTF_8));}zipOut.closeArchiveEntry();}/*** 本地文件写入Zip* @param zipOut* @param localFilePath* @param zipEntryName* @throws IOException*/public static void writeLocalFileToZip(ZipArchiveOutputStream zipOut, String localFilePath, String zipEntryName) throws IOException {File file = new File(localFilePath);if (!file.exists() || file.isDirectory()) {writeTextToZip(zipOut, zipEntryName + "_empty.txt", "文件不存在或是目录: " + localFilePath);return;}ZipArchiveEntry entry = new ZipArchiveEntry(zipEntryName);zipOut.putArchiveEntry(entry);try (InputStream fis = new FileInputStream(file)) {byte[] buffer = new byte[4096];int len;while ((len = fis.read(buffer)) != -1) {zipOut.write(buffer, 0, len);}}zipOut.closeArchiveEntry();}/*** 写入文本文件到Zip(如empty.txt)* @param zipOut* @param zipEntryName* @param content* @throws IOException*/public static void writeTextToZip(ZipArchiveOutputStream zipOut, String zipEntryName, String content) throws IOException {ZipArchiveEntry entry = new ZipArchiveEntry(zipEntryName);zipOut.putArchiveEntry(entry);zipOut.write(content.getBytes(StandardCharsets.UTF_8));zipOut.closeArchiveEntry();}/*** 打开HTTP文件流* @param url* @param connectTimeout* @param readTimeout* @return* @throws IOException*/public static InputStream openHttpStream(String url, int connectTimeout, int readTimeout) throws IOException {URLConnection conn = new URL(url).openConnection();conn.setConnectTimeout(connectTimeout);conn.setReadTimeout(readTimeout);return conn.getInputStream();}/*** 从url获取文件名* @param url* @return* @throws IOException*/public static String getFileName(String url)  {return url.substring(url.lastIndexOf('/')+1);}/*** 设置response* @param request* @param response* @param fileName* @throws UnsupportedEncodingException*/public static void setResponse(HttpServletRequest request, HttpServletResponse response, String fileName) throws UnsupportedEncodingException {if (!StringUtils.hasText(fileName)) {fileName = LocalDate.now() + UNNAMED;}if (!fileName.endsWith(SUFFIX_ZIP)) {fileName = fileName + SUFFIX_ZIP;}response.setHeader("Connection", "close");response.setHeader("Content-Type", "application/octet-stream;charset=UTF-8");String filename = encodeFileName(request, fileName);response.setHeader("Content-Disposition", "attachment;filename=" + filename);}/*** 文件名在不同浏览器兼容处理* @param request 请求信息* @param fileName 文件名* @return* @throws UnsupportedEncodingException*/public static String encodeFileName(HttpServletRequest request, String fileName) throws UnsupportedEncodingException {String userAgent = request.getHeader("USER-AGENT");// 火狐浏览器if (userAgent.contains("Firefox") || userAgent.contains("firefox")) {fileName = new String(fileName.getBytes(), "ISO8859-1");} else {// 其他浏览器fileName = URLEncoder.encode(fileName, "UTF-8");}return fileName;}
}

2.2 Controller 示例:按本地目录结构批量导出

假设有如下导出结构:

用户A/身份证/xxx.jpg (本地)xxx.png (本地)头像/xxx.jpg (HTTP)
用户B/empty.txt

模拟数据结构:

zipGroup:

import lombok.AllArgsConstructor;
import lombok.Data;import java.util.List;/**** @title* @author shijiangyong* @date 2025/4/28 16:36**/
@Data
@AllArgsConstructor
public class ZipGroup {/*** 用户名、文件名*/private String dirName;private List<ZipSubDir> subDirs;
}

zipGroupDir:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.List;/**** @title* @author shijiangyong* @date 2025/4/28 16:37**/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ZipSubDir {/*** 子目录*/private String subDirName;private List<ZipFileRef> fileRefs;
}

ZipFileRef:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/**** @title* @author shijiangyong* @date 2025/4/28 16:38**/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ZipFileRef {/*** 文件名*/private String name;/*** 本地路径*/private String localPath;/*** http路径*/private String httpUrl;
}

Controller通用代码:

package com.example.xiaoshitou.controller;import com.example.xiaoshitou.service.ZipService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/**** @title* @author shijiangyong* @date 2025/4/28 16:50**/
@RestController
@RequestMapping("/zip")
@AllArgsConstructor
public class ZipController {private final ZipService zipService;/***  打包下载* @param response*/@GetMapping("/download")public void downloadZip(HttpServletRequest request, HttpServletResponse response) {zipService.downloadZip(request,response);}
}

Service 层代码:

package com.example.xiaoshitou.service.impl;import com.example.xiaoshitou.entity.ZipFileRef;
import com.example.xiaoshitou.entity.ZipGroup;
import com.example.xiaoshitou.entity.ZipSubDir;
import com.example.xiaoshitou.service.ZipService;
import com.example.xiaoshitou.utils.ZipDownloadUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.springframework.stereotype.Service;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.zip.Deflater;/**** @title* @author shijiangyong* @date 2025/4/28 16:43**/
@Slf4j
@Service
public class ZipServiceImpl implements ZipService {@Overridepublic void downloadZip(HttpServletRequest request, HttpServletResponse response) {// ==== 示例数据 ====List<ZipGroup> data = Arrays.asList(new ZipGroup("小明", Arrays.asList(new ZipSubDir("身份证(本地)", Arrays.asList(new ZipFileRef("","E:/software/test/1.png",""),new ZipFileRef("","E:/software/test/2.png",""))),new ZipSubDir("头像(http)", Arrays.asList(// 百度随便找的new ZipFileRef("","","https://pic4.zhimg.com/v2-4d9e9f936b9968f53be22b594aafa74f_r.jpg"))))),new ZipGroup("小敏", Collections.emptyList()));try (BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(bos)) {String fileName = "资料打包_" + System.currentTimeMillis() + ".zip";ZipDownloadUtils.setResponse(request,response, fileName);// 快速压缩zipOut.setLevel(Deflater.BEST_SPEED);for (ZipGroup group : data) {String groupDir = ZipDownloadUtils.safeName(group.getDirName()) + "/";List<ZipSubDir> subDirs = group.getSubDirs();if (subDirs == null || subDirs.isEmpty()) {groupDir = ZipDownloadUtils.safeName(group.getDirName()) + "(无资料)/";ZipDownloadUtils.writeTextToZip(zipOut, groupDir + "empty.txt", "该目录无任何资料");continue;}for (ZipSubDir subDir : subDirs) {String subDirPath = groupDir + ZipDownloadUtils.safeName(subDir.getSubDirName()) + "/";List<ZipFileRef> fileRefs = subDir.getFileRefs();if (fileRefs == null || fileRefs.isEmpty()) {subDirPath = groupDir + ZipDownloadUtils.safeName(subDir.getSubDirName()) + "(empty)/";ZipDownloadUtils.writeTextToZip(zipOut, subDirPath + "empty.txt", "该类型无资料");continue;}for (ZipFileRef fileRef : fileRefs) {if (fileRef.getLocalPath() != null && !fileRef.getLocalPath().isEmpty()) {String name = ZipDownloadUtils.getFileName(fileRef.getLocalPath());fileRef.setName(name);ZipDownloadUtils.writeLocalFileToZip(zipOut, fileRef.getLocalPath(), subDirPath + ZipDownloadUtils.safeName(fileRef.getName()));} else if (fileRef.getHttpUrl() != null && !fileRef.getHttpUrl().isEmpty()) {String name = ZipDownloadUtils.getFileName(fileRef.getHttpUrl());fileRef.setName(name);ZipDownloadUtils.writeHttpFileToZip(zipOut, fileRef.getHttpUrl(), subDirPath + ZipDownloadUtils.safeName(fileRef.getName()));}}}}zipOut.finish();zipOut.flush();response.flushBuffer();} catch (Exception e) {throw new RuntimeException("打包下载失败", e);}}
}

3. 常见问题及安全建议

  • 防路径穿越(Zip Slip):所有目录/文件名务必用safeName过滤特殊字符
  • 大文件/大批量:建议分页、分批处理
  • 空目录写入:统一写empty.txt标识空目录
  • 本地文件不存在:Zip包内写入提示信息
  • HTTP下载失败:Zip包内写入“下载失败”提示
  • 避免泄露服务器绝对路径:仅在日志中记录本地路径,Zip内不暴露
  • 权限校验:实际生产需验证用户是否有权访问指定文件

4. 总结

这里介绍了如何从本地服务器路径HTTP混合读取文件并Zip打包下载,目录结构灵活可控。可根据实际需求扩展更多来源类型(如数据库、对象存储等)。

http://www.xdnf.cn/news/3235.html

相关文章:

  • windows 使用websocket++ (C++环境)
  • 高效管理远程服务器Termius for Mac 保姆级教程
  • 第三部分:走向共产主义 第二章:科技发展(续)
  • 使用Dagster定义数据资产:从入门到实践
  • Unity编辑器扩展之导出项目中所有预制体中文本组件文字内容
  • 提示词工程(GOT)把思维链推理过程图结构化
  • 移动端akamai风控分析
  • 【阿里云大模型高级工程师ACP习题集】2.7 通过微调增强模型能力 (下篇)(⭐️⭐️⭐️ 重点章节!!!)
  • 【LLM】基于 Ollama 部署 DeepSeek-R1 本地大模型
  • 2025 Java八股文深度解读版:原理+场景+高频追问答案
  • 【Unity】如何解决UI中的Button无法绑定带参数方法的问题
  • 【网工第6版】第6章 网络安全②
  • JESD204B 探究
  • VS Code技巧2:识别FreeCAD对象
  • Spring的源码Spring的上下文怎么存储
  • Electron Forge【实战】自定义菜单 -- 顶部菜单 vs 右键快捷菜单
  • 百度网盘golang实习面经
  • HTML from表单中只有一个input时,按回车键后表单自动提交(form表单的一个小坑)
  • 【C++】频繁分配和释放会产生内存碎片
  • Win下的Kafka安装配置
  • Tauri v1 与 v2 配置对比
  • 全面解析SimHash算法:原理、对比与Spring Boot实践指南
  • transformer-实现解码器Decoder
  • DIT(Diffusion In Transformer)学习笔记
  • Java继承中super的使用方法
  • SI5338-EVB Usage Guide(LVPECL、LVDS、HCSL、CMOS、SSTL、HSTL)
  • 电子病历高质量语料库构建方法与架构项目(智能数据目录篇)
  • SD - WAN 跨境网络专线部署方式介绍
  • 大数据在远程医疗中的创新应用:如何重塑医疗行业的未来
  • python + segno 生成个人二维码