Skip to content

Saving a remote file

Jimmy Wärting edited this page May 19, 2021 · 15 revisions

FileSaver is built for saving client side generated content, but if the content is coming from a server then there is different ways to achieve the goal of saving the file downloaded from the cloud.

Using Http Header

Content-Disposition attachment header is the best preferred way to download files from the browser. It has better cross browser compatibility, won't have any memory limit and it doesn't require any JavaScript.

Content-Type: application/octet-stream makes the browser incompatible to render the page so the fallback solution for browsers is to save the resource.

Content-Length is optional and using it will let the user how much there is left in a progress bar.

Content-Type: 'application/octet-stream; charset=utf-8'
Content-Disposition: attachment; filename="filename.jpg"; filename*="filename.jpg"
Content-Length: <size in bytes>

Directives

filename

Is followed by a string containing the original name of the file transmitted. The filename is always optional and must not be used blindly by the application: path information should be stripped, and conversion to the server file system rules should be done. This parameter provides mostly indicative information. When used in combination with Content-Disposition: attachment, it is used as the default filename for an eventual 'Save As" dialog presented to the user.

filename*

The parameters "filename" and "filename*" differ only in that "filename*" uses the encoding defined in RFC 5987. When both "filename" and "filename*" are present in a single header field value, "filename*" is preferred over "filename" when both are present and understood.

When saving remote file then you can't fetch it with AJAX. For a download to begin it has to be navigated to so the browser can do their thing. when using ajax then you take control over it instead.

Using a form element (to upload data and using other http methods)

If the file is generated using a POST request or that you have to upload something that needs to be converted. then you might think that you can't just navigate to the resource but you can actually use a simple <form> element to change the http method and the change tre encoding do both upload and download files that needs to be converted at the same time.

if you had use AJAX to request a Blob and created a objectURL then you have lost their ability to see a download progress or giving them a way to cancel the download

Using a[download]

If you have no way to add change the response headers a better way to save it directly to the hard drive would be to use the a[download] attribute

<a href="uploads/cat.png" download="cat.png">download cat.png</a>

This attribute instructs browsers to download a URL instead of navigating to it, so the user will be prompted to save it as a local file. If the attribute has a value, it is used as the pre-filled file name in the Save prompt (the user can still change the file name if they want). There are no restrictions on allowed values, though / and \ are converted to underscores. Most file systems limit some punctuation in file names, and browsers will adjust the suggested name accordingly.

Notes:

  • This attribute can be used with blob: URLs and data: URLs to download content generated by JavaScript, such as pictures created in an image-editor Web app.
  • If the HTTP header Content-Disposition: gives a different filename than this attribute, the HTTP header takes priority over this attribute.
  • If Content-Disposition: is set to inline, Firefox prioritizes Content-Disposition, like the filename case, while Chrome prioritizes the download attribute.

Using Ajax + FileSaver

background

If for some reason you need some headers (like authentication) for downloading the remote file. Then ajax will probably be the only way forward. Same goes if download attribute isn't supported in the browser you are targeting or content-disposition could not be added from the back-end server.

One way you could add request headers and modify the response header to include the content-disposition header is through Service Worker but I guess not everybody will go that extra mile. Unless you use StreamSaver.js which is the core method of how StreamSaver works. it basically mimics what a server have to do to save a file

An important thing in all request is to get the response as a blob. Don't construct a Blob afterwards, set the responseType to Blob or use the .blob() instead of .text() in fetch

When you use blob it tells the browser not to parse the text content, and to let the bytes pass through unprocessed. Creating a blob from textContent is problematic if downloading binary data

Diffrent ajax methods:

Plain vanilla XMLHttpRequest
var xhr = new XMLHttpRequest()
xhr.open(method, url)
xhr.responseType = 'blob'
xhr.onload = function() {
  FileSaver.saveAs(xhr.response, filename);
}
xhr.send()
Fetch API
// ES7
const res = await fetch(url)
const blob = await res.blob()
saveAs(blob, fileName)

// ES6
fetch(url)
  .then(res => res.blob())
  .then(blob => saveAs(blob, fileName))

// ES5
fetch(url)
  .then(function(res) {
    return res.blob()
  })
  .then(function(blob) {
    saveAs(blob, fileName)
  })
Angulars 1.x $http
$http({
  url: "http://localhost:8080/filename.zip",
  responseType: "blob"
}).then(function(response) {
  saveAs(response.data, fileName)
})
Angulars http
import {ResponseContentType } from '@angular/http'

@Injectable()
export class AngularService {

    constructor(private http: Http) {}

    download(model: MyModel) { //get file from service
        this.http.post("http://localhost/a2/pdf.php", JSON.stringify(model), {
            method: RequestMethod.Post,
            responseType: ResponseContentType.Blob,
            headers: new Headers({'Content-Type', 'application/x-www-form-urlencoded'})
        }).subscribe(
            response => { // download file
                var blob = new Blob([response.blob()], {type: 'application/pdf'});
                var filename = 'file.pdf';
                saveAs(blob, filename);
            },
            error => {
                console.error(`Error: ${error.message}`);
            }
        );
    }
}
jQuery's $.ajax()

Don't! They don't support changing the response type.