How to Stream Large Files into a Zip for Secure Download in Java Spring
Learn how to efficiently package local and remote files into a Zip archive while preserving directory structure, using streaming I/O in Java Spring, with safe filename handling, empty directory markers, error handling, and best practices for large or batch downloads.
1. Core Idea
Support packaging local server files (e.g., /data/upload/xxx.jpg) into a Zip while preserving the original directory structure.
Support writing remote HTTP‑downloaded files into the Zip.
All directory and file names must be safely processed.
Use streaming I/O suitable for large files or large numbers of files to prevent memory overflow.
If a directory contains no files, write an empty.txt marker.
2. Code Implementation
2.1 Utility class: write local & HTTP files to 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;
public class ZipDownloadUtils {
private static final String SUFFIX_ZIP = ".zip";
private static final String UNNAMED = "未命名";
/** Safe handling of file/dir names */
public static String safeName(String name) {
if (name == null) return "null";
return name.replaceAll("[\\/:*?\"<>|]", "_");
}
/** HTTP download into Zip */
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();
}
/** Local file into Zip */
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();
}
/** Write text file to Zip (e.g., empty.txt) */
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();
}
/** Open HTTP stream */
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();
}
/** Extract file name from URL */
public static String getFileName(String url) {
return url.substring(url.lastIndexOf('/') + 1);
}
/** Set response headers for download */
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);
}
/** Encode file name for different browsers */
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 example: batch export by local directory structure
Assume the following export structure:
用户A/
身份证/
xxx.jpg (本地)
xxx.png (本地)
头像/
xxx.jpg (HTTP)
用户B/
empty.txtData model definitions:
@Data
@AllArgsConstructor
public class ZipGroup {
private String dirName;
private List<ZipSubDir> subDirs;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ZipSubDir {
private String subDirName;
private List<ZipFileRef> fileRefs;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ZipFileRef {
private String name;
private String localPath;
private String httpUrl;
}Controller generic code:
@RestController
@RequestMapping("/zip")
@AllArgsConstructor
public class ZipController {
private final ZipService zipService;
@GetMapping("/download")
public void downloadZip(HttpServletRequest request, HttpServletResponse response) {
zipService.downloadZip(request, response);
}
}Service layer implementation:
@Service
@Slf4j
public class ZipServiceImpl implements ZipService {
@Override
public void downloadZip(HttpServletRequest request, HttpServletResponse response) {
// ==== Sample data ====
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. Common Issues and Security Recommendations
Path traversal (Zip Slip): Filter all directory/file names with safeName to remove special characters.
Large files / batch processing: Recommend pagination or batch handling to avoid memory pressure.
Empty directories: Write an empty.txt file to indicate an empty folder.
Local file missing: Write a notice inside the Zip instead of failing silently.
HTTP download failure: Write a “download failed” message into the Zip.
Avoid exposing server absolute paths: Log paths only on the server; do not include them in the archive.
Permission checks: In production, verify that the user has rights to access the requested files.
4. Summary
This article demonstrates how to read files from local server paths and HTTP sources, combine them into a Zip archive for download, and maintain flexible directory structures, with suggestions for extending to other sources such as databases or object storage.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
