I have written a directive based on Scott's answer. You'd use it like so:
<button class="btn btn-success"
download-response="getData()"
download-success="getDataSuccess()"
download-error="getDataError()"
download-name="{{name}}.pdf"
download-backup-url="/Backup/File.pdf">
Save
</button>
Issue: the code below throws an error TypeError: Invalid calling object in IE11 on the first method (line: saveBlob(blob, filename);). Even though it falls back to other methods of downloading, it is my understanding that the saveMethod1 should work in IE11.
Here is the code:
'use strict';
// directive allows to provide a function to be executed to get data to be downloaded
// attributes:
// download-response - Required. Function to get data. It must return a promise. It must be declared on the $scope.
// download-success - Optional. Function to be executed if download-response function was successfully resolved. It must be declared on the $scope.
// download-error - Optional. Function to be executed if download-response function return a promise rejection. It must be declared on the $scope.
// download-mime - Optional. provide a mime type of data being downloaded. Defaulted to "application/octet-stream"
// download-name - Optional. name of the file to download. Defaulted to "download.bin"
// download-backup-url - in case browser does not support dynamic download, this url will be called to get the file
angular.module('app.directives')
.directive('downloadResponse', [ '$parse', '$timeout',
function ($parse, $timeout) {
function saveMethod1(data, filename, contentType) {
// Support for saveBlob method (Currently only implemented in Internet Explorer as msSaveBlob, other extension in case of future adoption)
var saveBlob = navigator.msSaveBlob || navigator.webkitSaveBlob || navigator.mozSaveBlob || navigator.saveBlob;
if (saveBlob) {
// Save blob is supported, so get the blob as it's contentType and call save.
var blob = new Blob([data], { type: contentType });
saveBlob(blob, filename);
//console.log("SaveBlob Success");
} else {
throw 'saveBlob is not supported. Falling back to the next method';
}
}
function saveMethod2(data, filename, contentType, octetStreamMime) {
// Get the blob url creator
var urlCreator = window.URL || window.webkitURL || window.mozURL || window.msURL;
if (urlCreator) {
// Try to use a download link
var link = document.createElement("a");
var url;
if ("download" in link) {
// Prepare a blob URL
var blob = new Blob([data], { type: contentType });
url = urlCreator.createObjectURL(blob);
link.setAttribute("href", url);
// Set the download attribute (Supported in Chrome 14+ / Firefox 20+)
link.setAttribute("download", filename);
// Simulate clicking the download link
var event = document.createEvent('MouseEvents');
event.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
link.dispatchEvent(event);
//console.log("Download link Success");
} else {
// Prepare a blob URL
// Use application/octet-stream when using window.location to force download
var blob = new Blob([data], { type: octetStreamMime });
url = urlCreator.createObjectURL(blob);
window.location = url;
//console.log("window.location Success");
}
} else {
throw 'UrlCreator not supported. Falling back to the next method';
}
}
function saveMethod3(attrs) {
if (attrs.downloadBackupUrl && attrs.downloadBackupUrl != '') {
console.log('opening ' + attrs.downloadBackupUrl);
window.open('http://' + document.domain + attrs.downloadBackupUrl, '_blank');
} else {
throw 'Could not download a file using any of the available methods. Also you did not provide a backup download link. No more bullets left...';
}
}
return {
restrict: 'A',
scope: false,
link:function (scope, elm, attrs) {
var getDataHandler = $parse(attrs.downloadResponse);
elm.on('click', function() {
var promise = getDataHandler(scope);
promise.then(
function (data) {
if (attrs.downloadSuccess && attrs.downloadSuccess != '') {
var successHandler = $parse(attrs.downloadSuccess);
successHandler(scope);
}
var octetStreamMime = "application/octet-stream";
var filename = attrs.downloadName || "download.bin";
var contentType = attrs.downloadMime || octetStreamMime;
try {
saveMethod1(data, filename, contentType);
return;
} catch (e) {
console.log(e);
try {
saveMethod2(data, filename, contentType, octetStreamMime);
return;
} catch (e) {
console.log(e);
try {
saveMethod3(attrs);
return;
} catch (e) {
throw e;
}
throw e;
}
throw e;
}
},
function(data) {
if (attrs.downloadError && attrs.downloadError != '') {
var errorHandler = $parse(attrs.downloadError);
errorHandler(scope);
}
}
);
});
}
};
}
]);
Any help is greatly appreciated!
Problem:
I have resolved this issue. It appears that Internet Explorer 11 does not like aliases to the msSaveBlob function, demonstrated by the simplest examples:
// Succeeds
navigator.msSaveBlob(new Blob(["Hello World"]), "test.txt");
// Fails with "Invalid calling object"
var saveBlob = navigator.msSaveBlob;
saveBlob(new Blob(["Hello World"]), "test.txt");
So essentially the generic alias created to encapsulate saveBlob functionality, which should be permissible, prevents msSaveBlob from working as expected:
var saveBlob = navigator.msSaveBlob || navigator.webkitSaveBlob || navigator.mozSaveBlob || navigator.saveBlob;
Workaround:
So the work around is to test for msSaveBlob separately.
if(navigator.msSaveBlob)
navigator.msSaveBlob(blob, filename);
else {
// Try using other saveBlob implementations, if available
var saveBlob = navigator.webkitSaveBlob || navigator.mozSaveBlob || navigator.saveBlob;
if(saveBlob === undefined) throw "Not supported";
saveBlob(blob, filename);
}
tl;dr
So the msSaveBlob method works, as long as you don't alias the function. Perhaps it's a security precaution - but then perhaps a security exception would have been more appropriate, though I think it is most likely a flaw, considering the source. :)
Related
I am getting following error in IE 11 -
Error: Could not complete the operation due to error 800a138f.
Code is as follows -
File = function(k, j, i) {
var e = new Blob(k, i);
e.name = j;
e.lastModifiedDate = new Date();
return e;
}
Not sure what is going wrong. I do not see any other erros in the console. Just some subsequent error saying cannot find property of null (since above file is null).
Any idea whats wrong with above piece of code. Works fine with chrome and firefox?
EDIT :
The entire logic is -
try {
new File([], "")
} catch (g) {
console.log(g);
File = function(k, j, i) {
var e = new Blob(k, i);
e.name = j;
e.lastModifiedDate = new Date();
return e;
}
}
I added log to see why it goes to catch and I see following -
TypeError: Object doesn't support this action
Even though I don't have an IE under the hands to test it, I would say it's because window.File does exist on this browser and that you can't override this property.
Indeed, even though you are not able to call the File constructor from your script in this browser, the File constructor existed there, so e.g what you get in an input[type='file'] is an instance of File.
console.log(typeof window.File === "function" && 'File already exists');
inp.onchange = function() {
if(inp.files[0] instanceof File) {
console.log("yep, that's a File");
}
};
<input type="file" id="inp">
So they made this property non-writable, what now?
Well, that depends on what you wanted to do with this File anyway.
There is currently a very limited number of use cases for such an object:
Avoid to set the third param filename to FormData.append('fieldName', blob, filename),
and only in chrome, append the File to a FileList through DataTransfer.items.add() method.
Everything else you could have done with this File, will be done the same with a Blob, and these two methods won't be accessible to your fakeFile anyway.
But if you really want to do it, then I guess that simply choosing an other name for your function will make IE happy:
// Beware, untested code below
(function() {
var blob = new Blob(['foo'], {
type: 'text/plain'
}),
file;
try {
file = new File([blob], 'foo.txt');
if (file.name !== 'foo.txt' || !file.lastModifiedDate || !file.type === 'text/plain') {
throw new Error('invalid');
}
window.MyFile = File;
} catch (e) {
window.MyFile = function(k, j, i) {
var e = new Blob(k, i);
e.name = j;
e.lastModifiedDate = new Date();
return e;
};
}
})();
var file = new MyFile([new Blob(['bar'], {
type: 'text/plain'
})], 'mytext.txt');
console.log(file);
I am using Kendo's server side export functionality to be able to export xlsx/pdfs from the server. This is my configuration
kendo.saveAs({
dataURI: workbook.toDataURL(),
fileName: "STRResult.xlsx",
proxyURL:'/my/kendo/export/kendoServerExport',
forceProxy:true,
proxyTarget: "_blank"
});
The problem here is that I have a server (without code access) in Spring MCV and it has a global filter that checks for certain headers. I want to be able to set the required headers on this post request.
What configuration do I need to use to get it working?
TIA
I've provided a solution for this on the Kendo UI forum.
You can find it HERE
Solution as described on the forum:
I've build a solution for this based on some info found elsewhere on the internet.
I would like to share it with you, so perhaps Telerik might want to use is in some future version ;)
//#region -- Download file and save data to a local file --
// Invoke call to webservice and save file
// url - Url to get the file from
// method - GET/POST method
// defaultFilename - Default filename if none was given from the server
, saveAs: function (url, method, defaultFilename)
{
var caller = this;
Insad.ajaxMaskUI({
url: url,
type: method,
headers: {
'Authorization': Insad.GetBaseAuthenticationToken()
// ... Put your headers in here ...
},
maskUI: Insad.defaultMaskUI
, maskPageMsg: 'Downloading file'
})
.fail(function (xhr, textStatus, errorThrown) {
//console.log('error:', textStatus, errorThrown, xhr.getAllResponseHeaders(), xhr);
var msg = xhr.responseText;
if (msg === undefined) {
msg = 'Unknown error';
}
console.log('FileDownload failed -> Error handler. Errormessage: ', msg);
ShowWarning(msg);
caller.handleUIErrorDownload();
})
.done(function (data, textStatus, xhr) {
caller.handleUISuccessDownload(xhr, defaultFilename);
});
}
, handleUIErrorDownload: function ()
{
console.log('handleUIError');
// TODO: Reset stuff / give user more info?
// ... Put your code in here ...
}
, handleUISuccessDownload: function (xhr, defaultFilename)
{
console.log('handleUISuccess');
// Save file
var filename = Insad.GetFilenameFromContent(xhr);
if (filename === '') {
filename = defaultFilename;
}
Insad.SaveDataToFile(filename, xhr.responseText, xhr.getResponseHeader('Content-Type'));
// TODO: Give user more info?
// ... Put your code in here ...
}
//#endregion
//#region -- Save data to a local file --
// Get the filename from the Content-Disposition in the response xhr
, GetFilenameFromContent: function (xhr)
{
var filename = '';
var disposition = xhr.getResponseHeader('Content-Disposition');
// Only when inline or attachment are supported at this moment
if (disposition && (disposition.indexOf('attachment') !== -1 || disposition.indexOf('inline') !== -1)) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
}
}
return filename;
}
// The function that will write the data to a file :)
, SaveDataToFile: function (filename, data, mimeType)
{
var dataFileAsBlob = new Blob([data], {type:mimeType});
var downloadLink = document.createElement("a");
downloadLink.download = filename;
downloadLink.innerHTML = "Download";
if (window.webkitURL !== null) { // Is it Chrome based?
// Chrome allows the link to be clicked
// without actually adding it to the DOM.
downloadLink.href = window.webkitURL.createObjectURL(dataFileAsBlob);
}
else {
// Firefox requires the link to be added to the DOM
// before it can be clicked.
downloadLink.href = window.URL.createObjectURL(dataFileAsBlob);
downloadLink.onclick = destroyClickedElement; // --> TODO: ?? TEST ?? On Opera use: downloadLink.onclick = document.body.removeChild(event.target);
downloadLink.style.display = "none";
document.body.appendChild(downloadLink);
}
// Force download
downloadLink.click();
}
//#endregion
This is a part from our base library (Insad).
I think you get the idea. If you're missing something I would be happy to provide it to you :)
Instead of calling the 'old' saveAs like:
kendo.saveAs({
dataURI: url,
fileName: defaultFilename,
forceProxy: false
});
You can now use:
Insad.saveAs({
url: url,
method: 'GET', // or use 'POST' if you really must ;)
defaultFilename: defaultFilename
});
I left out the animation part but that shouldn't be to hard I think :)
Hi I'm tired after trying each solution but not able to make it work.
my http call in angular is
$http({
method: 'GET',
url: API_URL + 'v1/file/' + candidateId + '/download',
headers: {
'authToken': AuthService.getToken(),
},
responseType: 'arraybuffer'
})
.then(function onSuccess(response) {
successCallback(response);
},
function onError(response) {
errorCallback(response);
});
and in Success of this code
vm.onSuccessDownloadResume = function(response) {
var blob = new Blob([response.data], {type: response.headers('content-type')});
var objectUrl = URL.createObjectURL(blob);
window.open(objectUrl);
};
I tried webkitURL.createObjectURL(blob), it's working fine for chrome only but URL.createObject is not at all working.
getting message
URL.createObjectURL() is not a function()
Thanks
There's some compatibility issues related to createObjectURL, you can see more here: https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
And with Blob: https://developer.mozilla.org/en-US/docs/Web/API/BlobBuilder#Browser_compatibility
I had a similar issue, and this SO link helped me with a workaround: Blob constructor browser compatibility
I've copied this function and made some minor changes:
var NewBlob = function(data, datatype)
{
var out;
try {
out = new Blob([data], {type: datatype});
console.debug("case 1");
}
catch (e) {
window.BlobBuilder = window.BlobBuilder ||
window.WebKitBlobBuilder ||
window.MozBlobBuilder ||
window.MSBlobBuilder;
if (e.name == 'TypeError' && window.BlobBuilder) {
var bb = new BlobBuilder();
bb.append(data);
out = bb.getBlob(datatype);
console.debug("case 2");
}
else if (e.name == "InvalidStateError") {
// InvalidStateError (tested on FF13 WinXP)
out = new Blob([data], {type: datatype});
console.debug("case 3");
}
else {
// We're screwed, blob constructor unsupported entirely
console.debug("Errore");
}
}
return out;
}
Okay, I've read just about every Stack Overflow question on PDF downloading from a web service. None of them have helped me so far. I'm using this as a last ditch effort to try and get some answers. Basically, I'm making a GET request to the API, and need to get a dynamically generated PDF back. We've tried doing this with receiving a byte[] and now we're returning a stream with the content. The following is what we have in the web service controller:
var result = await resp.Content.ReadAsAsync<byte[]>();
var response = request.CreateResponse(HttpStatusCode.OK);
var dataStream = new MemoryStream(result);
response.Content = new StreamContent(dataStream);
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
response.Content.Headers.ContentDisposition.FileName = "idcard.pdf";
var fileStream = new FileStream(#"c:\temp\temp.pdf", FileMode.Create);
fileStream.Write(result, 0, result.Length);
fileStream.Close();
return response;
The FileStream part is a test we were doing to see if saving the data to a temporary file worked and that the PDF can be saved. That part does work. Going to c:\temp and opening the idcard.pdf file works perfectly. One of the problems with that is it does it silently and the user wouldn't know it's there. We can tell them, but we'd really prefer the PDF to open in the browser by default and/or have it save through the browser so they knew something happened.
My Angular code looks like this:
.factory('memberIdCard', ['$http', function($http) {
var get = function() {
return $http({
method: 'GET',
url: '/Member/IdCard',
headers: {
accept: 'application/octet-stream'
},
responseType: 'arraybuffer',
transformResponse: function(data) {
var pdf;
console.log('data: ', data);
if (data) {
pdf = new Blob([data], {
type: 'application/pdf'
});
console.log('pdf: ', pdf);
}
return pdf;
}
})
}
return {
get: get
}
}]);
I have tried this part with $http and $resource and neither works. Now, in my controller:
$scope.printIdCard = function() {
memberIdCard.get().then(function(data) {
var pdf = data.data;
FileSaver.saveAs(pdf, 'idcard.pdf');
var pdfUrl = window.URL.createObjectURL(pdf);
$scope.pdfView = $sce.trustAsResourceUrl(pdfUrl);
window.open($scope.pdfView);
});
As a note, FileSaver is from angular-file-saver.
After all that, the new window opens, but there's an error that says: Failed to load PDF Document, and if you try and open it in Adobe Acrobat it has an error that says: Adobe Acrobat Reader DC could not open 'idcard.pdf' because it is either not a supported file type or because the file has been damaged (for example, it was sent as an email attachment and wasn't correctly decoded).
Any help would be greatly appreciated. I feel like I've done everything that was suggested in many of the other SO questions, but maybe I'm missing something I just haven't been able to see.
Thanks!
I did something similar for .xlsx files, but the concept is the same. Hope this can help you, its working for me.
I got the javascript code to download the file from another SO answer, which I can't link because I don't remember where it was.
My web api controller looks like this:
[Route("all")]
[HttpGet]
public HttpResponseMessage GetAll(HttpRequestMessage request)
{
HttpResponseMessage response = null;
MemoryStream stream = _exportService.CreateDataStream();
response = request.CreateResponse(HttpStatusCode.OK);
response.Content = new ByteArrayContent(stream.GetBuffer());
response.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
response.Content.Headers.Add("content-type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
return response;
}
And the angular service:
(function (app) {
'use strict';
app.factory('exportService', exportService);
exportService.$inject = ['$q', '$http'];
function exportService($q, $http) {
var extension = '.xlsx';
var service = {
export: exportData
};
function exportData(event, fname){
var config = {
responseType: 'arraybuffer'
}
var path = 'api/export/'+event;
var deferred = $q.defer();
return $http.get(path, config).then(
function(response) {
var data = response.data;
var status = response.status;
var headers = response.headers();
var octetStreamMime = 'application/octet-stream';
var success = false;
var filename = fname + extension;
var contentType = headers['content-type'] || octetStreamMime;
try
{
// Try using msSaveBlob if supported
var blob = new Blob([data], { type: contentType });
if(navigator.msSaveBlob)
navigator.msSaveBlob(blob, filename);
else {
// Try using other saveBlob implementations, if available
var saveBlob = navigator.webkitSaveBlob || navigator.mozSaveBlob || navigator.saveBlob;
if(saveBlob === undefined) throw "Not supported";
saveBlob(blob, filename);
}
success = true;
deferred.resolve();
} catch(ex)
{
}
if(!success)
{
// Get the blob url creator
var urlCreator = window.URL || window.webkitURL || window.mozURL || window.msURL;
if(urlCreator)
{
// Try to use a download link
var link = document.createElement('a');
if('download' in link)
{
// Try to simulate a click
try
{
// Prepare a blob URL
var blob = new Blob([data], { type: contentType });
var url = urlCreator.createObjectURL(blob);
link.setAttribute('href', url);
// Set the download attribute (Supported in Chrome 14+ / Firefox 20+)
link.setAttribute("download", filename);
// Simulate clicking the download link
var event = document.createEvent('MouseEvents');
event.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
link.dispatchEvent(event);
success = true;
deferred.resolve();
} catch(ex) {
}
}
if(!success)
{
// Fallback to window.location method
try
{
var blob = new Blob([data], { type: octetStreamMime });
var url = urlCreator.createObjectURL(blob);
window.location = url;
success = true;
deferred.resolve();
} catch(ex) {
deferred.reject();
}
}
}
}
return deferred.promise;
},
function(error) {
return $q.reject(error);
});
}
return service;
}
})(angular.module('core.module'));
In a Google chrome extension I am working on, a file is downloaded from a server with an XMLHttpRequest. This file contains some binary data which are stored in an ArrayBuffer object. In order to provide the possibility to download this file I am using the createObjectURL API.
function publish(data) {
if (!window.BlobBuilder && window.WebKitBlobBuilder) {
window.BlobBuilder = window.WebKitBlobBuilder;
}
var builder = new BlobBuilder();
builder.append(data);
var blob = builder.getBlob();
var url = window.webkitURL.createObjectURL(blob);
$("#output").append($("<a/>").attr({href: url}).append("Download"));
}
It is working fine; except that the filename is an opaque UUID like 9a8f6a0f-dd0c-4715-85dc-7379db9ce142. Is there any way to force this filename to something more user-friendly?
you can force an arbitrary filename by setting the "download" attribute of your anchor
see: http://updates.html5rocks.com/2011/08/Downloading-resources-in-HTML5-a-download
I have never tried it before, but it should be possible to create a new File object (which allows you to specify a file name) and write your blob to it. Something along the lines of:
function publish(data, filename) {
if (!window.BlobBuilder && window.WebKitBlobBuilder) {
window.BlobBuilder = window.WebKitBlobBuilder;
}
fs.root.getFile(filename, {
create: true
}, function (fileEntry) {
// Create a FileWriter object for our FileEntry (log.txt).
fileEntry.createWriter(function (fileWriter) {
fileWriter.onwriteend = function (e) {
console.log('Write completed.');
};
fileWriter.onerror = function (e) {
console.log('Write failed: ' + e.toString());
};
var builder = new BlobBuilder();
builder.append(data);
var blob = builder.getBlob();
fileWriter.write(blob);
}, errorHandler);
}, errorHandler);
}
I think this could work for you.