7

I am trying to download multiple file with one http get request in my spring-mvc application.

I have looked at other posts, saying you could just zip the file and send this file but it's not ideal in my case, as the file are not in direct access from the application. To get the files I have to query a REST interface, which streams the file from either hbase or hadoop.

I can have files bigger than 1 Go, so downloading the files into a repository, zipping them and sending them to the client would be too long. (Considering that the big file are already zip, zipping won't compress them).

I saw here and there that you can use multipart-response to download multiple files at once, but I can't get any result. Here is my code:

String boundaryTxt = "--AMZ90RFX875LKMFasdf09DDFF3";
response.setContentType("multipart/x-mixed-replace;boundary=" + boundaryTxt.substring(2));
ServletOutputStream out = response.getOutputStream();
        
// write the first boundary
out.write(("\r\n"+boundaryTxt+"\r\n").getBytes());

String contentType = "Content-type: application/octet-stream\n";
        
for (String s:files){
    System.out.println(s);
    String[] split = s.trim().split("/");
    db = split[1];
    key = split[2]+"/"+split[3]+"/"+split[4];
    filename = split[4];
            
    out.write((contentType + "\r\n").getBytes());
    out.write(("\r\nContent-Disposition: attachment; filename=" +filename+"\r\n").getBytes());
    
    InputStream is = null;
    if (db.equals("hadoop")){
        is = HadoopUtils.get(key);
    }
    else if (db.equals("hbase")){
        is = HbaseUtils.get(key);
    }
    else{
        System.out.println("Wrong db with name: " + db);
    }
    byte[] buffer = new byte[9000]; // max 8kB for http get
    int data;
    while((data = is.read(buffer)) != -1) { 
        out.write(buffer, 0, data);
    } 
    is.close(); 
       
    // write bndry after data
    out.write(("\r\n"+boundaryTxt+"\r\n").getBytes());
    response.flushBuffer();
    }
// write the ending boundary
out.write((boundaryTxt + "--\r\n").getBytes());
response.flushBuffer();
out.close();
}   

The weird part is that I get different result depending on the navigator. Nothing happends in Chrome (looked at the console) and in Firefox, I got a prompt asking to download for each file but it doesn't have the right type nor the right name (nothing in console either).

Is there any bug in my code? If no, is there any alternative?

Edit

I also saw this post: Unable to send a multipart/mixed request to spring MVC based REST service

Edit 2

firefox result

The content of this file is what I want, but why can't I get the right name and why can't chrome download anything?

Community
  • 1
  • 1
