Stealth Rabbi
Stealth Rabbi

Reputation: 10346

Downloading large files via Spring MVC

I have a rest method for downloading files which works. But, it seems that the download doesn't start on the web client until the file is completely copied to the output stream, which can take a while for large files.

@GetMapping(value = "download-single-report")
public void downloadSingleReport(HttpServletResponse response) {

    File dlFile = new File("some_path");

    try {
        response.setContentType("application/pdf");
        response.setHeader("Content-disposition", "attachment; filename="+ dlFile.getName());
        InputStream inputStream = new FileInputStream(dlFile);
        IOUtils.copy(inputStream, response.getOutputStream());
        response.flushBuffer();
    } catch (FileNotFoundException e) {
        // error
    } catch (IOException e) {
        // error
    }
}

Is there a way to "stream" the file such that the download starts as soon as I begin writing to the output stream?

I also have a similar method that takes multiple files and puts them in a zip, adding each zip entry to the zip stream, and the download also only begins after the zip has been created:

        ZipEntry zipEntry = new ZipEntry(entryName);
        zipOutStream.putNextEntry(zipEntry);
        IOUtils.copy(fileStream, zipOutStream);

Upvotes: 7

Views: 5257

Answers (3)

Manish Salian
Manish Salian

Reputation: 516

You can use "StreamingResponseBody" File download would start immediately while the chunks are written to the output stream. Below is the code snippet

@GetMapping (value = "/download-single-report")
public ResponseEntity<StreamingResponseBody> downloadSingleReport(final HttpServletResponse response) {
    final File dlFile = new File("Sample.pdf");
    response.setContentType("application/pdf");
    response.setHeader(
            "Content-Disposition",
            "attachment;filename="+ dlFile.getName());

    StreamingResponseBody stream = out -> FileCopyUtils.copy(new FileInputStream(dlFile), out);

    return new ResponseEntity(stream, HttpStatus.OK);
}

Upvotes: 1

Ismail Durmaz
Ismail Durmaz

Reputation: 2631

You can use InputStreamResource to return stream result. I tested and it is started copying to output immediately.

    @GetMapping(value = "download-single-report")
    public ResponseEntity<Resource> downloadSingleReport() {
        File dlFile = new File("some_path");
        if (!dlFile.exists()) {
            return ResponseEntity.notFound().build();
        }

        try {
            try (InputStream stream = new FileInputStream(dlFile)) {
                InputStreamResource streamResource = new InputStreamResource(stream);
                return ResponseEntity.ok()
                        .contentType(MediaType.APPLICATION_PDF)
                        .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + dlFile.getName() + "\"")
                        .body(streamResource);
            }

            /*
            // FileSystemResource alternative
            
            FileSystemResource fileSystemResource = new FileSystemResource(dlFile);
            return ResponseEntity.ok()
                    .contentType(MediaType.APPLICATION_PDF)
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + dlFile.getName() + "\"")
                    .body(fileSystemResource);
           */ 
        } catch (IOException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

The second alternative is a partial download method.

    @GetMapping(value = "download-single-report-partial")
    public void downloadSingleReportPartial(HttpServletRequest request, HttpServletResponse response) {
        File dlFile = new File("some_path");
        if (!dlFile.exists()) {
            response.setStatus(HttpStatus.NOT_FOUND.value());
            return;
        }
        try {
            writeRangeResource(request, response, dlFile);
        } catch (Exception ex) {
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        }
    }

    public static void writeRangeResource(HttpServletRequest request, HttpServletResponse response, File file) throws IOException {
        String range = request.getHeader("Range");
        if (StringUtils.hasLength(range)) {
            //http
            ResourceRegion region = getResourceRegion(file, range);
            long start = region.getPosition();
            long end = start + region.getCount() - 1;
            long resourceLength = region.getResource().contentLength();
            end = Math.min(end, resourceLength - 1);
            long rangeLength = end - start + 1;

            response.setStatus(206);
            response.addHeader("Accept-Ranges", "bytes");
            response.addHeader("Content-Range", String.format("bytes %s-%s/%s", start, end, resourceLength));
            response.setContentLengthLong(rangeLength);
            try (OutputStream outputStream = response.getOutputStream()) {
                try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
                    StreamUtils.copyRange(inputStream, outputStream, start, end);
                }
            }
        } else {
            response.setStatus(200);
            response.addHeader("Accept-Ranges", "bytes");
            response.setContentLengthLong(file.length());
            try (OutputStream outputStream = response.getOutputStream()) {
                try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
                    StreamUtils.copy(inputStream, outputStream);
                }
            }
        }
    }

    private static ResourceRegion getResourceRegion(File file, String range) {
        List<HttpRange> httpRanges = HttpRange.parseRanges(range);
        if (httpRanges.isEmpty()) {
            return new ResourceRegion(new FileSystemResource(file), 0, file.length());
        }
        return httpRanges.get(0).toResourceRegion(new FileSystemResource(file));
    }

Spring Framework Resource Response Process

Resource response managed by ResourceHttpMessageConverter class. In writeContent method, StreamUtils.copy is called.

package org.springframework.http.converter;

public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<Resource> {
..
    protected void writeContent(Resource resource, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
        try {
            InputStream in = resource.getInputStream();
            try {
                StreamUtils.copy(in, outputMessage.getBody());
            }
            catch (NullPointerException ex) {
                // ignore, see SPR-13620
            }
            finally {
                try {
                    in.close();
                }
                catch (Throwable ex) {
                    // ignore, see SPR-12999
                }
            }
        }
        catch (FileNotFoundException ex) {
            // ignore, see SPR-12999
        }
    }
}

out.write(buffer, 0, bytesRead); sends data immediately to output (I have tested on my local machine). When whole data is transferred, out.flush(); is called.

package org.springframework.util;

public abstract class StreamUtils {
..
    public static int copy(InputStream in, OutputStream out) throws IOException {
        Assert.notNull(in, "No InputStream specified");
        Assert.notNull(out, "No OutputStream specified");
        int byteCount = 0;

        int bytesRead;
        for(byte[] buffer = new byte[4096]; (bytesRead = in.read(buffer)) != -1; byteCount += bytesRead) {
            out.write(buffer, 0, bytesRead);
        }

        out.flush();
        return byteCount;
    }
}

Upvotes: 3

StanislavL
StanislavL

Reputation: 57421

Use

IOUtils.copyLarge(InputStream input, OutputStream output)

Copy bytes from a large (over 2GB) InputStream to an OutputStream. This method buffers the input internally, so there is no need to use a BufferedInputStream.

The buffer size is given by DEFAULT_BUFFER_SIZE.

or

IOUtils.copyLarge(InputStream input, OutputStream output, byte[] buffer)

Copy bytes from a large (over 2GB) InputStream to an OutputStream. This method uses the provided buffer, so there is no need to use a BufferedInputStream.

http://commons.apache.org/proper/commons-io/javadocs/api-2.4/org/apache/commons/io/IOUtils.html

Upvotes: 1

Related Questions