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.pswp.cn/web/78796.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/78796.shtml
英文地址,請注明出處:http://en.pswp.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;確保系統實現…