Amit Pokhrel
Amit Pokhrel

Reputation: 290

Streaming file to file system using angular 7

I am trying to develop a file download using angular 7. I am using HttpClient and FileSaver for download. The problem I am having is that, when the HttpClient makes the download request to the server, it waits for the entire response to be completed (keeps the entire file in browser memory) and the save dialogue appears only at the end. I believe in case of large files, storing it in memory will cause problem. Is there a way I can show the save dialogue as soon as the status OK is received and stream the file to the filesystem. I also need to send the authorization header with the request.

My server side code:

@RequestMapping(value = "/file/download", method = RequestMethod.GET)
    public void downloadReport(@RequestParam("reportId") Integer reportId, HttpServletResponse response) throws IOException {
        if (null != reportId) {
            JobHandler handler = jobHandlerFactory.getJobHandler(reportId);
            InputStream inStream = handler.getReportInputStream();

            response.setContentType(handler.getContentType());
            response.setHeader("Content-Disposition", "attachment; filename=" + handler.getReportName());

            FileCopyUtils.copy(inStream, response.getOutputStream());
        }
    }

My Client code (angular)

downloadLinksByAction(id, param) {
      this._httpClient.get(AppUrl.DOWNLOAD, { params: param, responseType: 'blob', observe: 'response' }).subscribe((response: any) => {
        const dataType = response.type;
        const filename = this.getFileNameFromResponseContentDisposition(response);
        const binaryData = [];
        binaryData.push(response.body);
        const blob = new Blob(binaryData, { type: dataType });
        saveAs(blob, filename);
      }, err => {
        console.log('Error while downloading');
      });
  }

  getFileNameFromResponseContentDisposition = (res: Response) => {
    const contentDisposition = res.headers.get('content-disposition') || '';
    const matches = /filename=([^;]+)/ig.exec(contentDisposition);
    return matches && matches.length > 1 ? matches[1] : 'untitled';
  };

Upvotes: 3

Views: 6726

Answers (1)

Amit Pokhrel
Amit Pokhrel

Reputation: 290

Answering my own question hoping it might help someone stuck with the same problem. I figured out that there is not any way to initiate streaming directly to file system with an ajax call. What I ended up doing is create a new endpoint called /token. This endpoint would take parameters required for file download and create a JWT signed token. This token will be used as a queryParameters for /download?token=xxx endpoint. I bypassed this endpoint from spring security with .authorizeRequests().antMatchers("/download").permitAll(). Since /download requires a signed token, I just need to verify if the signature is valid for an authentic download request. Then in the client side I just created a dynamic <a> tag and triggered a click() event. Token Provider:

import com.google.common.collect.ImmutableMap;
import com.vh.dashboard.dataprovider.exceptions.DataServiceException;
import com.vh.dashboard.dataprovider.exceptions.ErrorCodes;
import com.vh.dashboard.security.CredentialProvider;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.TextCodec;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.Map;

@Component
public class TokenProvider {

    @Value("${security.jwt.download.signing-key}")
    private String tokenSignKey;

    @Autowired
    CredentialProvider credentialProvider;

    private static int VALIDITY_MILISECONDS = 6000000;

    public String generateToken(Map claimsMap) {
        Date expiryDate = new Date(
                System.currentTimeMillis() + (VALIDITY_MILISECONDS));

        return Jwts.builder().setExpiration(expiryDate)
                .setSubject(credentialProvider.getLoginName())
                .addClaims(claimsMap).addClaims(
                        ImmutableMap
                                .of("userId", credentialProvider.getUserId()))
                .signWith(
                        SignatureAlgorithm.HS256,
                        TextCodec.BASE64.encode(tokenSignKey)).compact();
    }

    public Map getClaimsFromToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(TextCodec.BASE64.encode(tokenSignKey))
                    .parseClaimsJws(token).getBody();

        } catch (Exception e) {
            throw new DataServiceException(e, ErrorCodes.INTERNAL_SERVER_ERROR);
        }
    }
}

client code:

 this._httpClient.post(AppUrl.DOWNLOADTOKEN, param).subscribe((response: any) => {
          const url = AppUrl.DOWNLOAD + '?token=' + response.data;
          const a = document.createElement('a');
          a.href = url;
//This download attribute will not change the route but download the file.
          a.download = 'file-download';
          document.body.appendChild(a);
          a.click();
          document.body.removeChild(a);
        }

Upvotes: 4

Related Questions