Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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);
like image 666
Stealth Rabbi Avatar asked Aug 15 '17 13:08

Stealth Rabbi


People also ask

How do I download a file from a file in Spring MVC?

In Spring MVC application, to download a resource such as a file to the browser, you need to do the following in your controller. Use the void return type for your request-handling method and add HttpServletResponse as an argument to the method. Set the response’s content type to the file’s content type.

How to download large files using webclient?

The best way to download large files using WebClient it to download the file in chunks. To do that we need to use Flux publisher that can emit zero to N events.

What is the use of dodownload in spring?

Note that, unlike traditional Spring controller’s methods, the method doDownload () does not return a view name, because our purpose is to send a file to the client. The method exits as soon as the file is completely transferred to the client.

How to test file download application in Spring Boot?

I am using Postman tool to test the file download application. make sure your Spring Boot application is up and running. Look at the download URL and download path in the above image. So your file download URL could be anything from where you want to download the file.


1 Answers

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;
    }
}
like image 103
Ismail Durmaz Avatar answered Oct 13 '22 23:10

Ismail Durmaz