Whitefret
  • 1,057
  • 1
  • 10
  • 21
  • You can loop the download requests in JS, and after each request is finished a new download is triggered. I think this seems easier than hacking around to trigger multiple downloads. – We are Borg Apr 27 '16 at 09:56
  • @WeareBorg I only have a basic knowledge in js, can you give me some ressources to do it? – Whitefret Apr 27 '16 at 09:57
  • Unfortunately no, I am a backend developer, don't even know JS. I can give you code for ZIP download if you want in Java, that I have... – We are Borg Apr 27 '16 at 09:58
  • @WeareBorg just one question about that. does the zip step involve in my case, waiting for the full download from the rest interface to the web app and then forwading the zip to the client? – Whitefret Apr 27 '16 at 10:01
  • @WeareBorg by ressources, links are enough, I am not against learning new things – Whitefret Apr 27 '16 at 10:03
  • The line terminator in HTTP is defined as `\r\n`, not `\n`. – user207421 Apr 27 '16 at 10:09
  • @EJP still no result. I will edit my post with the code and what I get in Firefox (chrome is sending nothing) – Whitefret Apr 27 '16 at 10:15
  • Your `contentType` variable contains `\n`, and you are now sending it followed by another `\r\n`. – user207421 Apr 27 '16 at 10:22
  • I don't understand what you mean by restinterface to webapp. The zip is created on server's filesystem and then forwarded to the client.. – We are Borg Apr 27 '16 at 10:44
  • I have added the server side code for zip download. If that's something you decide later you don't want, I will remove the answer. – We are Borg Apr 27 '16 at 10:50
  • @WeareBorg the files in question are in a hadoop/hbase cluster. The rest interface is here to communicate with the cluster. Here, the java code communicate by the rest interface. So my question is: does the server needs to download the files first on it's filesystem? Thanks for the code, will upvote it for the moment – Whitefret Apr 27 '16 at 11:23
  • I don't know much about Hadoop, but as long as you can convert it to byte-array, you should be fine. You will ofcourse have to modify the code which I gave a bit, but it should work. Just added an extra method in update to write byte-array to a zip file. – We are Borg Apr 27 '16 at 11:24
  • @WeareBorg I can read the file from hbase or hadoop as inputstream so that is no problem. Here I worry about the latency, because having to donwload first on server then on client will take time. I mostly want to download 1Go or more files – Whitefret Apr 27 '16 at 11:28
  • @EJP Code works for firefox, thank you. Still not working for Chrome but now I know it works, the fix may be somewhere else – Whitefret Apr 27 '16 at 11:32
  • I don't see how you are going to achieve that. You need some sort of buffer where the files go, either RAM or your FS(Filesystem). You cannot just move files from one location to other without somewhere first having them, physics stopping u. Also, having it in FS is cheaper as the content is not loaded in RAM, which is not only expensive, but size in RAM increases almost exponentially with file-size. – We are Borg Apr 27 '16 at 11:33
  • Presuming your webapp-server and your hadoop cluster are different physical machines, you can run a tiny file-server on your Hadoop cluster(if that's allowed), and forward the download request to the tiny file-server and serve the file from there. – We are Borg Apr 27 '16 at 11:35
  • @WeareBorg yeah I know (most) of that, I just wondered if there was a way to compress by small chunks then send them to client. That's the reason I didn't go for zip in the first place. Thank you for your answers. – Whitefret Apr 27 '16 at 11:36
  • @WeareBorg There might be a way to make zip with hdfs, the thing is I also have files in Hbase – Whitefret Apr 27 '16 at 11:37
  • If memory is a problem on client side for accepting huge ZIP files, you can always chop the ZIP into multiple parts. But the best way to do all this file download is async, so atleast the client wont notice. Secondly, I don't see you have any escape other than to download. Good luck.. :-) – We are Borg Apr 27 '16 at 11:39
  • 1
    @WeareBorg The think I am doing at the moment is plain streaming so I don't keep data on the server (except from the cache). But I couldn't make multipart work. Now it is solved – Whitefret Apr 27 '16 at 11:41

2 Answers2

6

This is the way you can do the download via zip :

try {
      List<GroupAttachments> groupAttachmentsList = attachIdList.stream().map(this::getAttachmentObjectOnlyById).collect(Collectors.toList()); // Get list of Attachment objects
            Person person = this.personService.getCurrentlyAuthenticatedUser();
            String zipSavedAt = zipLocation + String.valueOf(new BigInteger(130, random).toString(32)); // File saved location
            byte[] buffer = new byte[1024];
            FileOutputStream fos = new FileOutputStream(zipSavedAt);
            ZipOutputStream zos = new ZipOutputStream(fos);

                GroupAttachments attachments = getAttachmentObjectOnlyById(attachIdList.get(0));

                    for (GroupAttachments groupAttachments : groupAttachmentsList) {
                            Path path = Paths.get(msg + groupAttachments.getGroupId() + "/" +
                                    groupAttachments.getFileIdentifier());   // Get the file from server from given path
                            File file = path.toFile();
                            FileInputStream fis = new FileInputStream(file);
                            zos.putNextEntry(new ZipEntry(groupAttachments.getFileName()));
                            int length;

                            while ((length = fis.read(buffer)) > 0) {
                                zos.write(buffer, 0, length);
                            }
                            zos.closeEntry();
                            fis.close();

                    zos.close();
                    return zipSavedAt;
            }
        } catch (Exception ignored) {
        }
        return null;
    }

Controller method for downloading zip :

 @RequestMapping(value = "/URL/{mode}/{token}")
    public void downloadZip(HttpServletResponse response, @PathVariable("token") String token,
                            @PathVariable("mode") boolean mode) {
        response.setContentType("application/octet-stream");
        try {
            Person person = this.personService.getCurrentlyAuthenticatedUser();
            List<Integer> integerList = new ArrayList<>();
            String[] integerArray = token.split(":");
            for (String value : integerArray) {
                integerList.add(Integer.valueOf(value));
            }
            if (!mode) {
                String zipPath = this.groupAttachmentsService.downloadAttachmentsAsZip(integerList);
                File file = new File(zipPath);
                response.setHeader("Content-Length", String.valueOf(file.length()));
                response.setHeader("Content-Disposition", "attachment; filename=\"" + person.getFirstName() + ".zip" + "\"");
                InputStream is = new FileInputStream(file);
                FileCopyUtils.copy(IOUtils.toByteArray(is), response.getOutputStream());
                response.flushBuffer();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Have fun, incase of doubt, lemme know.

Update

Byte-array in a ZIP file. You can use this code in a loop as in the first method I gave :

public static byte[] zipBytes(String filename, byte[] input) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ZipOutputStream zos = new ZipOutputStream(baos);
    ZipEntry entry = new ZipEntry(filename);
    entry.setSize(input.length);
    zos.putNextEntry(entry);
    zos.write(input);
    zos.closeEntry();
    zos.close();
    return baos.toByteArray();
}
We are Borg
  • 5,117
  • 17
  • 102
  • 225
4

You can do it using the multipart/x-mixed-replace content type. You can add this like response.setContentType("multipart/x-mixed-replace;boundary=END"); and loop through the files and write each to the response output stream. You can check out this example for reference.

Another approach is to create a REST end point that will let you download one single file and then repeatedly call this end point for each file individually.

Kuncheria
  • 1,182
  • 9
  • 16