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.mzph.cn/web/78796.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Elasticsearch】在kibana中能获取已创建的api keys吗?

在 Kibana 中&#xff0c;目前没有直接的界面功能可以列出或查看已创建的 API 密钥&#xff08;API keys&#xff09;。API 密钥的管理和查看主要通过 Elasticsearch 的 REST API 来完成&#xff0c;而不是通过 Kibana 的管理界面。 在 Kibana 中使用 Dev Tools 查看 API 密钥…

公司项目架构搭建者

公司项目架构搭建者分析 项目架构搭建的核心角色 #mermaid-svg-FzOOhBwW3tctx2AR {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-FzOOhBwW3tctx2AR .error-icon{fill:#552222;}#mermaid-svg-FzOOhBwW3tctx2AR .err…

《技术驯化情感:AI伴侣、监控与伦理框架的重构挑战》

技术渗透与情感异化机制 情感计算技术正通过多种核心算法和数据处理方法深入人类生活&#xff0c;其在重构人类情感关系的同时也潜藏情感异化风险。本节从生物特征捕捉、行为模式诱导和认知框架重塑三方面解析情感计算的技术机理&#xff0c;并探讨其导致的情感依赖现象。 生物…

32单片机——独立看门狗

1、IWDG的简介 IWDG&#xff1a;Independent watchdog&#xff0c;即独立看门狗 独立看门狗本质上是一个定时器&#xff0c;该定时器是一个12位的递减计数器&#xff0c;当计数器的值减到0的时候&#xff0c;就会产生一个复位信号 如果在计数没减到0之前&#xff0c;重置计数器…

[计算机网络]数据链路层

408考纲(数链层部分): 0 概论&#xff1a;数据链路层都干什么事&#xff0c;提供啥功能 比物理层再高一层就是数据链路层&#xff0c;咱们上一篇讲物理层&#xff0c;物理层直接接触传输介质&#xff0c;现在数据链路层是使用物理层的传输服务&#xff0c;然后实现更多的功能。…

OpenAI大变革!继续与微软等,以非营利模式冲击AGI

今天凌晨2点&#xff0c;OpenAI宣布&#xff0c;将继续由非营利组织控制&#xff1b;现有的营利性实体将转变为一家公共利益公司&#xff1b;非营利组织将控制该公共利益公司&#xff0c;并成为其重要的持股方。 这也就是说OpenAI曾在去年提到的由非营利性转变成营利性公司&am…

库存怎么管?怎样才能做到有效的库存管理?

说到库存管理&#xff0c;估计大多数老板和管理者都有过“烦心事”。一方面&#xff0c;库存过多&#xff0c;货物堆积如山&#xff0c;堆在仓库里也不动&#xff0c;结果占地方还占用资金&#xff1b;另一方面&#xff0c;又有可能遇到客户急着要货&#xff0c;可是库存却紧张…

Kotlin-空值和空类型

变量除了能引用一个具体的值之外,还有一种特殊的值,那就是 null, 它代表空值, 也就是不引用任何对象 在Kotlin中, 对空值的处理是非常严格的,正常情况下,我们的变量是不能直接赋值为 null 的,否则无法编译通过, 这直接在编译阶段就避免了空指针问题 Kotlin中所有的类型默认都是…

[特殊字符]算法次元突破:螺旋矩阵的“能量解码术” vs 超立方体的“维度折叠指南”

&#x1f50d; 引言 如果科幻电影中的能量矩阵是算法的考题&#xff0c;你会用螺旋指针破解它的DNA吗&#xff1f; 如果《星际穿越》的五维空间变成编程题&#xff0c;你敢用动态规划丈量时间的褶皱吗&#xff1f; 今天&#xff0c;我们将化身算法世界的能量解…

高光谱相机赋能烟叶分选:精准、高效与智能化的新突破

烟草产业作为中国重要的经济支柱&#xff0c;烟叶分选的质量与效率直接影响行业效益。传统人工分选存在效率低、主观性强、标准难以统一等问题&#xff0c;而机器视觉技术受限于可见光波段&#xff0c;难以捕捉烟叶深层特征。深圳中达瑞和科技有限公司推出的高光谱相机解决方案…

矩阵求导常用公式解析:标量、向量与矩阵的导数计算

