I need to force download of file using JavaScript. I am using Angular and restangular to communicate with API. I am now working on file download action from API... API returns me raw bytes of that file and these headers:
Content-Disposition:attachment; filename="thefile"
Content-Length:2753
So I have raw bytes, but I do not know how to handle it to download this file to client...Can you provide me some solution of this issue? How can I handle returns response from server to open in client browser Save As dialog?
EDITED:
Server does not send me content-type of the file...Also in call's headers need to be auth token so I cannot use direct open window with url..
Code is:
vm.downloadFile = function(fileId){
var action = baseHelpers.one('files/' + fileId + '/content').get();
action.then(function(result){});
}
My first guess would be: Just request that API URL directly and not with an asynchronous request. You should be able to do something like this in your code
$window.location = "http://example.org/api/download"
For a solution using RESTangular I found this snipped, maybe you can try it:
Restangular.one('attachments', idAtt).withHttpConfig({responseType: 'blob'}}.get({}, {"X-Auth-Token": 'token'}).then(function(response) {
var url = (window.URL || window.webkitURL).createObjectURL(response);
window.open(url);
});
I had an endpoint on .Net server
[HttpPost]
[Route("api/tagExportSelectedToExcel")]
and a React frontend with axios. The task was to add a button which downloads a file from this API. I spent several hours before found this solution. I hope it will be helpful for someone else.
This is what I did:
axios('/api/tagExportSelectedToExcel', {
data: exportFilter,
method: 'POST',
responseType: 'blob'
}).then(res => resolveAndDownloadBlob(res));
Where resolveAndDownloadBlob:
/**
* Resolved and downloads blob response as a file.
* FOR BROWSERS ONLY
* #param response
*/
function resolveAndDownloadBlob(response: any) {
let filename = 'tags.xlsx';
filename = decodeURI(filename);
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(url);
link.remove();
}
It's difficult to answer without seeing your code calling the API, but in general the way you do this is to send a form rather than using ajax. Typically you'd have a hidden iframe with a name="downloadframe" or similar, and then use a form like this:
<form id="downloadform" target="downloadframe" method="POST" action="/the/api/endpoint">
<input type="hidden" name="apiParameter" value="parameter-value">
</form>
Then you'd fill in the fields of the form and submit it programmatically. Here's an example not using Angular, but adapting it would be simple (though not necessary):
var form = document.getElementById("downloadform");
form.apiParameter.value = "appropriate value";
form.submit();
When the browser gets the response, it sees the Content-Disposition header and asks the user where to save the file.
You can even build the form dynamically rather than using markup if you prefer.
Related
This is my FastAPI(python) code, which returns a .ics file:
#app.get("/latLong/")
async def read_item(lat: float,long:float):
mainFunc(lat,long)
return FileResponse("/tmp/myics.ics")
This is my frontend code in Javascript using Fetch API:
<script>
async function apiCall(long,lat) {
let myObject = await fetch('myapi.com/lat/long');
let myText = await myObject.text();
}
</script>
So from my visor (my api logs), it successfully calls the API. But from the front end, I am trying to get it to return the file.
The end result I would like to achieve is when the user clicks a button, the browser grabs the location, then sends the location to the API, and the API returns a file that the user can download.
First, you need to adjust your endpoint on server side to accept path parameters, as in the way it is currently defined, lat and long are expected to be query parameters; however, in your javascript code you are trying to send those coordinates as path parameters. Thus, your endpoint should look like this:
#app.get("/{lat}/{long}/")
async def read_item(lat: float, long: float):
Next, set the filename in FileResponse, so that it can be included in the Content-Disposition response header, which can later be retrieved on client side:
return FileResponse("/tmp/myics.ics", filename="myics.ics")
If you are doing a cross-origin request (see FastAPI CORS as well), make sure to set the Access-Control-Expose-Headers response header on server side, indicating that the Content-Disposition header should be made available to JS scripts running in the browser; otherwise, the filename won't be accessible on client side. Example:
headers = {'Access-Control-Expose-Headers': 'Content-Disposition'}
return FileResponse("/tmp/myics.ics", filename="myics.ics", headers=headers)
On client side, you could use a similar approach to this answer or this answer. The below example also takes into account scenarios where the filename includes unicode characters (i.e., -, !, (, ), etc.) and hence, comes (utf-8 encoded) in the form of, for instance, filename*=utf-8''Na%C3%AFve%20file.txt (see here for more details). In such cases, the decodeURIComponent() function is used to decode the filename. Working example below:
const url ='http://127.0.0.1:8000/41.64007/-47.285156'
fetch(url)
.then(res => {
const disposition = res.headers.get('Content-Disposition');
filename = disposition.split(/;(.+)/)[1].split(/=(.+)/)[1];
if (filename.toLowerCase().startsWith("utf-8''"))
filename = decodeURIComponent(filename.replace("utf-8''", ''));
else
filename = filename.replace(/['"]/g, '');
return res.blob();
})
.then(blob => {
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a); // append the element to the dom
a.click();
a.remove(); // afterwards, remove the element
});
I'm writing a webapp in Angular where authentication is handled by a JWT token, meaning that every request has an "Authentication" header with all the necessary information.
This works nicely for REST calls, but I don't understand how I should handle download links for files hosted on the backend (the files reside on the same server where the webservices are hosted).
I can't use regular <a href='...'/> links since they won't carry any header and the authentication will fail. Same for the various incantations of window.open(...).
Some solutions I thought of:
Generate a temporary unsecured download link on the server
Pass the authentication information as an url parameter and manually handle the case
Get the data through XHR and save the file client side.
All of the above are less than satisfactory.
1 is the solution I am using right now. I don't like it for two reasons: first it is not ideal security-wise, second it works but it requires quite a lot of work especially on the server: to download something I need to call a service that generates a new "random" url, stores it somewhere (possibly on the DB) for a some time, and returns it to the client. The client gets the url, and use window.open or similar with it. When requested, the new url should check if it is still valid, and then return the data.
2 seems at least as much work.
3 seems a lot of work, even using available libraries, and lot of potential issues. (I would need to provide my own download status bar, load the whole file in memory and then ask the user to save the file locally).
The task seems a pretty basic one though, so I'm wondering if there is anything much simpler that I can use.
I'm not necessarily looking for a solution "the Angular way". Regular Javascript would be fine.
Here's a way to download it on the client using the download attribute, the fetch API, and URL.createObjectURL. You would fetch the file using your JWT, convert the payload into a blob, put the blob into an objectURL, set the source of an anchor tag to that objectURL, and click that objectURL in javascript.
let anchor = document.createElement("a");
document.body.appendChild(anchor);
let file = 'https://www.example.com/some-file.pdf';
let headers = new Headers();
headers.append('Authorization', 'Bearer MY-TOKEN');
fetch(file, { headers })
.then(response => response.blob())
.then(blobby => {
let objectUrl = window.URL.createObjectURL(blobby);
anchor.href = objectUrl;
anchor.download = 'some-file.pdf';
anchor.click();
window.URL.revokeObjectURL(objectUrl);
});
The value of the download attribute will be the eventual file name. If desired, you can mine an intended filename out of the content disposition response header as described in other answers.
Technique
Based on this advice of Matias Woloski from Auth0, known JWT evangelist, I solved it by generating a signed request with Hawk.
Quoting Woloski:
The way you solve this is by generating a signed request like AWS does, for example.
Here you have an example of this technique, used for activation links.
backend
I created an API to sign my download urls:
Request:
POST /api/sign
Content-Type: application/json
Authorization: Bearer...
{"url": "https://path.to/protected.file"}
Response:
{"url": "https://path.to/protected.file?bewit=NTUzMDYzZTQ2NDYxNzQwMGFlMDMwMDAwXDE0NTU2MzU5OThcZDBIeEplRHJLVVFRWTY0OWFFZUVEaGpMOWJlVTk2czA0cmN6UU4zZndTOD1c"}
With a signed URL, we can get the file
Request:
GET https://path.to/protected.file?bewit=NTUzMDYzZTQ2NDYxNzQwMGFlMDMwMDAwXDE0NTU2MzU5OThcZDBIeEplRHJLVVFRWTY0OWFFZUVEaGpMOWJlVTk2czA0cmN6UU4zZndTOD1c
Response:
Content-Type: multipart/mixed; charset="UTF-8"
Content-Disposition': attachment; filename=protected.file
{BLOB}
frontend (by jojoyuji)
This way you can do it all on a single user click:
function clickedOnDownloadButton() {
postToSignWithAuthorizationHeader({
url: 'https://path.to/protected.file'
}).then(function(signed) {
window.location = signed.url;
});
}
An alternative to the existing "fetch/createObjectURL" and "download-token" approaches already mentioned is a standard Form POST that targets a new window. Once the browser reads the attachment header on the server response, it will close the new tab and begin the download. This same approach also happens to work nicely for displaying a resource like a PDF in a new tab.
This has better support for older browsers and avoids having to manage a new type of token. This will also have better long-term support than basic auth on the URL, since support for username/password on the url is being removed by browsers.
On the client-side we use target="_blank" to avoid navigation even in failure cases, which is particularly important for SPAs (single page apps).
The major caveat is that the server-side JWT validation has to get the token from the POST data and not from the header. If your framework manages access to route handlers automatically using the Authentication header, you may need to mark your handler as unauthenticated/anonymous so that you can manually validate the JWT to ensure proper authorization.
The form can be dynamically created and immediately destroyed so that it is properly cleaned up (note: this can be done in plain JS, but JQuery is used here for clarity) -
function DownloadWithJwtViaFormPost(url, id, token) {
var jwtInput = $('<input type="hidden" name="jwtToken">').val(token);
var idInput = $('<input type="hidden" name="id">').val(id);
$('<form method="post" target="_blank"></form>')
.attr("action", url)
.append(jwtInput)
.append(idInput)
.appendTo('body')
.submit()
.remove();
}
Just add any extra data you need to submit as hidden inputs and make sure they are appended to the form.
Pure JS version of James' answer
function downloadFile (url, token) {
let form = document.createElement('form')
form.method = 'post'
form.target = '_blank'
form.action = url
form.innerHTML = '<input type="hidden" name="jwtToken" value="' + token + '">'
console.log('form:', form)
document.body.appendChild(form)
form.submit()
document.body.removeChild(form)
}
I would generate tokens for download.
Within angular make an authenticated request to obtain a temporary token (say an hour) then add it to the url as a get parameter. This way you can download files in any way you like (window.open ...)
I have two file inputs and button. When the button is pressed, it should send the two files to the server and wait to receive a file back (the two files processed server-side and the result is returned).
HTML
<input id='file-input1' type='file'>
<input id='file-input2' type='file'>
<button id='send-btn'>
JavaScript (Client)
var input1 = document.getElementById('file-input1');
var input2 = document.getElementById('file-input2');
var btn = document.getElementById('send-btn');
var file1 = null;
var file2 = null;
input1.addEventListener('change', () => file1 = input1.files[0]);
input2.addEventListener('change', () => file2 = input2.files[0]);
btn.addEventListener('click', () => {
if (file1 === null || file2 === null) return;
_sendfiles(file1, file2);
});
function _sendfiles(file1, file2) {
let xmlhttp = new XMLHttpRequest();
xml.open("PUT", "/process", true);
xmlhttp.send({'file1': file1, 'file2': file2});
}
JavaScript (Server)
app.put('/process', (req, res) => {
// Get files from request
// Do stuff with them to generate a third file
// Send generated file back
});
I'm not sure how to receive the files on the server-side, nor how to wait to receive the server's file on the client side. The use of third-party modules is discouraged but not completely out of the question. I'm also not married to the idea of using XMLHttpRequest().
To send files from the client I'd suggest using Fetch + FormData API for convenience:
const formData = new FormData()
formData.append('file1', file1)
formData.append('file2', file2)
fetch(`/api/companies/${id}/logo`, {
method: 'PUT',
body: formData
})
Passing FormData instance to body will automatically set Content-Type: multipart/form-data header.
On the server side I'd suggest using multer since you already use express. You can, of course, implement your own middleware to retrieve files from the request stream if you want to (I didn't do it myself so can't help much).
To receive a file on the client you can do the following (I assume you want this file to be downloaded to the user's file system):
Way #1 (simple):
In the response just send a download URL of this file. Then use this solution to create a link and trigger click event on it. The file will be downloaded by a browser.
Way #2 (not so simple):
On the server use res.sendFile method to send a file (if it's located on fs - otherwise you can send a file Buffer like this for instance).
Then on the client you have response.blob() method to access file blob.
Use a similar trick to download this blob into a file with the help of URL.createObjectURL API.
Additionally, Response API allows you to pipe the stream and do other things with it if you need to (see Streams API).
EDIT (the simplest way)
As Endless pointed out there is a much simpler way actually. I guess I spent too much time dealing with AJAX requests... 🤷♂️
You can just submit your HTML form by clicking on submit button, this way a browser will send POST (yeah, can't do PUT this way) request with Content-Type: multipart/form-data automatically since you have inputs with type file:
<form method='post' action='/process'>
<input name='file1' type='file'>
<input name='file2' type='file'>
<button type='submit'>Submit</button>
</form>
So no need to set any event listeners or use any JS in fact.
Then on the server use res.sendFile and add a Content-Disposition: attachment; filename="filename.jpg" header to make sure browser will download it as an attachment and not open it as a webpage.
The biggest disadvantage here is that there is no built-in convenient way in a browser to subscribe to the request's completion event. I.e. there is no success event on the form which you can listen to.
So, if you need it then a nice approach would be to send a cookie from the server along with the file.
On the client set an interval at the moment you submit the form and there check if the cookie exists. If it exists then this means the file is downloaded.
I have a JavaScript Pivot table that displays data to the user. The user can select the columns/rows etc. I've added an export to Excel button, that performs an ajax post with the current data/view of the pivot table (json data). It posts the data to my server that converts that to an Excel file.
Creating the file all works well, but my problem is getting the file to the user. From what I've read I can't send a file to a user after an ajax post.
I'm happy to use a plain html post (in fact it is what I want since I can redirect the user to the file), but I don't know how to include the pivot table data as part of the post since it is not a form.
I know I can save the file locally and send a url back but this complicates things and I would like to avoid it.
Is it possible to do this without saving the file locally and sending a url where the file is located?
Browsers don't (yet) support JSON serialization of forms, so AFAIK it's not really possible to send JSON to backend using pure forms.
I have two solutions that will not require saving file on the server:
1) Simple solution would be to generate an invisible form with JavaScript, create hidden input of name json and populate it with JSON content to send to the server. On the server side, you would read the form data and parse JSON that is stored in the data. Then you just generate the file and send the file in response. The browser should trigger download dialog.
var form = document.createElement('form');
form.method = 'post';
form.action = 'url';
var input = document.createElement('input');
input.type = 'hidden';
textarea.name = 'json';
textarea.value = JSON.stringify(your_json);
form.appendChild(input);
document.body.appendChild(form);
form.submit();
form.parentNode.removeChild(form);
2) Second option uses ajax to send data to the server. The browsers need to support several APIs, though.
You do your JSON request as usual and the server should respond with header Content-Type: here-the-MIME-type-of-your-file and with the file contents in response body.
The code on client side should look like:
var json = JSON.stringify({here: ['your', 'json', 'to', 'send', 'to', 'server']});
var xhr = new XMLHttpRequest();
xhr.open('POST', '/your/api/url', true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.responseType = 'arraybuffer';
xhr.addEventListener('load', function () {
var blob = new Blob([this.response], { // reading response, not responseText
type: this.getResponseHeader('Content-Type')
});
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'file_name.txt'; // set proper extension
document.body.appendChild(a); // it needs to be added to the document in order to work
a.click();
a.parentNode.removeChild(a);
});
xhr.send(json);
Also, the created URL objects should be revoked at some point, otherwise they will exist until the page reloads. But I think they cannot be revoked before the user downloads the file. So it's one task still to do with the above code.
I have a problem when I try to download a file stored on a server. I make a call and get a right response in which I have all the information I need, in the headers I have the content type and the file name, and I have the file body in the response body.
What I want to do is to simply make a download process, so I tried to do so, data being the http call response :
// Get headers info
let headers = data.headers
let contentType = headers.get("Content-Type")
let name = headers.get("name")
// Initialize Blob
let blob = new Blob([data.text()], {type: contentType})
// Make the download process
let a = window.document.createElement("a")
a.href = window.URL.createObjectURL(blob)
a.download = name
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
For a text file, it's working as it's an easy format, but for like a picture or a PDF file it makes download a file of the right name and type, but they can't be well read.
Has anyone an idea ? Thanks !
Found a way to do it using the way described below by simulating an tag with href and download parameters :)
how to set a file name using window.open