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
In Java, How to Determine If a Char Array Contains a Particular Character
How to Have Each Record of Json on a Separate Line
How to Use Jackson to Deserialise an Array of Objects
How to Increase the Java Heap Size Permanently
Finding Max Value in an Array Using Recursion
How to Solve Liquibase Checksum Validation Fail After Liquibase Upgrade
Java.Lang.Noclassdeffounderror: Org/Json/Simple/Parser/Parseexception With Eclipse and Spring
Java: How to Ask User If He/She Wants to Continue Program
Remove Duplicate Values from Hashmap in Java
Java.Sql.Sqlexception: Access Denied for User 'Root'@'Localhost' (Using Password: Yes)
Mapstruct: Map List of Objects, When Object Is Mapped from Two Objects
How to Get Multiple Columns from Table Using Jpa
How to Update Thousands of Records into MySQL Db in Milliseconds
Set Layout Width Percentage of the Total Screen Width
A Jsonobject Text Must Begin With '{' At 1 [Character 2 Line 1] With '{' Error
Java Spring Boot Test: How to Exclude Java Configuration Class from Test Context