Return 来自客户端浏览器 (REST) 上的服务器的 zip(或任何文件)
Return a zip (or any file) from the server on the client browser (REST)
所以我为服务器使用 Java,为客户端使用 Angular。我目前正在开发一项功能,您可以从 table 中 select 多个文件,当您按下下载时,它会生成一个 zip 文件并将其下载到您的浏览器。截至目前,服务器现在创建了 zip 文件,我可以在服务器文件中访问它。剩下要做的就是让它在客户端的浏览器上下载。 (zip文件在客户端下载后被删除)
经过一些研究,我发现您可以使用 fileOutputStream 来执行此操作。我还看到了一些类似改造的工具......我正在使用 REST,这就是我的代码的样子。我将如何尽可能简单地实现我的目标?
Angular
httpGetDownloadZip(target: string[]): Observable<ServerAnswer> {
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<ServerAnswer>(this.BASE_URL + '/files/downloadZip', options)
.pipe(catchError(this.handleError<ServerAnswer>('httpGetZip')));
}
Java压缩方式
public void 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
ZipUtil.pack(new File("Download"), new File("selected-files.zip"));
// what do I return ???
return;
}
Java 上下文
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
Controller.getDownloadZip(files, folderName);
// what do I return to download the file on the client's browser ????
return;
}
});
您仍然可以在服务器上使用 HttpServletRequest...
然后获取它的OutputStream并写入它。
@RequestMapping(method = RequestMethod.POST , params="action=downloadDocument")
public String downloadDocument(@RequestParam(value="documentId", required=true) String documentId,
HttpServletRequest request,
HttpServletResponse response )
{
try {
String docName = null;
String documentSavePath = getDocumentSavePath();
PDocument doc = mainService.getDocumentById(iDocumentId);
if(doc==null){
throw new RuntimeException("document with id: " + documentId + " not found!");
}
docName = doc.getName();
String path = documentSavePath + ContextUtils.fileSeperator() + docName;
response.setHeader("Content-Disposition", "inline;filename=\"" + docName + "\"");
OutputStream out = response.getOutputStream();
response.setContentType("application/word");
FileInputStream stream = new FileInputStream(path);
IOUtils.copy(stream, out);
out.flush();
out.close();
} catch(FileNotFoundException fnfe){
logger.error("Error downloading document! - document not found!!!! " + fnfe.getMessage() , fnfe);
} catch (IOException e) {
logger.error("Error downloading document!!! " + e.getMessage(),e);
}
return null;
}
您可以尝试将 responseType 用作 arraybuffer。
例如:
return this.http.get(URL_API_REST + 'download?filename=' + filename, {
responseType: 'arraybuffer'
});
在我的项目中,包括前端 (angular) 和后端 (java)。
我们使用了以下解决方案(希望对您有用):
Angular:
https://github.com/eligrey/FileSaver.js
let observable = this.downSvc.download(opts);
this.handleData(observable, (data) => {
let content = data;
const blob = new Blob([content], { type: 'application/pdf' });
saveAs(blob, file);
});
Java:
public void download(HttpServletRequest request,HttpServletResponse response){
....
response.setHeader("Content-Disposition",
"attachment;filename=\"" + fileName + "\"");
try (
OutputStream os = response.getOutputStream();
InputStream is = new FileInputStream(file);) {
byte[] buf = new byte[1024];
int len = 0;
while ((len = is.read(buf)) > -1) {
os.write(buf, 0, len);
}
os.flush();
}
成功下载 zip 文件的可能方法如下文所述。
首先,考虑return在您的 downloadZip
方法中引用作为压缩结果获得的 zip 文件:
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;
}
现在,修改您的 HttpHandler
以执行下载:
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();
}
}
});
现在的问题是如何处理Angular中的下载。
正如前面代码中所建议的那样,如果资源是 public 或者您有办法管理您的安全令牌,例如将其作为参数包含在 URL 中,一个可能的解决方案是不使用 Angular HttpClient
,而是使用带有 href
的锚点,直接指向您的后端处理程序方法。
如果您需要使用 Angular HttpClient
,也许要包含您的身份验证令牌,那么您可以尝试这个很棒的 SO question.
中提出的方法
首先,在您的 handler
中,将压缩文件内容编码为 Base64 以简化字节处理任务(在一般用例中,您通常可以 return 从您的服务器 JSON 包含文件内容和描述该内容的元数据的对象,如内容类型等):
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();
}
}
});
之后,在客户端 Angular 组件中使用以下代码执行下载:
this.downloadService.httpGetDownloadZip(['target1','target2']).pipe(
tap((b64Data) => {
const blob = this.b64toBlob(b64Data, 'application/zip');
const blobUrl = URL.createObjectURL(blob);
window.open(blobUrl);
})
).subscribe()
如上述问题所示,b64toBlob
将如下所示:
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;
}
可能您需要稍微修改服务中的 httpGetDownloadZip
方法以考虑 returned base 64 数据 - 基本上,将 ServerAnswer
更改为 string
作为 returned 信息类型:
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')));
}
所以我为服务器使用 Java,为客户端使用 Angular。我目前正在开发一项功能,您可以从 table 中 select 多个文件,当您按下下载时,它会生成一个 zip 文件并将其下载到您的浏览器。截至目前,服务器现在创建了 zip 文件,我可以在服务器文件中访问它。剩下要做的就是让它在客户端的浏览器上下载。 (zip文件在客户端下载后被删除)
经过一些研究,我发现您可以使用 fileOutputStream 来执行此操作。我还看到了一些类似改造的工具......我正在使用 REST,这就是我的代码的样子。我将如何尽可能简单地实现我的目标?
Angular
httpGetDownloadZip(target: string[]): Observable<ServerAnswer> {
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<ServerAnswer>(this.BASE_URL + '/files/downloadZip', options)
.pipe(catchError(this.handleError<ServerAnswer>('httpGetZip')));
}
Java压缩方式
public void 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
ZipUtil.pack(new File("Download"), new File("selected-files.zip"));
// what do I return ???
return;
}
Java 上下文
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
Controller.getDownloadZip(files, folderName);
// what do I return to download the file on the client's browser ????
return;
}
});
您仍然可以在服务器上使用 HttpServletRequest...
然后获取它的OutputStream并写入它。
@RequestMapping(method = RequestMethod.POST , params="action=downloadDocument")
public String downloadDocument(@RequestParam(value="documentId", required=true) String documentId,
HttpServletRequest request,
HttpServletResponse response )
{
try {
String docName = null;
String documentSavePath = getDocumentSavePath();
PDocument doc = mainService.getDocumentById(iDocumentId);
if(doc==null){
throw new RuntimeException("document with id: " + documentId + " not found!");
}
docName = doc.getName();
String path = documentSavePath + ContextUtils.fileSeperator() + docName;
response.setHeader("Content-Disposition", "inline;filename=\"" + docName + "\"");
OutputStream out = response.getOutputStream();
response.setContentType("application/word");
FileInputStream stream = new FileInputStream(path);
IOUtils.copy(stream, out);
out.flush();
out.close();
} catch(FileNotFoundException fnfe){
logger.error("Error downloading document! - document not found!!!! " + fnfe.getMessage() , fnfe);
} catch (IOException e) {
logger.error("Error downloading document!!! " + e.getMessage(),e);
}
return null;
}
您可以尝试将 responseType 用作 arraybuffer。
例如:
return this.http.get(URL_API_REST + 'download?filename=' + filename, {
responseType: 'arraybuffer'
});
在我的项目中,包括前端 (angular) 和后端 (java)。
我们使用了以下解决方案(希望对您有用):
Angular: https://github.com/eligrey/FileSaver.js
let observable = this.downSvc.download(opts);
this.handleData(observable, (data) => {
let content = data;
const blob = new Blob([content], { type: 'application/pdf' });
saveAs(blob, file);
});
Java:
public void download(HttpServletRequest request,HttpServletResponse response){
....
response.setHeader("Content-Disposition",
"attachment;filename=\"" + fileName + "\"");
try (
OutputStream os = response.getOutputStream();
InputStream is = new FileInputStream(file);) {
byte[] buf = new byte[1024];
int len = 0;
while ((len = is.read(buf)) > -1) {
os.write(buf, 0, len);
}
os.flush();
}
成功下载 zip 文件的可能方法如下文所述。
首先,考虑return在您的 downloadZip
方法中引用作为压缩结果获得的 zip 文件:
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;
}
现在,修改您的 HttpHandler
以执行下载:
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();
}
}
});
现在的问题是如何处理Angular中的下载。
正如前面代码中所建议的那样,如果资源是 public 或者您有办法管理您的安全令牌,例如将其作为参数包含在 URL 中,一个可能的解决方案是不使用 Angular HttpClient
,而是使用带有 href
的锚点,直接指向您的后端处理程序方法。
如果您需要使用 Angular HttpClient
,也许要包含您的身份验证令牌,那么您可以尝试这个很棒的 SO question.
首先,在您的 handler
中,将压缩文件内容编码为 Base64 以简化字节处理任务(在一般用例中,您通常可以 return 从您的服务器 JSON 包含文件内容和描述该内容的元数据的对象,如内容类型等):
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();
}
}
});
之后,在客户端 Angular 组件中使用以下代码执行下载:
this.downloadService.httpGetDownloadZip(['target1','target2']).pipe(
tap((b64Data) => {
const blob = this.b64toBlob(b64Data, 'application/zip');
const blobUrl = URL.createObjectURL(blob);
window.open(blobUrl);
})
).subscribe()
如上述问题所示,b64toBlob
将如下所示:
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;
}
可能您需要稍微修改服务中的 httpGetDownloadZip
方法以考虑 returned base 64 数据 - 基本上,将 ServerAnswer
更改为 string
作为 returned 信息类型:
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')));
}