矩阵求导常用公式解析&#xff1a;标量、向量与矩阵的导数计算 矩阵求导常用公式解析&#xff1a;标量、向量与矩阵的导数计算矩阵求导的布局问题1. 分子布局 vs 分母布局对比表2. 布局冲突的典型场景分析3. 混合布局的兼容性处理 一、标量对向量求导1. 线性函数求导2. 二次型函…

NocoDB:开源的 Airtable 替代方案

NocoDB:开源的 Airtable 替代方案 什么是 NocoDB?NocoDB 的主要特点丰富的电子表格界面工作流自动化应用商店程序化访问NocoDB 的应用场景使用 Docker 部署 NocoDB1. 创建数据目录2. 运行 Docker 容器3. 访问 NocoDB注意事项总结什么是 NocoDB? NocoDB 是一款功能强大的开源…

全格式文档转 Markdown 工具,Docker 一键部署,支持 API 调用

以下是简要介绍&#xff1a; 这是一款可以快速将任意文档文件转markdown格式内容的工具&#xff0c;提供API转换接口&#xff0c;方便集成与应用原理就是利用libreoffice、pandoc文件转换工具&#xff0c;把所有文档类型的文件逐步转化&#xff0c;最终转成markdown格式的内容…

MATLAB绘制饼图(二维/三维)

在数据分析与展示领域&#xff0c;饼图是一种直观且高效的可视化工具&#xff0c;能够在瞬间传递各部分与整体的比例关系。今天&#xff0c;我将分享一段 MATLAB 绘制二维及三维饼图的代码&#xff0c;助你轻松将数据以饼图形式呈现于众人眼前。 无论是二维饼图的简洁明了&…

AI笔记-1

Halide Perovskites (HPs) 卤化物钙钛矿 卤化物钙钛矿&#xff08;HPs&#xff09;已被 公认为 光伏和发光器件 中最有前途的材料之一 在本观点中&#xff0c;我们将探讨钙钛矿的定义&#xff0c;主要聚焦于由 较重卤素&#xff08;Cl、Br和I&#xff09;组成的钙钛矿亚群&…

excel表数据导入数据库

前两天&#xff0c;有个两DB之间的数据导出导入的需求。对方提供的是excel表&#xff0c;我这边是mysql数据库&#xff0c;excel表第一行是字段名&#xff0c;之后的行是记录的值。 其实没有多复杂&#xff0c;我先将exel转成csv&#xff0c;结果mysql导入csv&#xff0c;第一行…

智能机器人在物流行业的应用:效率提升与未来展望

随着全球电子商务的蓬勃发展&#xff0c;物流行业正面临着前所未有的挑战和机遇。传统的物流模式已经难以满足日益增长的市场需求&#xff0c;尤其是在效率、成本控制和精准配送方面。智能机器人技术的出现&#xff0c;为物流行业的转型升级提供了强大的动力。本文将探讨智能机…

如何对 Redis 进行水平扩展和垂直扩展以应对微服务流量的增长?

核心概念&#xff1a; 垂直扩展 (Scale Up): 提升单个节点的性能。简单来说就是给现有的 Redis 服务器增加更多的 CPU 、内存、更快的存储&#xff08;SSD&#xff09;或更高的网络带宽。水平扩展 (Scale Out): 增加更多节点来分担负载。这意味着部署多个 Redis 实例&#xff…

Elasticsearch知识汇总之ElasticSearch与OpenSearch比较

四 ElasticSearch与OpenSearch比较 OpenSearch项目分为 OpenSearch&#xff08;源自 Elasticsearch 7.10.2&#xff09;与 OpenSearch Dashboards&#xff08;源自 Kibana 7.10.2&#xff09;两部分。此外&#xff0c;OpenSearch 项目也将成为之前发布的 Elasticsearch 发行版&…

《OmniMeetProTrack 全维会议链智能追录系统 软件设计文档》

撰稿人&#xff1a;wjz 一、引言 1.1 目的 本软件设计文档详细描述了 OmniMeetProTrack 全维会议链智能追录系统的架构、组件、模块设计及实现细节&#xff0c;旨在为开发人员、利益相关者和维护人员提供系统的全面设计蓝图。本文档基于需求定义文档&#xff0c;确保系统实现…