Spring REST - create ZIP file and send it to the client
Asked Answered
B

5

57

I want to create a ZIP file that contains my archived files that I received from the backend, and then send this file to a user. For 2 days I have been looking for the answer and can't find proper solution, maybe you can help me :)

For now, the code is like this (I know I shouldn't do it all in the Spring controller, but don't care about that, it is just for testing purposes, to find the way to make it works):

    @RequestMapping(value = "/zip")
    public byte[] zipFiles(HttpServletResponse response) throws IOException {
        // Setting HTTP headers
        response.setContentType("application/zip");
        response.setStatus(HttpServletResponse.SC_OK);
        response.addHeader("Content-Disposition", "attachment; filename=\"test.zip\"");

        // Creating byteArray stream, make it bufferable and passing this buffer to ZipOutputStream
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(byteArrayOutputStream);
        ZipOutputStream zipOutputStream = new ZipOutputStream(bufferedOutputStream);

        // Simple file list, just for tests
        ArrayList<File> files = new ArrayList<>(2);
        files.add(new File("README.md"));

        // Packing files
        for (File file : files) {
            // New zip entry and copying InputStream with file to ZipOutputStream, after all closing streams
            zipOutputStream.putNextEntry(new ZipEntry(file.getName()));
            FileInputStream fileInputStream = new FileInputStream(file);

            IOUtils.copy(fileInputStream, zipOutputStream);

            fileInputStream.close();
            zipOutputStream.closeEntry();
        }

        if (zipOutputStream != null) {
            zipOutputStream.finish();
            zipOutputStream.flush();
            IOUtils.closeQuietly(zipOutputStream);
        }
        IOUtils.closeQuietly(bufferedOutputStream);
        IOUtils.closeQuietly(byteArrayOutputStream);

        return byteArrayOutputStream.toByteArray();
    }

But the problem is, that using the code, when I enter the URL localhost:8080/zip, I get a file test.zip.html instead of .zip file.

When I remove .html extension and leave just test.zip it opens correctly. So my questions are:

  • How to avoid returning this .html extension?
  • Why is it added?

I have no idea what else can I do. I was also trying replace ByteArrayOuputStream with something like:

OutputStream outputStream = response.getOutputStream();

and set the method to be void so it returns nothing, but It created .zip file which was damaged?

On my MacBook after unpacking the test.zip I was getting test.zip.cpgz which was again giving me test.zip file and so on.

On Windows the .zip file was damaged as I said and couldn't even open it.

I also suppose, that removing .html extension automatically will be the best option, but how?

Hope it is no as hard as It seems to be :)
Thanks

Backbite answered 14/1, 2015 at 21:40 Comment(1)
you might find the answer here helpful.. https://mcmap.net/q/412268/-how-to-return-a-zip-file-stream-using-java-springbootMedium
B
45

The problem is solved.

I replaced:

response.setContentType("application/zip");

with:

@RequestMapping(value = "/zip", produces="application/zip")

And now I get a clear, beautiful .zip file.


If any of you have either better or faster proposition, or just want to give some advice, then go ahead, I am curious.

Backbite answered 14/1, 2015 at 22:36 Comment(2)
+1 For non-trivial question and answering your own question! The only question / concern I have with this implementation is the final return statement. I think this would load the entire file into memory because of toByteArray(). Right? I wonder if there is a way to stream it to avoid some OutOfMemoryException errors if the directory is too large.Mandibular
see this page if you need generic solution for any case: https://mcmap.net/q/410843/-spring-rest-create-zip-file-and-send-it-to-the-clientMarcin
C
43
@RequestMapping(value="/zip", produces="application/zip")
public void zipFiles(HttpServletResponse response) throws IOException {

    //setting headers  
    response.setStatus(HttpServletResponse.SC_OK);
    response.addHeader("Content-Disposition", "attachment; filename=\"test.zip\"");

    ZipOutputStream zipOutputStream = new ZipOutputStream(response.getOutputStream());

    // create a list to add files to be zipped
    ArrayList<File> files = new ArrayList<>(2);
    files.add(new File("README.md"));

    // package files
    for (File file : files) {
        //new zip entry and copying inputstream with file to zipOutputStream, after all closing streams
        zipOutputStream.putNextEntry(new ZipEntry(file.getName()));
        FileInputStream fileInputStream = new FileInputStream(file);

        IOUtils.copy(fileInputStream, zipOutputStream);

        fileInputStream.close();
        zipOutputStream.closeEntry();
    }    

    zipOutputStream.close();
}
Capitol answered 9/11, 2016 at 0:12 Comment(6)
because this solution streams data directly to response output stream.Extenuatory
This is java server side codes, how to download it in client by javascript??Used
@HarveyDent - yous should ask that as a new question.Capitol
Please, I was stuck into this problem for two whole days. thanks in advanceUsed
you just call the url. in the code above it would be http://<host>/zipCapitol
see this page if you need generic solution for any case: https://mcmap.net/q/410843/-spring-rest-create-zip-file-and-send-it-to-the-clientMarcin
T
21
@RequestMapping(value="/zip", produces="application/zip")
public ResponseEntity<StreamingResponseBody> zipFiles() {
    return ResponseEntity
            .ok()
            .header("Content-Disposition", "attachment; filename=\"test.zip\"")
            .body(out -> {
                var zipOutputStream = new ZipOutputStream(out);

                // create a list to add files to be zipped
                ArrayList<File> files = new ArrayList<>(2);
                files.add(new File("README.md"));

                // package files
                for (File file : files) {
                    //new zip entry and copying inputstream with file to zipOutputStream, after all closing streams
                    zipOutputStream.putNextEntry(new ZipEntry(file.getName()));
                    FileInputStream fileInputStream = new FileInputStream(file);

                    IOUtils.copy(fileInputStream, zipOutputStream);

                    fileInputStream.close();
                    zipOutputStream.closeEntry();
                }

                zipOutputStream.close();
            });
}
Tamtama answered 15/11, 2019 at 21:24 Comment(4)
+1 for helping me figure out how to do this without creating a HttpServletResponse, since this doesn't seem to set a status code on 404s.Laverne
If there is a run time exception anywhere in the body block, will still return 200Hughes
Attaching a relevant post about trouble handling exception with StreamingResponseBody https://mcmap.net/q/412269/-exception-handling-in-streamingresponsebody-not-working/5777189Hughes
see this page if you need generic solution for any case: https://mcmap.net/q/410843/-spring-rest-create-zip-file-and-send-it-to-the-clientMarcin
P
4

