【问题标题】:Return a zip (or any file) from the server on the client browser (REST)在客户端浏览器 (REST) 上从服务器返回一个 zip(或任何文件)
【发布时间】:2021-09-28 19:47:27
【问题描述】:

所以我将 Java 用于服务器,将 Angular 用于客户端。我目前正在开发一项功能,您可以从表格中选择多个文件,当您按下下载时,它会生成一个 zip 文件并将其下载到您的浏览器。截至目前,服务器现在创建了 zip 文件,我可以在服务器文件中访问它。剩下要做的就是让它在客户端的浏览器上下载。 (客户端下载后zip文件被删除)

经过一番研究,我发现您可以使用 fileOutputStream 来执行此操作。我还看到了一些工具,例如改造……我正在使用 REST,这就是我的代码的样子。我将如何尽可能简单地实现我的目标?

角度

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

【问题讨论】:

    标签: java angular rest download zip


    【解决方案1】:

    以下段落中描述了一种成功下载 zip 文件的可能方法。

    首先,考虑在您的 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 中处理下载。

    如前面代码中所建议的,如果资源是公开的,或者您有办法管理安全令牌,例如,将其作为参数包含在 URL 中,一种可能的解决方案是不使用 Angular HttpClient,而是一个带有href 的锚点,它直接指向您的后端处理程序方法。

    如果您需要使用 Angular HttpClient,也许要包含您的身份验证令牌,那么您可以尝试这个伟大的 SO question 中提出的方法。

    首先,在您的handler 中,将压缩文件内容编码为 Base64 以简化字节处理任务(在一般用例中,您通常可以从服务器返回一个 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 方法以考虑返回的 base 64 数据 - 基本上,将 ServerAnswer 更改为 string 作为返回的信息类型:

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

    【讨论】:

      【解决方案2】:

      您可以尝试使用 responseType 作为数组缓冲区。

      例如:

      return this.http.get(URL_API_REST + 'download?filename=' + filename, {
          responseType: 'arraybuffer'
        });
      

      【讨论】:

        【解决方案3】:

        在我的项目中,包括前端(角度)和后端(java)。

        我们使用了以下解决方案(希望对您有用):

        角度: 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();
            }
        

        【讨论】:

          【解决方案4】:

          您仍然可以在服务器上使用 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;
              }
          

          【讨论】:

          • 我很困惑...我该把它放在哪里?
          • 您说“我正在使用 REST”如果您使用的是 REST,这是 REST 控制器内部的一个休息功能。
          猜你喜欢
          • 2023-04-03
          • 1970-01-01
          • 2011-05-31
          • 2018-11-29
          • 2014-10-05
          • 2014-09-27
          • 1970-01-01
          • 2018-03-04
          • 1970-01-01
          相关资源
          最近更新 更多