Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to return a zip file stream using Java Springboot

I use Springboot, I want to generate zip file and then return to frontend.

@PostMapping(value="/export", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<ZipOutputStream> export() {
    // customService.generateZipStream() is a service method that can 
    //generate zip file using ZipOutputStream and then return this stream
    ZipOutputStream zipOut = customService.generateZipStream();
    return ResponseEntity
                  .ok()
                  .header("Content-Disposition", "attachment;filename=export.zip")
                  .header("Content-Type","application/octet-stream")
                  .body(zipOut)
}

The zip file can be generated correctly(in local dir) but I got below error when return stream to frontend:

spring.HttpMediaTypeNotAcceptableException: Could not find acceptable representation

Then i checked in google and changed return type to ResponseEntity<StreamResponseBody>, but how should I change ZipOutputStream to StreamResponseBody in method body(...), the solution in google is create zip output stream within body() method like that:

   // pseudocode
   .body(out -> { 
                   ZipOutputStream zipOut = new ZipOutputStream(out));
                   zipOut.putEntry(...);
                   zipOut.write(...);
                   zipOut.closeEntry();
                   ... balabala
                }

My question is how to use StreamResponseBody in this scenario or any alternative solution to return a zip stream that might a little large.

like image 905
Rollsbean Avatar asked Apr 23 '26 21:04

Rollsbean


2 Answers

If you are using Spring 3, you can use a lot of the swagger annotation interfaces to help cleanly build your ResponseEntity while using the StreamingResponseBody to properly prototype the format to be expected by spring.

The body code here is a shortened way of mapping the ZipOutputStream stream into StreamingResponseBody type that the controller expects to return (.body(out -> {...}) does this in my code below).

The [controller] code would look like this:

    @GetMapping(value = "/myZip")
    @Operation(
        summary = "Retrieves a ZIP file from the system given a proper request ID.",
        responses = {
            @ApiResponse(
                description = "Get ZIP file containing data for the ID.",
                responseCode = "200",
                content = @Content(schema = @Schema(implementation = StreamingResponseBody.class))),
            @ApiResponse(
                description = "Unauthenticated",
                responseCode = "401",
                content = @Content(schema = @Schema(implementation = ApiErrorResponse.class))),
            @ApiResponse(
                description = "Forbidden. Access Denied.",
                responseCode = "403",
                content = @Content(schema = @Schema(implementation = ApiErrorResponse.class)))
        })
    public ResponseEntity<StreamingResponseBody> myZipBuilder(@RequestParam String id, HttpServletResponse response)
        throws IOException {
        final String fileName = "MyRequest_" + id + "_" + new SimpleDateFormat("MMddyyyy").format(new Date());

        return ResponseEntity.ok()
            .header(CONTENT_DISPOSITION,"attachment;filename=\"" + fileName + ".zip\"")
            .contentType(MediaType.valueOf("application/zip"))
            .body(out -> myZipService.build(id, response.getOutputStream()));
    }

The code for your service build method simply needs to take in the whatever parms you need for the data, plus your ServletOutputStream responseOutputStream parm to allow you to build your ZipOutputStream object seeded by that stream.

In my little example below, you can see I build some CSV data in the buildDataLists method (not shown), which is just a List of List<String[]>.. I then take each of the top level List items and push them into the ZipOutputStream object using my streamWriteCsvToZip. The point is, you build your ZIP stream seeded with the responseOutputStream you were given from the controller. Once you have built your zip fully, make sure to close it up (in my case zos.close()). Then return the zos object to the controller.

    /**
     * Get ZIP file containing datafiles for a given request id
     *
     * @param id of the request
     * @param responseOutputStream for streaming the zip results
     * @return ZipOutputStream a ZIP file stream for the contents
     * @throws AccessDeniedException    if user does not have access to this function
     * @throws UnauthenticatedException if user is not authenticated
     */
    public ZipOutputStream build(String id, ServletOutputStream responseOutputStream) throws IOException {

        try {
            List<List<String[]>> csvFilesContents = buildDataLists(id);

            final ZipOutputStream zos = new ZipOutputStream(responseOutputStream);
            streamWriteCsvToZip("control", id, zos, csvFilesContents.remove(0));
            streamWriteCsvToZip("roles", id, zos, csvFilesContents.remove(0));
            streamWriteCsvToZip("accounts", id, zos, csvFilesContents.remove(0));

            zos.close(); // finally closing the ZipOutputStream to mark completion of ZIP file
            return zos;
        } catch (IOException | ClientException ex) {
            throw ex;
        }
    }

No magic here. Just take your data and put it into the zip stream. In my case, I was pulling list/array data, dropping it into a CSV, then putting that CSV in to the zip (as an entry by using zos.putNextEntry(entry);). Both the CSVs and the ZIP are kept as streams so that nothing is written to the filesystem during this operation, and the final result can be just streamed out by the controller. Make sure to close out the entry each time you write one to the zip output stream (zos.closeEntry()).


    private void streamWriteCsvToZip(String csvName, String id, ZipOutputStream zos, List<String[]> csvFileContents)
        throws IOException {
        String filename = id + "_" + csvName + ".csv";
        ZipEntry entry = new ZipEntry(filename); // create a zip entry and add it to ZipOutputStream
        zos.putNextEntry(entry);

        CSVWriter csvWriter = new CSVWriter(new OutputStreamWriter(zos));  // Directly write bytes to the output stream
        csvWriter.writeAll(csvFileContents);  // write the contents
        csvWriter.flush(); // flush the writer
        zos.closeEntry(); // close the entry. Note: not closing the zos just yet as we need to add more files to our ZIP
    }
like image 192
Kim Gentes Avatar answered Apr 25 '26 10:04

Kim Gentes


Thanks for everyone's help, I checked yours answers and solved this issue by updating export logic.

My solution:

Re-define method as customService.generateZipStream(ZipOutputStream zipOut)so that I can create a zip stream with StreamResponseBody in controller layer and then send it to service layer, in service layer, i will do export.

presudo code as below:

@PostMapping(value="/export", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity< StreamResponseBody > export() {
    // customService.generateZipStream() is a service method that can 
    //generate zip file using ZipOutputStream and then return this stream
    
    return ResponseEntity
                  .ok()
                  .header("Content-Disposition", "attachment;filename=export.zip")
                  .body(outputStream -> {
                     // Use inner implement and set StreamResponseBody to ZipOutputStream
                     try(ZipOutputStream zipOut = new ZipOutputStream(outputStream)) {
                         customService.generateZipStream(zipOut);
                     }
                  });
}

customService presudo code:

public void generateZipStream(ZipOutputStream zipOut) {
    // ... do export here
    zipOut.putEntry(...);
    zipOut.write(...);
    zipOut.closeEntry();
    // ... balabala

}

Hope it can help you if you have similar question.

like image 21
Rollsbean Avatar answered Apr 25 '26 10:04

Rollsbean