Download file with a REST request needing headers and giving the content - javascript

I am using AngularJs with a REST API. I don't have the hand on the REST API.
I can store digital object with the API by sending a REST request.
I can get it also with a GET request.
The requests needs to have some specific headers.
My goal is to give the user a "download and save as" link.
For now on the click event i make the request :
this.file = function (file) {
var url = config.domain + 'file/' + file;
var methods = resource(url, null, {
'get': {
method:'GET',
headers:{ 'Authorization' : user.auth,
'secret-key' : user.secretkey}
}
transformResponse : function(data, headersGetter){
return {content:data}; //transform octet stream into text, angular returns an array containing 1 character per element.
},
});
return methods;
};
in the return body I have the file content (see below). I would like to download it. How is it possible ? Notice that I can't store the file as a URL.
Would it be possible to open a window wich make the rest call with the good headers and save the file ?
EDIT
I need the solution to be able to work well with a 50Mo File.
example of a PDF file content I have :
%PDF-1.7
£´ÅÖçø
2 0 obj
[/ICCBased 3 0 R]
endobj
3 0 obj
<<
/Filter /FlateDecode
/Length 2596
/N 3
>>
stream
xwTSÙϽ7½PÐkhRH
½H.*1 JÀ"6DTpDQ¦2(à£C±"Q±ëDÔqpId­ß¼yïÍß÷~k½ÏÝgï}ÖºüÂLX ¡XáçÅg`ðlàp³³BøF|Ølø½º ùû*Ó?Áÿ¹Y"1PçòøÙ\É8=W%·Oɶ4MÎ0JÎ"Y2Vsò,[|öe9ó2<ËsÎâeðäÜ'ã9¾`çø¹2¾&ctI#Æoä±|N6(Ü.æsSdl-c(2- ãyàHÉ_ðÒ/XÌÏËÅÎÌZ.$§&\SáÏÏMçÅÌ07#â1ØYárfÏüYym²";Ø8980m-m¾(Ô]ü÷v^îDøÃöW~
°¦eµÙúmi]ëP»ýÍ`/²¾u}qº|^RÄâ,g+«ÜÜ\Kk)/èïúC_|ÏR¾Ýïåaxó8t1C^7nfz¦DÄÈÎâpùæøþuü$¾/ED˦L Lµ[ÈB#øøÃþ¤Ù¹ÚøÐX¥!#~(* {d+Ðï}ÆGùÍÑûÏþ}W¸LþÈ$cGD2¸QÎìüZ4 E#ê#èÀ¶À¸àA(q`1àD µ ­`'¨u 46ptcà48.Ë`ÜR0)ð
Ì#ÈRt CȲXäCP%CBH#ë R¨ªê¡fè[è(tº
C· Qhúz#0 ¦ÁZ°l³`O8ÁÉð28.·Àp|îOÃàX
?§:¢0ÂFBx$ !«¤i#Ú¤¹H§È[EE1PLÊ⢡V¡6£ªQP¨>ÔUÔ(j
õMFk¢ÍÑÎèt,:.FW Ðè³èô8ú¡c1L&³³³Ó9Æa¦±X¬:Öë
År°bl1¶
{{{;}#âtp¶8_\<N+ÄU
[.....]

I think you could using blob, something like
var content=...the content of your request;
var mypdf = new Blob(content, {type : 'application/pdf'});
and check answer from "panzi" in this other question Using HTML5/Javascript to generate and save a file
(One character per element in the array seem pretty nice binary. Probably you don't need to transform it. https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Sending_and_Receiving_Binary_Data )

Maybe you could do something like this?
var a = document.createElement('a');
a.href = 'data:attachment/pdf,' + encodeURI(data);
a.target = '_blank';
a.download = 'filename.pdf';
a.click();
You'd just have to make sure that data was in the correct format, which IIRC is base64.

you can use this instead of above code :
var url = config.domain + 'file/' + file;
var parameters = "Authorization=" + user.auth +
"&secret-key=" + user.secretkey;
var reportParameters = url + encodeURIComponent(parameters);
window.location.assign(reportParameters);

Thanks everybody for helping find a solution.
I am not able to find a satisfying solution in javascript and client side application.
I am going to make a proxy class communicating with the API.
This will send the REST request with security headers and will give as response the file content.
This class will be called by a HTTP GET request, so the 'save as' process will be managed easily thanks to right response headers.

Related

PDF Download in HREF of a-element results in empty PDF

I use a-element as download-button for files I have had to query with AJAX before as described here, here and here. I put to put the file-data as Data-URI into a a-element to create a download-button. Unfortunately I can not just point to the file but have to do it like that. With most download-formats HTML, CSV it works like this:
var mimeType = "text/html"; // example. works also with others.
var BOM = '\ufeff';
var url = "data:"+ mimeType +";charset=UTF-8," + BOM + encodeURIComponent(response.data);
var linkElem = document.querySelector("#hiddenDownloadLink");
var link = angular.element(linkElem);
link.prop("href", url);
link.prop("download", 'myFile.' + extension);
linkElem.click();
Okay. That works. But not for PDF.
I create a PDF in my backend (java, with openhtmltopdf but it doesn't matter I guess, since the PDF is definitely correct):
httpOutputMessage.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/pdf");
httpOutputMessage.getHeaders().add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"myFile.pdf\"");
makePdf(httpOutputMessage.getBody());
If I query the backend directly, or even if I log the output into a file, everything is fine. But when I use my download-controller as described above, I get a PDF with the right number of pages but completely empty! I think there must be an encoding issue. I tried with and without BOM, also with or without encodeURIComponent.
I also tried to use the base64-decoding as. Because window.atob(response.data) fails because of line breaks and others, I tried this conversion. The result is an broken PDF. Results in broken PDFs. I am not sure if that makes any sense.
My PDF-data starts like this, so it's not compressed or encoded anyhow:
%PDF-1.4
%����
1 0 obj
<<
/Type /Catalog
/Version /1.7
/Pages 2 0 R
>>
endobj
3 0 obj
<<
I also tried to convert the bytestream to blob and generate a link as described here orhere, but that creates broken PDFs.
Any Ideas, why I get empty PDFs or what might go wrong here and how can I repait the download-link?
--
Edit 1
I also get valid, but blank PDF when I try
var blob = new Blob([response.data], {type: 'application/pdf'});
var url = window.URL.createObjectURL(blob);
link.prop("href", url);
When I pipe response.data through this function, I get a broken PDF.
var utf8_to_b64 = function(str) {
var unescape = window.unescape || window.decodeURI;
str = encodeURIComponent(str);
str = unescape(str);
str = window.btoa(str);
return str;
};
So I could fully reproduce your situation. I loaded and output a 4 page PDF file through an AJAX request from backend, and with your code example got a 4 page blank PDF.
Here's what I did after (and got the right PDF to download):
I base64 encoded the output from the backend. In my case I was using PHP so it was something like this:
$pdf = file_get_contents('test.pdf');
header('Content-Type: application/pdf');
echo base64_encode($pdf);
Then in the frontend I changed only this line:
var url = "data:"+ mimeType +";charset=UTF-8," + BOM + encodeURIComponent(response.data);
to this:
var url = "data:"+ mimeType +";base64," + encodeURIComponent(response.data);
Hope this helps.

Download a file from a server with JavaScript / Typescript

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

Saving file with JavaScript File API results wrong encoding

I have a problem (or may be two) with saving files using HTML5 File API.
A files comes from the server as a byte array and I need to save it. I tried several ways described on SO:
creating blob and opening it in a new tab
creating a hidden anchor tag with "data:" in href attribute
using FileSaver.js
All approaches allow to save the file but with breaking it by changing the encoding to UTF-8, while the file (in current test case) is in ANSI. And it seems that I have to problems: at the server side and at the client side.
Server side:
Server side is ASP.NET Web API 2 app, which controller sends the file using HttpResponseMessage with StreamContent. The ContentType is correct and corresponds with actual file type.
But as can be seen on the screenshot below server's answer (data.length) is less then actual file size calculated at upload (file.size). Also here could be seen that HTML5 File object has yet another size (f.size).
If I add CharSet with value "ANSI" to server's response message's ContentType property, file data will be the same as it was uploaded, but on saving result file still has wrong size and become broken:
Client side:
I tried to set charset using the JS File options, but it didn't help. As could be found here and here Eli Grey, the author of FileUplaod.js says that
The encoding/charset in the type is just metadata for the browser, not an encoding directive.
which means, if I understood it right, that it is impossible to change the encoding of the file.
Issue result: at the end I can successfully download broken files which are unable to open.
So I have two questions:
How can I save file "as is" using File API. At present time I cannot use simple way with direct link and 'download' attribute because of serverside check for access_token in request header. May be this is the "bottle neck" of the problem?
How can I avoid setting CharSet at server side and also send byte array "as is"? While this problem could be hacked in some way I guess it's more critical. For example, while "ANSI" charset solves the problem with the current file, WinMerge shows that it's encoding is Cyrillic 'Windows-1251' and also can any other.
P.S. the issue is related to all file types (extensions) except *.txt.
Update
Server side code:
public HttpResponseMessage DownloadAttachment(Guid fileId)
{
var stream = GetFileStream(fileId);
var message = new HttpResponseMessage(HttpStatusCode.OK);
message.Content = new StreamContent(stream);
message.Content.Headers.ContentLength = file.Size;
message.Content.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType)
{
// without this charset files sent with bigger size
// than they are as shown on image 1
CharSet = "ANSI"
};
message.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
{
FileName = file.FileName + file.Extension,
Size = file.Size
};
return message;
}
Client side code (TypeScript):
/*
* Handler for click event on download <a> tag
*/
private downloadFile(file: Models.File) {
var self = this;
this.$service.downloadAttachment(this.entityId, file.fileId).then(
// on success
function (data, status, headers, config) {
var fileName = file.fileName + file.extension;
var clientFile = new File([data], fileName);
// here's the issue ---^
saveAs(clientFile, fileName);
},
// on fail
function (error) {
self.alertError(error);
});
}
My code is almost the same as in answers on related questions on SO: instead of setting direct link in 'a' tag, I handle click on it and download file content via XHR (in my case using Angularjs $http service). Getting the file content I create a Blob object (in my case I use File class that derives from Blob) and then try to save it using FileSaver.js. I also tried approach with encoded URL to Blob in href attribute, but it only opens a new tab with a file broken the same way. I found that the problem is in Blob class - calling it's constructor with 'normal' file data I get an instance with 'wrong' size as could be seen on first two screenshots. So, as I understand, my problem not in the way I try to save my file, but in the way I create it - File API

Download file from API using javascript

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.

JavaScript/jQuery to download file via POST with JSON data

I have a jquery-based single-page webapp. It communicates with a RESTful web service via AJAX calls.
I'm trying to accomplish the following:
Submit a POST that contains JSON data to a REST url.
If the request specifies a JSON response, then JSON is returned.
If the request specifies a PDF/XLS/etc response, then a downloadable binary is returned.
I have 1 & 2 working now, and the client jquery app displays the returned data in the web page by creating DOM elements based on the JSON data. I also have #3 working from the web-service point of view, meaning it will create and return a binary file if given the correct JSON parameters. But I'm unsure the best way to deal with #3 in the client javascript code.
Is it possible to get a downloadable file back from an ajax call like this? How do I get the browser to download and save the file?
$.ajax({
type: "POST",
url: "/services/test",
contentType: "application/json",
data: JSON.stringify({category: 42, sort: 3, type: "pdf"}),
dataType: "json",
success: function(json, status){
if (status != "success") {
log("Error loading data");
return;
}
log("Data loaded!");
},
error: function(result, status, err) {
log("Error loading data");
return;
}
});
The server responds with the following headers:
Content-Disposition:attachment; filename=export-1282022272283.pdf
Content-Length:5120
Content-Type:application/pdf
Server:Jetty(6.1.11)
Another idea is to generate the PDF and store it on the server and return JSON that includes a URL to the file. Then, issue another call in the ajax success handler to do something like the following:
success: function(json,status) {
window.location.href = json.url;
}
But doing that means I would need to make more than one call to the server, and my server would need to build downloadable files, store them somewhere, then periodically clean up that storage area.
There must be a simpler way to accomplish this. Ideas?
EDIT: After reviewing the docs for $.ajax, I see that the response dataType can only be one of xml, html, script, json, jsonp, text, so I'm guessing there is no way to directly download a file using an ajax request, unless I embed the binary file in using Data URI scheme as suggested in the #VinayC answer (which is not something I want to do).
So I guess my options are:
Not use ajax and instead submit a form post and embed my JSON data into the form values. Would probably need to mess with hidden iframes and such.
Not use ajax and instead convert my JSON data into a query string to build a standard GET request and set window.location.href to this URL. May need to use event.preventDefault() in my click handler to keep browser from changing from the application URL.
Use my other idea above, but enhanced with suggestions from the #naikus answer. Submit AJAX request with some parameter that lets web-service know this is being called via an ajax call. If the web service is called from an ajax call, simply return JSON with a URL to the generated resource. If the resource is called directly, then return the actual binary file.
The more I think about it, the more I like the last option. This way I can get information back about the request (time to generate, size of file, error messages, etc.) and I can act on that information before starting the download. The downside is extra file management on the server.
Any other ways to accomplish this? Any pros/cons to these methods I should be aware of?
letronje's solution only works for very simple pages. document.body.innerHTML += takes the HTML text of the body, appends the iframe HTML, and sets the innerHTML of the page to that string. This will wipe out any event bindings your page has, amongst other things. Create an element and use appendChild instead.
$.post('/create_binary_file.php', postData, function(retData) {
var iframe = document.createElement("iframe");
iframe.setAttribute("src", retData.url);
iframe.setAttribute("style", "display: none");
document.body.appendChild(iframe);
});
Or using jQuery
$.post('/create_binary_file.php', postData, function(retData) {
$("body").append("<iframe src='" + retData.url+ "' style='display: none;' ></iframe>");
});
What this actually does: perform a post to /create_binary_file.php with the data in the variable postData; if that post completes successfully, add a new iframe to the body of the page. The assumption is that the response from /create_binary_file.php will include a value 'url', which is the URL that the generated PDF/XLS/etc file can be downloaded from. Adding an iframe to the page that references that URL will result in the browser promoting the user to download the file, assuming that the web server has the appropriate mime type configuration.
I've been playing around with another option that uses blobs. I've managed to get it to download text documents, and I've downloaded PDF's (However they are corrupted).
Using the blob API you will be able to do the following:
$.post(/*...*/,function (result)
{
var blob=new Blob([result]);
var link=document.createElement('a');
link.href=window.URL.createObjectURL(blob);
link.download="myFileName.txt";
link.click();
});
This is IE 10+, Chrome 8+, FF 4+. See https://developer.mozilla.org/en-US/docs/Web/API/URL.createObjectURL
It will only download the file in Chrome, Firefox and Opera. This uses a download attribute on the anchor tag to force the browser to download it.
I know this kind of old, but I think I have come up with a more elegant solution. I had the exact same problem. The issue I was having with the solutions suggested were that they all required the file being saved on the server, but I did not want to save the files on the server, because it introduced other problems (security: the file could then be accessed by non-authenticated users, cleanup: how and when do you get rid of the files). And like you, my data was complex, nested JSON objects that would be hard to put into a form.
What I did was create two server functions. The first validated the data. If there was an error, it would be returned. If it was not an error, I returned all of the parameters serialized/encoded as a base64 string. Then, on the client, I have a form that has only one hidden input and posts to a second server function. I set the hidden input to the base64 string and submit the format. The second server function decodes/deserializes the parameters and generates the file. The form could submit to a new window or an iframe on the page and the file will open up.
There's a little bit more work involved, and perhaps a little bit more processing, but overall, I felt much better with this solution.
Code is in C#/MVC
public JsonResult Validate(int reportId, string format, ReportParamModel[] parameters)
{
// TODO: do validation
if (valid)
{
GenerateParams generateParams = new GenerateParams(reportId, format, parameters);
string data = new EntityBase64Converter<GenerateParams>().ToBase64(generateParams);
return Json(new { State = "Success", Data = data });
}
return Json(new { State = "Error", Data = "Error message" });
}
public ActionResult Generate(string data)
{
GenerateParams generateParams = new EntityBase64Converter<GenerateParams>().ToEntity(data);
// TODO: Generate file
return File(bytes, mimeType);
}
on the client
function generate(reportId, format, parameters)
{
var data = {
reportId: reportId,
format: format,
params: params
};
$.ajax(
{
url: "/Validate",
type: 'POST',
data: JSON.stringify(data),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
success: generateComplete
});
}
function generateComplete(result)
{
if (result.State == "Success")
{
// this could/should already be set in the HTML
formGenerate.action = "/Generate";
formGenerate.target = iframeFile;
hidData = result.Data;
formGenerate.submit();
}
else
// TODO: display error messages
}
There is a simplier way, create a form and post it, this runs the risk of resetting the page if the return mime type is something that a browser would open, but for csv and such it's perfect
Example requires underscore and jquery
var postData = {
filename:filename,
filecontent:filecontent
};
var fakeFormHtmlFragment = "<form style='display: none;' method='POST' action='"+SAVEAS_PHP_MODE_URL+"'>";
_.each(postData, function(postValue, postKey){
var escapedKey = postKey.replace("\\", "\\\\").replace("'", "\'");
var escapedValue = postValue.replace("\\", "\\\\").replace("'", "\'");
fakeFormHtmlFragment += "<input type='hidden' name='"+escapedKey+"' value='"+escapedValue+"'>";
});
fakeFormHtmlFragment += "</form>";
$fakeFormDom = $(fakeFormHtmlFragment);
$("body").append($fakeFormDom);
$fakeFormDom.submit();
For things like html, text and such, make sure the mimetype is some thing like application/octet-stream
php code
<?php
/**
* get HTTP POST variable which is a string ?foo=bar
* #param string $param
* #param bool $required
* #return string
*/
function getHTTPPostString ($param, $required = false) {
if(!isset($_POST[$param])) {
if($required) {
echo "required POST param '$param' missing";
exit 1;
} else {
return "";
}
}
return trim($_POST[$param]);
}
$filename = getHTTPPostString("filename", true);
$filecontent = getHTTPPostString("filecontent", true);
header("Content-type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"$filename\"");
echo $filecontent;
It is been a while since this question was asked but I had the same challenge and want to share my solution. It uses elements from the other answers but I wasn't able to find it in its entirety. It doesn't use a form or an iframe but it does require a post/get request pair. Instead of saving the file between the requests, it saves the post data. It seems to be both simple and effective.
client
var apples = new Array();
// construct data - replace with your own
$.ajax({
type: "POST",
url: '/Home/Download',
data: JSON.stringify(apples),
contentType: "application/json",
dataType: "text",
success: function (data) {
var url = '/Home/Download?id=' + data;
window.location = url;
});
});
server
[HttpPost]
// called first
public ActionResult Download(Apple[] apples)
{
string json = new JavaScriptSerializer().Serialize(apples);
string id = Guid.NewGuid().ToString();
string path = Server.MapPath(string.Format("~/temp/{0}.json", id));
System.IO.File.WriteAllText(path, json);
return Content(id);
}
// called next
public ActionResult Download(string id)
{
string path = Server.MapPath(string.Format("~/temp/{0}.json", id));
string json = System.IO.File.ReadAllText(path);
System.IO.File.Delete(path);
Apple[] apples = new JavaScriptSerializer().Deserialize<Apple[]>(json);
// work with apples to build your file in memory
byte[] file = createPdf(apples);
Response.AddHeader("Content-Disposition", "attachment; filename=juicy.pdf");
return File(file, "application/pdf");
}
In short, there is no simpler way. You need to make another server request to show PDF file. Al though, there are few alternatives but they are not perfect and won't work on all browsers:
Look at data URI scheme. If binary data is small then you can perhaps use javascript to open window passing data in URI.
Windows/IE only solution would be to have .NET control or FileSystemObject to save the data on local file system and open it from there.
Not entirely an answer to the original post, but a quick and dirty solution for posting a json-object to the server and dynamically generating a download.
Client side jQuery:
var download = function(resource, payload) {
$("#downloadFormPoster").remove();
$("<div id='downloadFormPoster' style='display: none;'><iframe name='downloadFormPosterIframe'></iframe></div>").appendTo('body');
$("<form action='" + resource + "' target='downloadFormPosterIframe' method='post'>" +
"<input type='hidden' name='jsonstring' value='" + JSON.stringify(payload) + "'/>" +
"</form>")
.appendTo("#downloadFormPoster")
.submit();
}
..and then decoding the json-string at the serverside and setting headers for download (PHP example):
$request = json_decode($_POST['jsonstring']), true);
header('Content-Type: application/csv');
header('Content-Disposition: attachment; filename=export.csv');
header('Pragma: no-cache');
$scope.downloadSearchAsCSV = function(httpOptions) {
var httpOptions = _.extend({
method: 'POST',
url: '',
data: null
}, httpOptions);
$http(httpOptions).then(function(response) {
if( response.status >= 400 ) {
alert(response.status + " - Server Error \nUnable to download CSV from POST\n" + JSON.stringify(httpOptions.data));
} else {
$scope.downloadResponseAsCSVFile(response)
}
})
};
/**
* #source: https://github.com/asafdav/ng-csv/blob/master/src/ng-csv/directives/ng-csv.js
* #param response
*/
$scope.downloadResponseAsCSVFile = function(response) {
var charset = "utf-8";
var filename = "search_results.csv";
var blob = new Blob([response.data], {
type: "text/csv;charset="+ charset + ";"
});
if (window.navigator.msSaveOrOpenBlob) {
navigator.msSaveBlob(blob, filename); // #untested
} else {
var downloadContainer = angular.element('<div data-tap-disabled="true"><a></a></div>');
var downloadLink = angular.element(downloadContainer.children()[0]);
downloadLink.attr('href', window.URL.createObjectURL(blob));
downloadLink.attr('download', "search_results.csv");
downloadLink.attr('target', '_blank');
$document.find('body').append(downloadContainer);
$timeout(function() {
downloadLink[0].click();
downloadLink.remove();
}, null);
}
//// Gets blocked by Chrome popup-blocker
//var csv_window = window.open("","","");
//csv_window.document.write('<meta name="content-type" content="text/csv">');
//csv_window.document.write('<meta name="content-disposition" content="attachment; filename=data.csv"> ');
//csv_window.document.write(response.data);
};
I think the best approach is to use a combination, Your second approach seems to be an elegant solution where browsers are involved.
So depending on the how the call is made. (whether its a browser or a web service call) you can use a combination of the two, with sending a URL to the browser and sending raw data to any other web service client.
Found it somewhere long time ago and it works perfectly!
let payload = {
key: "val",
key2: "val2"
};
let url = "path/to/api.php";
let form = $('<form>', {'method': 'POST', 'action': url}).hide();
$.each(payload, (k, v) => form.append($('<input>', {'type': 'hidden', 'name': k, 'value': v})) );
$('body').append(form);
form.submit();
form.remove();
I have been awake for two days now trying to figure out how to download a file using jquery with ajax call. All the support i got could not help my situation until i try this.
Client Side
function exportStaffCSV(t) {
var postData = { checkOne: t };
$.ajax({
type: "POST",
url: "/Admin/Staff/exportStaffAsCSV",
data: postData,
success: function (data) {
SuccessMessage("file download will start in few second..");
var url = '/Admin/Staff/DownloadCSV?data=' + data;
window.location = url;
},
traditional: true,
error: function (xhr, status, p3, p4) {
var err = "Error " + " " + status + " " + p3 + " " + p4;
if (xhr.responseText && xhr.responseText[0] == "{")
err = JSON.parse(xhr.responseText).Message;
ErrorMessage(err);
}
});
}
Server Side
[HttpPost]
public string exportStaffAsCSV(IEnumerable<string> checkOne)
{
StringWriter sw = new StringWriter();
try
{
var data = _db.staffInfoes.Where(t => checkOne.Contains(t.staffID)).ToList();
sw.WriteLine("\"First Name\",\"Last Name\",\"Other Name\",\"Phone Number\",\"Email Address\",\"Contact Address\",\"Date of Joining\"");
foreach (var item in data)
{
sw.WriteLine(string.Format("\"{0}\",\"{1}\",\"{2}\",\"{3}\",\"{4}\",\"{5}\",\"{6}\"",
item.firstName,
item.lastName,
item.otherName,
item.phone,
item.email,
item.contact_Address,
item.doj
));
}
}
catch (Exception e)
{
}
return sw.ToString();
}
//On ajax success request, it will be redirected to this method as a Get verb request with the returned date(string)
public FileContentResult DownloadCSV(string data)
{
return File(new System.Text.UTF8Encoding().GetBytes(data), System.Net.Mime.MediaTypeNames.Application.Octet, filename);
//this method will now return the file for download or open.
}
Good luck.
I liked Frank's idea and decided to do my own twist to it. As trying to do it in one post is very complicated, I'm using the two post method but only hitting the database once and no need to save the file or clean up file when completed.
First I run the ajax request to retrieve the data but instead of returning the data from the controller I will return a GUID that is tied to a TempData storage of the records.
$.get("RetrieveData", { name: "myParam"} , function(results){
window.location = "downloadFile?id=" + results
});
public string RetrieveData(string name)
{
var data = repository.GetData(name);
string id = Guid.NewGuid().ToString();
var file = new KeyValuePair<string, MyDataModel>(name, data);
TempData[id]=file;
return id;
}
Then when I call the window.location I pass the Guid to the new method and get the data from TempData. After this method is executed TempData will be free.
public ActionResult DownloadFile(string id)
{
var file = (KeyValuePair<string,MyDataModel>)TempData[id];
var filename = file.Key;
var data = file.Value;
var byteArray = Encoding.UTF8.GetBytes(data);
...
return File(byteArray, "text/csv", "myFile.csv");
}
Another approach instead of saving the file on the server and retrieving it, is to use .NET 4.0+ ObjectCache with a short expiration until the second Action (at which time it can be definitively dumped). The reason that I want to use JQuery Ajax to do the call, is that it is asynchronous. Building my dynamic PDF file takes quite a bit of time, and I display a busy spinner dialog during that time (it also allows other work to be done). The approach of using the data returned in the "success:" to create a Blob does not work reliably. It depends on the content of the PDF file. It is easily corrupted by data in the response, if it is not completely textual which is all that Ajax can handle.
Solution
Content-Disposition attachment seems to work for me:
self.set_header("Content-Type", "application/json")
self.set_header("Content-Disposition", 'attachment; filename=learned_data.json')
Workaround
application/octet-stream
I had something similar happening to me with a JSON, for me on the server side I was setting the header to
self.set_header("Content-Type", "application/json")
however when i changed it to:
self.set_header("Content-Type", "application/octet-stream")
It automatically downloaded it.
Also know that in order for the file to still keep the .json suffix you will need to it on filename header:
self.set_header("Content-Disposition", 'filename=learned_data.json')
The Problems with Making your own events
Many of the solutions proposed on this article have the JavaScript run asynchronously and create a link element then calling
const a = documet.createElement("a")
a.click()
or creating a mouse event
new MouseEvent({/* ...some config */})
This would seem fine right? What could be wrong with this?
What is an Event-Sourcing?
Event sourcing has a bunch of meanings across computing such as a system of pub sub in a cloud based architecture, or the browser api EventSource. In the context of a browser
all events have a source and that source has hidden property that says who initiated this event (the user or the site).
Knowing this we can start to understand why two click events might not be treated the same
user click* new MouseEvent()
----------- -----------
| Event 1 | | Event 2 |
----------- -----------
| |
|----------------------|
|
|
----------------------
| Permissions Policy | Available in chrome allows the server to control
---------------------- what features are going to be used by the JS
|
|
----------------------------
| Browser Fraud Protection | The Browser REALLY doesnt like being told to pretend
---------------------------- to be a user. If you will remember back to the early
| 2000s when one click spun off 2000 pop ups. Well here
| is where popups are blocked, fraudulent ad clicks are
\ / thrown out, and most importantly for our case stops
v fishy downloads
JavaScript Event Fires
So I just Can't Download off A POST That's Dumb
No, of course you can. You just need to give the user a chance to create the event. Here are a number of patterns that you can use to create user flows that are obvious and convectional and will not be flagged as fraud. (using jsx sorry not sorry)
A Form can be used to navigate to a url with a post action.
const example = () => (
<form
method="POST"
action="/super-api/stuff"
onSubmit={(e) => {/* mutably change e form data but don't e.preventDetfault() */}}
>
{/* relevant input fields of your download */}
</form>
)
Preloading If your download is non-configurable you may want to consider preloading the download into resp.blob() or new Blob(resp) this tells the browser that this is a file and we wont be doing any string operations on it. As with the other answers you can use window.URL.createObjectURL what is not mentioned is that
createObjectURL CAN MAKE A MEMORY LEAK IN JAVASCRIPTsource
If you don't want the C++ bully's to come make fun of you you must free this memory. Ahh but I'm just a hobbiest who loves his garbage collector. Have no fear this is very simple if you are working in most frameworks (for me react) you just register some sort of clean up effect on your component and your right as rain.
const preload = () => {
const [payload, setPayload] = useState("")
useEffect(() => {
fetch("/super-api/stuff")
.then((f) => f.blob())
.then(window.URL.createObjectURL)
.then(setPayload)
return () => window.URL.revokeObjectURL(payload)
}, [])
return (<a href={payload} download disabled={payload === ""}>Download Me</a>)
}
I think I got close, but something is corrupting the file (Image), any way, maybe some one can disclose the problem of this approach
$.ajax({
url: '/GenerateImageFile',
type: 'POST',
cache: false,
data: obj,
dataType: "text",
success: function (data, status, xhr) {
let blob = new Blob([data], { type: "image/jpeg" });
let a = document.createElement('a');
a.href = window.URL.createObjectURL(blob);
a.download = "test.jpg";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.removeObjectURL(a.href);
},
complete: function () {
},
beforeSend: function () {
}
});
With HTML5, you can just create an anchor and click on it. There is no need to add it to the document as a child.
const a = document.createElement('a');
a.download = '';
a.href = urlForPdfFile;
a.click();
All done.
If you want to have a special name for the download, just pass it in the download attribute:
const a = document.createElement('a');
a.download = 'my-special-name.pdf';
a.href = urlForPdfFile;
a.click();

Categories