Geração e Download Direto de Arquivos ZIP em Aplicações Java Web

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);
                }
            }
        }
    }
}

Tags: java Spring Framework ZIP Download de Arquivos HttpServletResponse

Publicado em 6-16 21:07 por Thomas