SpringBoot压缩目录为zip文件后下载给客户端

经常遇到需求,服务器要批量压缩一个或者多个文件,响应给客户端。这里演示一个Demo。

客户端通过参数指定服务器上的某个文件夹,服务器对该文件夹进行zip压缩后响应给客户端。

Controller

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.LinkedList;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
@RequestMapping("/download")
public class DownloadController {
	
	@GetMapping
	public void download(HttpServletRequest request,
						HttpServletResponse response,
						@RequestParam("folder") String folder) throws UnsupportedEncodingException {
		Path folderPath = Paths.get(folder);
		if (!Files.isDirectory(folderPath)) {
			// 文件夹不存在
			response.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return ;
		}
		
		// 二进制数据流
		response.setContentType("application/octet-stream");
		
		// 附件形式打开
		response.setHeader("Content-Disposition", "attachment; filename=" + new String((folderPath.getFileName().toString() +  ".zip").getBytes("GBK"),"ISO-8859-1"));
		
		try (ZipOutputStream zipOutputStream = new ZipOutputStream(response.getOutputStream())){
			LinkedList<String> path = new LinkedList<>();
			
			Files.walkFileTree(folderPath, new FileVisitor<Path>() {
	
				@Override
				public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
					// 开始遍历目录
					if (!dir.equals(folderPath)) {
						path.addLast(dir.getFileName().toString());
						// 写入目录 
						ZipEntry zipEntry = new ZipEntry(path.stream().collect(Collectors.joining("/", "", "/")));
						try {
							zipOutputStream.putNextEntry(zipEntry);
							zipOutputStream.flush();
						} catch (IOException e) {
							throw new RuntimeException(e);
						}
					}
					return FileVisitResult.CONTINUE;
				}
	
				@Override
				public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
					// 开始遍历文件
					try (InputStream inputStream = Files.newInputStream(file)) {
						
						// 创建一个压缩项,指定名称
						String fileName = path.size() > 0 
								? path.stream().collect(Collectors.joining("/", "", "")) + "/" + file.getFileName().toString()
								: file.getFileName().toString();
						
						ZipEntry zipEntry = new ZipEntry(fileName);
						// 添加到压缩流
						zipOutputStream.putNextEntry(zipEntry);
						// 写入数据
						int len = 0;
						// 10kb缓冲区
						byte[] buffer = new byte[1024 * 10];
						while ((len = inputStream.read(buffer)) > 0) {
							zipOutputStream.write(buffer, 0, len);
						}
						
						zipOutputStream.flush();
					} catch (IOException e) {
						throw new RuntimeException(e);
					}
					return FileVisitResult.CONTINUE;
				}
				@Override
				public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
					return FileVisitResult.CONTINUE;
				}
				@Override
				public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
					// 结束遍历目录
					if (!path.isEmpty()) {
						path.removeLast();
					}
					return FileVisitResult.CONTINUE;
				}
			});
			zipOutputStream.closeEntry();
		} catch (IOException e) {
			e.printStackTrace();
		} 
	}
}

演示

下载本地磁盘的 D:\\Nginx 目录,注意文件路径参数有特殊符号,需要进行uri编码。

http://localhost/download?folder=D%3a%5c%5cnginx-1.18.0

对比原始文件夹和下载后解压开来的文件夹,大小一致说明没问题。