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.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
How to Stream Large Files into a Zip for Secure Download in Java Spring

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.txt

Data 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.

SpringFile DownloadzipStreaming I/O
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.