I am using REST Web Service of Spring Boot and I have designed the endpoints to always return ResponseEntity whether it is JSON or PDF or ZIP and I came up with the following solution which is partially inspired by denov's answer in this question as well as another question where I learned how to convert ZipOutputStream into byte[] in order to feed it to ResponseEntity as output of the endpoint.

Anyway, I created a simple utility class with two methods for pdf and zip file download

@Component
public class FileUtil {
    public BinaryOutputWrapper prepDownloadAsPDF(String filename) throws IOException {
        Path fileLocation = Paths.get(filename);
        byte[] data = Files.readAllBytes(fileLocation);

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.parseMediaType("application/pdf"));
        String outputFilename = "output.pdf";
        headers.setContentDispositionFormData(outputFilename, outputFilename);
        headers.setCacheControl("must-revalidate, post-check=0, pre-check=0");

        return new BinaryOutputWrapper(data, headers); 
    }

    public BinaryOutputWrapper prepDownloadAsZIP(List<String> filenames) throws IOException {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.parseMediaType("application/zip"));
        String outputFilename = "output.zip";
        headers.setContentDispositionFormData(outputFilename, outputFilename);
        headers.setCacheControl("must-revalidate, post-check=0, pre-check=0");

        ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
        ZipOutputStream zipOutputStream = new ZipOutputStream(byteOutputStream);

        for(String filename: filenames) {
            File file = new File(filename); 
            zipOutputStream.putNextEntry(new ZipEntry(filename));           
            FileInputStream fileInputStream = new FileInputStream(file);
            IOUtils.copy(fileInputStream, zipOutputStream);
            fileInputStream.close();
            zipOutputStream.closeEntry();
        }           
        zipOutputStream.close();
        return new BinaryOutputWrapper(byteOutputStream.toByteArray(), headers); 
    }
}

And now the endpoint can easily return ResponseEntity<?> as shown below using the byte[] data and custom headers that is specifically tailored for pdf or zip.

@GetMapping("/somepath/pdf")
public ResponseEntity<?> generatePDF() {
    BinaryOutputWrapper output = new BinaryOutputWrapper(); 
    try {
        String inputFile = "sample.pdf"; 
        output = fileUtil.prepDownloadAsPDF(inputFile);
        //or invoke prepDownloadAsZIP(...) with a list of filenames
    } catch (IOException e) {
        e.printStackTrace();
        //Do something when exception is thrown
    } 
    return new ResponseEntity<>(output.getData(), output.getHeaders(), HttpStatus.OK); 
}

The BinaryOutputWrapper is a simple immutable POJO class I created with private byte[] data; and org.springframework.http.HttpHeaders headers; as fields in order to return both data and headers from utility method.

Pneumothorax answered 5/6, 2018 at 3:35 Comment(1)
ByteArrayOutputStream stores the bytes in memory. So for large files you can run out of heap space very quickly.Karleen
M
0

Only thing that worked for Spring Boot application (without any hardcoded file paths!)

@GetMapping(value = "/zip-download", produces="application/zip")
public void zipDownload(@RequestParam List<String> name, HttpServletResponse response) throws IOException {

    response.setStatus(HttpServletResponse.SC_OK);
    response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + zipFileName + "\"");

    ZipOutputStream zipOut = new ZipOutputStream(response.getOutputStream());

    for (String fileName : name) {

        // resource content length
        int contentLength = 123;
        // resource input stream
        InputStream stream = InputStream.nullInputStream();

        ZipEntry zipEntry = new ZipEntry(fileName);
        zipEntry.setSize(contentLength);

        zipOut.putNextEntry(zipEntry);
        StreamUtils.copy(stream, zipOut);

        zipOut.closeEntry();
    }

    zipOut.finish();
    zipOut.close();
}

Marcin answered 28/2, 2023 at 17:12 Comment(3)
Unfortunately, it didn't work for me. The client gave the error invalid zip file.Scarcely
@Scarcely probably because it was empty, that's common thingMarcin
Yeah that was the issue. Sorry about the down vote. I cannot change it now due to Stackoverflow restrictions. ThanksScarcely

© 2022 - 2024 — McMap. All rights reserved.