Para permitir que usuários baixem arquivos compactados no formato ZIP diretamente de uma aplicação web, sem a necessidade de salvar o arquivo ZIP temporariamente no servidor, a seguinte abordagem pode ser implementada. Esta solução integra o frontend JavaScript com um backend Java.
Interface de Usuário (Frontend)
O cliente pode iniciar o download por meio de uma simples chamada HTTP para um endpoint no servidor. Isso pode ser feito usando JavaScript, por exemplo, ao clicar em um botão.
function iniciarDownloadZip() {
// Redireciona o navegador para o endpoint do backend que irá gerar e enviar o ZIP.
// O basePath deve ser configurado para apontar para a raiz da aplicação.
window.location.href = "${basePath}/api/arquivos/baixarZip";
}
Lógica do Servidor (Backend - Spring Controller)
No lado do servidor, um controlador Java processa a requisição, localiza o diretório ou arquivos a serem compactados e, em seguida, utiliza uma classe utilitária para gerar o arquivo ZIP e transmiti-lo diretamente ao cliente através da resposta HTTP.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
@Controller
@RequestMapping("/api/arquivos")
public class DownloadController {
private static final Logger LOG = LoggerFactory.getLogger(DownloadController.class);
@GetMapping("/baixarZip")
public void baixarArquivosCompactados(HttpServletRequest request, HttpServletResponse response) {
// Define o caminho base do diretório que contém os arquivos a serem compactados.
// Em uma aplicação real, este valor viria de uma propriedade de configuração.
String caminhoParaArquivos = "/caminho/para/seus/arquivos/de/teste"; // Exemplo: C:/temp/docs
File diretorioDeOrigem = new File(caminhoParaArquivos);
if (!diretorioDeOrigem.exists()) {
LOG.error("O diretório de origem especificado não foi encontrado: {}", caminhoParaArquivos);
try {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Diretório de origem não encontrado.");
} catch (IOException e) {
LOG.error("Erro ao enviar resposta de erro.", e);
}
return;
}
try {
// Invoca o utilitário para comprimir e enviar o arquivo ZIP.
ZipStreamerUtil.comprimirEEnviarZip(response, caminhoParaArquivos, "DocumentosCompactados");
} catch (IOException e) {
LOG.error("Ocorreu um erro ao gerar ou enviar o arquivo ZIP.", e);
try {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Falha ao gerar o arquivo ZIP.");
} catch (IOException ex) {
LOG.error("Erro ao enviar resposta de erro.", ex);
}
}
}
}
Classe Utilitária para Compressão e Envio (Java)
Esta classe manipula a criação do stream ZIP e configura os cabeçalhos HTTP necessários para o download. Ela utiliza um método recursivo para adicionar os arquivos e diretórios ao stream ZIP, manetndo a estrutura de pastas.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class ZipStreamerUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ZipStreamerUtil.class);
private static final int TAMANHO_BUFFER = 4096; // Tamanho do buffer para leitura/escrita de arquivos
/**
* Compacta os arquivos de um diretório e os envia diretamente para o cliente via HttpServletResponse.
* O arquivo ZIP não é salvo no servidor.
*
* @param response A resposta HTTP para escrever o conteúdo ZIP.
* @param caminhoFonte O caminho absoluto do diretório ou arquivo a ser compactado.
* @param nomeDownload O nome que o arquivo ZIP terá ao ser baixado pelo cliente (sem a extensão .zip).
* @throws IOException Se ocorrer um erro de I/O durante a compactação ou envio.
*/
public static void comprimirEEnviarZip(HttpServletResponse response, String caminhoFonte, String nomeDownload) throws IOException {
long inicioCompactacao = System.currentTimeMillis();
ZipOutputStream fluxoSaidaZip = null;
try {
// Configura os cabeçalhos da resposta HTTP para um download de arquivo ZIP.
response.reset();
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType("application/zip"); // Tipo MIME para arquivos ZIP
// Codifica o nome do arquivo para garantir compatibilidade com caracteres especiais e espaços.
String nomeArquivoZipFinal = URLEncoder.encode(nomeDownload + ".zip", StandardCharsets.UTF_8.name());
// Define o cabeçalho Content-Disposition para indicar que é um anexo para download.
response.setHeader("Content-Disposition", "attachment; filename=\"" + nomeArquivoZipFinal + "\"");
// Cria um ZipOutputStream diretamente a partir do OutputStream da resposta.
fluxoSaidaZip = new ZipOutputStream(new BufferedOutputStream(response.getOutputStream()));
File arquivoOuDiretorioOriginal = new File(caminhoFonte);
if (!arquivoOuDiretorioOriginal.exists()) {
throw new FileNotFoundException("O caminho de origem para compactação não existe: " + caminhoFonte);
}
// Inicia a adição recursiva de arquivos ao stream ZIP.
adicionarItemAoZip(arquivoOuDiretorioOriginal, fluxoSaidaZip, "");
long fimCompactacao = System.currentTimeMillis();
LOGGER.info("Compactação concluída com sucesso em {} ms.", (fimCompactacao - inicioCompactacao));
} finally {
if (fluxoSaidaZip != null) {
try {
fluxoSaidaZip.close(); // Garante que o stream ZIP seja fechado.
} catch (IOException e) {
LOGGER.error("Erro ao fechar o ZipOutputStream.", e);
}
}
}
}
/**
* Adiciona recursivamente arquivos e diretórios a um ZipOutputStream, mantendo a estrutura de pastas.
*
* @param itemAtual O arquivo ou diretório atual a ser processado.
* @param fluxoSaidaZip O ZipOutputStream para onde os dados serão escritos.
* @param caminhoRelativo O caminho relativo do item dentro do arquivo ZIP.
* @throws IOException Se ocorrer um erro de I/O.
*/
private static void adicionarItemAoZip(File itemAtual, ZipOutputStream fluxoSaidaZip, String caminhoRelativo) throws IOException {
byte[] buffer = new byte[TAMANHO_BUFFER];
if (itemAtual.isFile()) {
// Se for um arquivo, adiciona como uma entrada ao ZIP.
fluxoSaidaZip.putNextEntry(new ZipEntry(caminhoRelativo));
try (FileInputStream entradaArquivo = new FileInputStream(itemAtual)) {
int bytesLidos;
while ((bytesLidos = entradaArquivo.read(buffer)) != -1) {
fluxoSaidaZip.write(buffer, 0, bytesLidos);
}
}
fluxoSaidaZip.closeEntry();
} else if (itemAtual.isDirectory()) {
File[] conteudoDiretorio = itemAtual.listFiles();
// Se o diretório estiver vazio ou não contiver arquivos listáveis.
if (conteudoDiretorio == null || conteudoDiretorio.length == 0) {
// Adiciona uma entrada para diretório vazio para preservar a estrutura de pastas.
fluxoSaidaZip.putNextEntry(new ZipEntry(caminhoRelativo + "/"));
fluxoSaidaZip.closeEntry();
} else {
// Para cada item dentro do diretório, chama a função recursivamente.
for (File subItem : conteudoDiretorio) {
String novoCaminhoRelativo = (caminhoRelativo.isEmpty() ? "" : caminhoRelativo + "/") + subItem.getName();
adicionarItemAoZip(subItem, fluxoSaidaZip, novoCaminhoRelativo);
}
}
}
}
}