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')));
}