How to Return a Zip File to the Browser Via the Response Outputstream

How do I return a zip file to the browser via the response OutputStream?

You need to set the Content-Type response header to the value application/zip (or application/octet-stream, depending on the target browser). Additionally, you may want to send additional response headers indicating attachment status and filename.

Return a zip (or any file) from the server on the client browser (REST)

A possible approach to successfully download your zip file can be the described in the following paragraphs.

First, consider returning a reference to the zip file obtained as the compression result in your downloadZip method:

public File getDownloadZip(String[] files, String folderName) throws IOException {
[...] // The method is huge but basically I generate a folder called "Download/" in the server

// Zipping the "Download/" folder
File selectedFilesZipFile = new File("selected-files.zip")
ZipUtil.pack(new File("Download"), selectedFilesZipFile);

// return the zipped file obtained as result of the previous operation
return selectedFilesZipFile;
}

Now, modify your HttpHandler to perform the download:

server.createContext("/files/downloadZip", new HttpHandler() {

@Override
public void handle(HttpExchange exchange) throws IOException {
if (!handleTokenPreflight(exchange)) { return; }
System.out.println(exchange.getRequestURI());
Map<String, String> queryParam = parseQueryParam(exchange.getRequestURI().getQuery());

String authToken = exchange.getRequestHeaders().getFirst("token");
String target = queryParam.get("target") + ",";
String[] files = new String[Integer.parseInt(queryParam.get("numberOfFiles"))];

[...] // I process the data in this entire method and send it to the previous method that creates a zip

// Get a reference to the zipped file
File selectedFilesZipFile = Controller.getDownloadZip(files, folderName);

// Set the appropiate Content-Type
exchange.getResponseHeaders().set("Content-Type", "application/zip");

// Optionally, if the file is downloaded in an anchor, set the appropiate content disposition
// exchange.getResponseHeaders().add("Content-Disposition", "attachment; filename=selected-files.zip");

// Download the file. I used java.nio.Files to copy the file contents, but please, feel free
// to use other option like java.io or the Commons-IO library, for instance
exchange.sendResponseHeaders(200, selectedFilesZipFile.length());
try (OutputStream responseBody = httpExchange.getResponseBody()) {
Files.copy(selectedFilesZipFile.toPath(), responseBody);
responseBody.flush();
}
}
});

Now the problem is how to deal with the download in Angular.

As suggested in the previous code, if the resource is public or you have a way to manage your security token, including it as a parameter in the URL, for instance, one possible solution is to not use Angular HttpClient but an anchor with an href that points to your ever backend handler method directly.

If you need to use Angular HttpClient, perhaps to include your auth tokens, then you can try the approach proposed in this great SO question.

First, in your handler, encode to Base64 the zipped file contents to simplify the task of byte handling (in a general use case, you can typically return from your server a JSON object with the file content and metadata describing that content, like content-type, etcetera):

server.createContext("/files/downloadZip", new HttpHandler() {

@Override
public void handle(HttpExchange exchange) throws IOException {
if (!handleTokenPreflight(exchange)) { return; }
System.out.println(exchange.getRequestURI());
Map<String, String> queryParam = parseQueryParam(exchange.getRequestURI().getQuery());

String authToken = exchange.getRequestHeaders().getFirst("token");
String target = queryParam.get("target") + ",";
String[] files = new String[Integer.parseInt(queryParam.get("numberOfFiles"))];

[...] // I process the data in this entire method and send it to the previous method that creates a zip

// Get a reference to the zipped file
File selectedFilesZipFile = Controller.getDownloadZip(files, folderName);

// Set the appropiate Content-Type
exchange.getResponseHeaders().set("Content-Type", "application/zip");

// Download the file
byte[] fileContent = Files.readAllBytes(selectedFilesZipFile.toPath());
byte[] base64Data = Base64.getEncoder().encode(fileContent);
exchange.sendResponseHeaders(200, base64Data.length);
try (OutputStream responseBody = httpExchange.getResponseBody()) {
// Here I am using Commons-IO IOUtils: again, please, feel free to use other alternatives for writing
// the base64 data to the response outputstream
IOUtils.write(base64Data, responseBody);
responseBody.flush();
}
}
});

After that, use the following code in you client side Angular component to perform the download:

this.downloadService.httpGetDownloadZip(['target1','target2']).pipe(
tap((b64Data) => {
const blob = this.b64toBlob(b64Data, 'application/zip');
const blobUrl = URL.createObjectURL(blob);
window.open(blobUrl);
})
).subscribe()

As indicated in the aforementioned question, b64toBlob will look like this:

private b64toBlob(b64Data: string, contentType = '', sliceSize = 512) {
const byteCharacters = atob(b64Data);
const byteArrays = [];

for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);

const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}

const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}

const blob = new Blob(byteArrays, {type: contentType});
return blob;
}

Probably you will need to slightly modify the httpGetDownloadZip method in your service to take into account the returned base 64 data - basically, change ServerAnswer to string as the returned information type:

httpGetDownloadZip(target: string[]): Observable<string> {
const params = new HttpParams().set('target', String(target)).set('numberOfFiles', String(target.length));
const headers = new HttpHeaders().set('token', this.tokenService.getStorageToken());
const options = {
headers,
params,
};
return this.http
.get<string>(this.BASE_URL + '/files/downloadZip', options)
.pipe(catchError(this.handleError<ServerAnswer>('httpGetZip')));
}

Returning ZipOutputStream to browser

Just had to do this exact same thing yesterday.

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
ZipOutputStream zip = new ZipOutputStream(baos);

.... populate ZipOutputStream

String filename = "out.zip";
// the response variable is just a standard HttpServletResponse
response.setHeader("Content-Disposition","attachment; filename=\"" + filename + "\"");
response.setContentType("application/zip");

try{
response.getOutputStream().write(baos.toByteArray());
response.flushBuffer();
}
catch (IOException e){
e.printStackTrace();
}
finally{
baos.close();
}

Note I'm using a ByteArrayOutputStream wrapper and toByteArray but you could probably just write any other type of Outputstream directly to the response with a standard InputStream.read() OutputStream.write() loop.

Off hand I'm actually not sure which is faster if any, but I suspect my use of ByteArrayOutputStream here might not be the most memory conscious approach:



Related Topics



Leave a reply



Submit