Given https://www.example.com/image-list:
...
<a href="/image/1337">
<img src="//static.example.com/thumbnails/86fb269d190d2c85f6e0468ceca42a20.png"/>
</a>
<a href="//static.example.com/full/86fb269d190d2c85f6e0468ceca42a20.png"
download="1337 - Hello world!.png">
Download
</a>
...
This is a user script environment, so I have no access to server configuration. As such:
I can't make server accept user-friendly file names like https://static.example.com/full/86fb269d190d2c85f6e0468ceca42a20 - 1337 - Hello World!.png.
I can't configure Cross-Origin Resource Sharing. www.example.com and static.example.com are separated by CORS wall by design.
How to make Firefox and Chrome display Save File As dialog with the suggested file name "1337 - Hello world!.png" when a user clicks on the "Download" link?
After some failing and googling, I learned these problems:
Firefox completely ignores existence of the download attribute on some image MIME types.
Firefox completely ignores existence of the download attribute on cross-site links.
Chrome completely ignores value of the download attribute on cross-site links.
All these points don't make any sense to me, all look like "let's put random non-sensical limitations on the feature", but I have to accept them as it's my environment.
Do any ways to solve the problem exist?
Background: I'm writing a user script for an image board which uses MD5 hashes as file names. I want to make saving with user-friendly names easier. Anything which gets me closer to this would be helpful.
I guess I can get around the limitations by using object URLs to blobs and a local proxy with hacked CORS headers, but this setup is obviously beyond reasonable. Saving through canvas could work (are images "protected" by CORS in this case too?), but it will either force double lossy compression or lossy-to-lossless conversion, given JPEG files, neither of which are good.
All modern browsers will ignore the download attribute in the anchor tag for cross-origin URL'S.
Reference : https://html.spec.whatwg.org/multipage/links.html#downloading-resources
According to the spec makers, this represents a security loophole as a user could be tricked into downloading malicious files while browsing a secure site, believing that the file is also originating from the same secure site.
Any interesting conversation for implementing this feature in the firefox browser can be found here : https://bugzilla.mozilla.org/show_bug.cgi?id=676619
[ Edit by Athari ]
Quote from specification:
This could be dangerous, because, for instance, a hostile server could be trying to get a user to unknowingly download private information and then re-upload it to the hostile server, by tricking the user into thinking the data is from the hostile server.
Thus, it is in the user's interests that the user be somehow notified that the resource in question comes from quite a different source, and to prevent confusion, any suggested filename from the potentially hostile interface origin should be ignored.
Clarification on the mysterious scenario:
the more serious issue with CORS downloads is if a malicious site forces a download of a file form a legitimate site and some how gets access to its content. so lets say I download the user gmail inbox page and explore its messages.
in this case an evil site will have to fool the user into downloading the file and uploading it back to the server, so lets say we have a gmail.com/inbox.html actually contains all the user mail messages and the attacker sites offers a download link for a coupon file, that should be uploaded to another evil site. the coupon will supposedly offer a 30% discount on a new Ipad. the download link will actually point to gmail.com/inbox.html and will download it as "30off.coupon", the if the user will download this file and upload it without checking it's content the evil site will get the user "coupon" and so its inbox content.
Important notes:
Google originally didn't limit download attribute by CORS and was explicitly against this. It was later forced to adjust Chrome implementation.
Google was opposed to using CORS for this.
Alternative solutions were proposed with giving a user a warning about cross-origin downloads. They were ignored.
Well there can be notification or deny/allow mechanism when downloading from another origin (e.g. like in case of geolocation API). Or not to send cookies in case of cross origin request with download attribute.
Some developers do share the opinion that the restriction is too strong, severely limits the usage of the feature and that the scenario is so complicated that the user who would do this would easily download and run an executable file. Their opinion was disregarded.
The case against allowing cross-origin downloads is centered around the premise that visitors of an [evil] site (eg, discountipads.com) could unknowingly download a file from a site containing their own personal information (eg, gmail.com) and save it to their disk using a misleading name (eg, "discount.coupon") AND THEN proceed to another malicious page where they manually upload that same file they just downloaded. This is quite far-fetched in my opinion, and anyone who would succumb to such trivial trickery perhaps does not belong online in the first place. I mean c'mon...Click here to download our special discount offer and then re-upload it through our special form! Seriously? Download our special offer and then email it to this Yahoo address for a big discount! Do the people who fall for these things even know how to do email attachments?
I'm all for browser security, but if the good people of Chromium have no problem with this I don't see why Firefox has to completely banish it. At the very least I'd like to see a preference in about:config to enable cross-origin #download for "advanced" users (default it to false). Even better would be a confirmation box similar to: "Although this page is encrypted, the information you submit through this form won't be" or: "This page is requesting to install addons" or: "Files downloaded from the web may harm your computer" or even: "The security certificate of this page is invalid" ...y'know what I mean? There are myriad ways to heighten the user's awareness and inform them this might not be safe. One extra click and a short (or long?) delay is enough to let them assess the risk.
As the web grows, and the use of CDNs grows, and the presence of advanced web-apps grows, and the need to manage files hosted across servers grows, features like #download will become more important. And when a browser like Chrome supports it fully whereas Firefox does not, this is not a win for Firefox.
In short, I think that mitigating the potential evil uses of #download by simply ignoring the attribute in cross-origin scenarios is a woefully ill-thought move. I'm not saying the risk is entirely non-existent, quite the contrary: I am saying there are plenty of risky things one does online in the course of his day...downloading ANY file is high among them. Why not work around that issue with a well-thought user experience?
Overall, considering widespread use of CDNs and intentionally putting user-generated content on a different domain, the primary use for the download attribute is specifying a file name for blob downloads (URL.createObjectURL) and the like. It can't be used in a lot of configurations and certainly not very useful in user scripts.
Try something like:
Get the external image to your server first
Return the fetched image from your server.
Dynamically create an anchor with download name and .click() it!
while the above was just a pretty short tips list... give this a try:
on www.example.com place a fetch-image.php with this content:
<?php
$url = $_GET["url"]; // the image URL
$info = getimagesize($url); // get image data
header("Content-type: ". $info['mime']); // act as image with right MIME type
readfile($url); // read binary image data
die();
or with any other server-side language that achieves the same.
The above should return any external image as it's sitting on your domain.
On your image-list page, what you can try now is:
<a
href="//static.example.com/thumbnails/86fb269d190d2c85f6e0468ceca42a20.png"
download="1337 - Hello world!.png">DOWNLOAD</a>
and this JS:
function fetchImageAndDownload (e) {
e.preventDefault(); // Prevent browser's default download stuff...
const url = this.getAttribute("href"); // Anchor href
const downloadName = this.download; // Anchor download name
const img = document.createElement("img"); // Create in-memory image
img.addEventListener("load", () => {
const a = document.createElement("a"); // Create in-memory anchor
a.href = img.src; // href toward your server-image
a.download = downloadName; // :)
a.click(); // Trigger click (download)
});
img.src = 'fetch-image.php?url='+ url; // Request image from your server
}
[...document.querySelectorAll("[download]")].forEach( el =>
el.addEventListener("click", fetchImageAndDownload)
);
You should see finally the image downloaded as
1337 - Hello world!.png
instead of 86fb269d190d2c85f6e0468ceca42a20.png like it was the case.
Notice: I'm not sure about the implications of simultaneous requests toward fetch-image.php - make sure to test, test.
If you have access to both, backend and frontend code, here are steps which could help you
I'm not sure which type of backend language you are using, so I will just explain what need to be done without code sample.
In backend, for preview your code should work as is, if you get in query string something like ?download=true then your backend should pack file as dispositioned content, in other words you would use content-disposition response header. This will open you possibility to put additional attributes to content, like filename, so it could be simething like this
Content-Disposition: attachment; filename="filename.jpg"
Now, in frontent, any link which should behave as download button need to contain ?download=true in href query parameter AND target="_blank" which will temporary open another tab in browser for download purpose.
<a href="/image/1337">
<img src="//static.example.com/thumbnails/86fb269d190d2c85f6e0468ceca42a20.png"/>
</a>
<a href="//static.example.com/full/86fb269d190d2c85f6e0468ceca42a20.png?download=true" target="_blank" title="1337 - Hello world!.png">
Download Full size
</a>
I know that this works without CORS setup and if user clicks on download link, but I never tested Save As dialog in browser... and it will take some time to build this again, so please give it a try.
Relevant Chromium API...
https://developer.chrome.com/extensions/downloads#event-onDeterminingFilename
Example...
chrome.downloads.onDeterminingFilename.addListener(function(item,suggest){
suggest({filename:"downloads/"+item.filename}); // suggest only the folder
suggest({filename:"downloads/image23.png"}); // suggest folder and filename
});
Oops... you're on server side but I assumed client side! I'll leave this here though in case someone needs it.
You can try to do this
var downloadHandler = function(){
var url = this.dataset.url;
var name = this.dataset.name;
// by this you can automaticaly convert any supportable image type to other, it is destination image format
var mime = this.dataset.type || 'image/jpg';
var image = new Image();
//We need image and canvas for converting url to blob.
//Image is better then recieve blob through XHR request, because of crossOrigin mode
image.crossOrigin = "Anonymous";
image.onload = function(oEvent) {
//draw image on canvas
var canvas = document.createElement('canvas');
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;
canvas.getContext('2d').drawImage(this, 0, 0, canvas.width, canvas.height);
// get image from canvas as blob
var binStr = atob( canvas.toDataURL(mime).split(',')[1] ),
len = binStr.length,
arr = new Uint8Array(len);
for (var i = 0; i < len; i++ ) {
arr[i] = binStr.charCodeAt(i);
}
var blob = new Blob( [arr], {type: mime} );
//IE not works with a.click() for downloading
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(blob, name);
} else {
var a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = name;
a.click();
}
};
image.src = url;
}
document.querySelector("[download]").addEventListener("click", downloadHandler)
<button
data-name="file.png"
data-url="https://tpc.googlesyndication.com/simgad/14257743829768205599"
data-type="image/png"
download>
download
</button>
Another modern way for modern browsers (except Internet Explorer)
var downloadHandler = function(){
var url = this.dataset.url;
var name = this.dataset.name;
fetch(url).then(function(response) {
return response.blob();
}).then(function(blob) {
//IE and edge not works with a.click() for downloading
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(blob, name);
} else {
var a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = name;
a.click();
}
});
};
document.querySelector("[download]").addEventListener("click", downloadHandler)
<button
data-name="file.png"
data-url="https://tpc.googlesyndication.com/simgad/14257743829768205599"
download>
download
</button>
I'm able to rename base64 images with a save as input field.
I think your best bet is to create your own "save as" box. When a user clicks "download", display a "File Name: {input field}" and a save button. Have the save button change filename and then call the download function.
function save2() {
var gh = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADNQTFRFAAAAPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09EvTTBAAAABB0Uk5TAA8fLz9PX29/j5+vv8/f7/rfIY4AAARsSURBVHja7VvZkusqDAyrAQvQ/3/teZjExrHDZoGrbl0/z0RtLY1oya/X/8+Nh/NHzYuAGPRz9llERET1GACJiIgYngaAkmvNnwTgERHtgwDefngawDofgDkAwPlp4I4AzHQA/gggstk0jF/P7ELQ3wBQPBoBRPRzTyI8P/bBGvh79FMstFXCvDSAt0kHzyBYNv7jj/iAx48DEiwzEfj0AFi/EEw4F+2B/5mfXQsbB4ZDQOKGwM2ioE+4hUdEm3jCjzybRbw4gIXkrxfbySnqCfYhS48rG23fs/wRGYdcGIQv1PsOcIgTkp//xTcs4WbyTEjs67pmFfh8+3+X1s0Jy3z7rxezaZ9EdTjI2MC1MpA37LqN65kjdoJuPmtUUpC40NmvLy2WntM3OcH09RupE8KdMLjefufgBE1gvz2blnj/2pDY7wikSPold9M+dCVSWpDuln1HUMLuCfsHEndP2H+9uO+kJEfVaicNq+zin9udxY6gQcrRlFeNHUG1oCfpjpIjAtmaukQXHRabpJwdMNlFSzZFdL3Dv4WkrlH4lyH6Y6jOgj0BSPUGWV0InrQAztISr2UgahFe1r3XJgHRC9C+qhK3CqC/4H6Sm1XV64ApCKt5NegOgFTGGGPMIlnhx22NA64zhUsppTxVMtcuvY5hcCqX31DhgAu+EgZ+WLjSjoPJvF6mBH5lIFvC7wHBJ7kAAAByjFdkAvdDg0o7/PPByiOCSSIvbfhBo6HExvES/ftwjOs7v7iyoZCl0qhMhHWpDQoX9QvH/xJd+osriAbr9ZktEQONCm3yAD5EEU833YWIlgsA1PD5UwGAGz4DLAAIw0eAeQBs/CTaZi2o8VNYyAIwP2qAHsCSZYGR6xD5xtgPTwGeBzB+I0Xlj+Oajo2kCEK+GRqfg2sWwEAaKhCNLDdsRCkgnwLg8kEeDyDmLQwHoAp3w+EA1kJPPBoAL6lEYnAZmuLtfCwRbToZLwEYNP7X5Vs33NEFuI15BS6U7+auuydmGkoKXI1Kt9RlIZPHIIllLbfzWwboCm2AF480b7WUQkipDWySkhPlg7ggU9apWPFqkWzV2TZC1Am1a1UMltMWW8F6Xve4qpRCX86U3ZQkcEtFF79UKtW8RSJnsvr+IDK7N23HRScH+mrtWQ/RCF3D+DYOaM337bOKftvQ78iKps3fjbDIrkeX22cVLqAKAovVFfD1DzRi/V4AgbWmDMW8ivmO7Qto9FlV/FvGr5xsZilj3/hXI00UTPcKi6PYgkrXR5qnb/72ZuRho03fSF5E1xOGg7qvb5VPz2akTmcbnT48LExDCysycxitdGfRcWUbar2gvj59cDfqyH3NoMpNyt+k5r77t1B+tb/eZNzJtTt1y+4umXM49b9g1AmFUPvloDdzqsppDweA/RuSOoDLv6D7GvRAKPUP5ceo3DWbX4nFXm5iy8ubEfqCWiut22HDDqZcyBuP6zL6s0euLVzbBqunfWbFpTZmhfdjjVFy9seO/6nnH0Mpp/3TjvofAAAAAElFTkSuQmCC"
var a = document.createElement('a');
a.href = gh;
a.download = document.getElementById("input").value;
a.click()
}
Filename: <input id="input" name="input" placeholder="enter image name"></insput><button onclick="save2()">Save</button>
The above code will let you name the file for the user If you want the user to be able to name the file himself then you need to create an input field that updates a.download = 'imagetest.png'; through a listening function or onkeychange. I got it to work through "a.download = document.getElementById("input").value;". It's amazing what getElementByID can do.
Browsers are very limited in the ability to name files for people to download. I imagine it's a lack of functionality that has been overlooked.
If you want to take it a step further you can hide the "file name:" input field with display:hidden;. And you can have a "download" button with an onclick function that sets the "file name: input / save" div to no longer set to display:hidden;. This can also be done through getelementbyID.
I did however notice that there seems to be an issue with renaming the image with a URL instead of base64. So I tried integrating my code with yours though none of the code below seemed to work properly when clicking on your download link. I still think it's close enough that you may want to fiddle around with it if this is the route you want to go:
<script>
function save2() {
var gh = "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"
var a = document.createElement('a');
a.href = gh;
a.download = document.getElementById("input").value;
a.click()
}
function downloadit(){
var x = document.getElementById("input").value;
document.getElementById("dl").getAttribute("download") = x;
}
document.getElementById("dl").onclick = function() {
document.getElementById("dl").download= a.download = document.getElementById("input").value;
return false;
}
</script>
<a href="/image/1337">
<img src="//static.example.com/thumbnails/86fb269d190d2c85f6e0468ceca42a20.png"/>
</a>
<a onclick='downloadit()' id="dl" href="//static.example.com/full/86fb269d190d2c85f6e0468ceca42a20.png"
download="1337 - Hello world!.png">
Download
</a>
Filename: <input id="input" name="input" placeholder="enter image name"></insput><button onclick="save2()">Save</button>
Lastly, I was able to rename your example.com image with the following script. Though when I try it with a working googleimage it doesn't seem to rename. So you might want to dabble with this as well.
document.getElementById("dl").onclick = function() {
document.getElementById("dl").download=document.getElementById("input").value;
}
Filename: <input id="input" name="input" placeholder="enter image name"></input>
<a id="dl" href="//static.example.com/full/86fb269d190d2c85f6e0468ceca42a20.png"
download="1337 - Hello world!.png">
Download
</a>
Related
Update
Since asking the question below and arriving at a more fundamental question after finding the error in the code, I found some more information such as in the MDN web docs for the downloads API method downloads.download() it states that a revoke of an object url should be performed only after the file/url has been downloaded. So, I spent some time trying to understand whether or not a web extension makes the downloads API onChanged event 'available' to javascript of a web page and don't think it does. I don't understand why the downloads API is available to extensions only, especailly when there are quite a few questions concerning this same memory-usage/object-url-revocation issue. For example, Wait for user to finish downloading a blob in Javascript.
If you know, would you please explain? Thank you.
Starting with Firefox browser closed, and right clicking on a local html file to open in Firefox, it opens with five firefox.exe processes as viewed in Windows Task Manager. Four of the processes start with between 20,000k and 25,000k of memory and one with about 115,000k.
This html page has an indexedDB database with 50 object stores each containing 50 objects. Each object is extracted from its object store and converted to string using JSON.stringify, and written to a two-dimensional array. Afterward, all elements of the array are concatenated into one large string, converted to a blob and written to the hard disk through a URL object which is revoked immediately afterward. The final file is about 190MB.
If the code is stopped just before the conversion to blob, one of the firefox.exe process's memory usage increases to around 425,000k and then falls back to 25,000k in about 5-10 seconds after the elements of the array have been concatenated into a single string.
If the code is run to completion, the memory usage of that same firefox.exe process grows to about 1,000,000k and then drops to about 225,000k. The firefox.exe process that started at 115,000k also increases at the blob stage of the code to about 325,000k and never decreases.
After the blob is written to disk as a text file, these two firefox.exe processes never release the approximate 2 x 200,000k increase in memory.
I have set every variable used in each function to null and the memory is never freed unless the page is refreshed. Also, this process is initiated by a button click event; and if it is run again without an intermediate refresh, each of these two firefox.exe processes grab an additional 200,000k of memory with each run.
I haven't been able to figure out how to free the memory?
The two functions are quite simple. json[i][j] holds the string version of the jth object from the ith object store in the database. os_data[] is an array of small objects { "name" : objectStoreName, "count" : n }, where n is the number of objects in the store. The build_text fuction appears to release the memory if write_to_disk is not invoked. So, the issue appears to be related to the blob or the url.
I'm probably overlooking something obvious. Thank you for any direction you can provide.
EDIT:
I see from JavaScript: Create and save file that I have a mistake in the revokeObjectURL(blob) statment. It can't revoke blob, the createObjectURL(blob) needed to be saved to a variable like url and then revoke url, not blob.
That worked for the most part and the memory is released from both of the firefox.exe processes mentioned above, in most cases. This leaves me with one small question about the timing of the revoke of the url.
If the revoke is what allows for the release of memory, should the url be revoked only after the file has been successfully downloaded? If the revoke takes place before the user clicks ok to download the file, what happens? Suppose I click the button to prepare the file from the database and after it's ready the browser brings up the window for downloading, but I wait a little while thinking about what to name the file or where to save it, won't the revoke statment be run already but the url is still 'held' by the browser since it is what will be downloaded? I know I can still download the file, but does the revoke still release the memory? From my small amount of experimenting with this one example, it appears that it does not get released in this scenario.
If there was an event that fires when the file has either successfully or unsuccessfully been downloaded to the client, is not that the time when the url should be revoked? Would it be better to set a timeout of a few minutes before revoking the url, since I'm pretty sure there is not an event indicating download to client has ended.
I'm probably not understanding something basic about this. Thanks.
function build_text() {
var i, j, l, txt = "";
for ( i = 1; i <=50; i++ ) {
l = os_data[i-1].count;
for ( j = 1; j <= l; j++ ) {
txt += json[i][j] + '\n';
}; // next j
}; // next i
write_to_disk('indexedDB portfolio', txt);
txt = json = null;
} // close build_text
function write_to_disk( fileName, data ) {
fileName = fileName.replace(".","");
var blob = new Blob( [data], { type: 'text/csv' } ), elem;
if ( window.navigator.msSaveOrOpenBlob ) {
window.navigator.msSaveBlob(blob, fileName);
} else {
elem = window.document.createElement('a');
elem.href = window.URL.createObjectURL(blob);
elem.download = fileName;
document.body.appendChild(elem);
elem.click();
document.body.removeChild(elem);
window.URL.revokeObjectURL(blob);
}; // end if
data = blob = elem = fileName = null;
} // close write_to_disk
I am a bit lost as to what is the question here...
But let's try to answer, at least part of it:
For a starter let's explain what URL.createObjectURL(blob) roughly does:
It creates a blob URI, which is an URI pointing to the Blob blob in memory just like if it was in an reachable place (like a server).
This blob URI will mark blob as being un-collectable by the Garbage Collector (GC) for as long as it has not been revoked, so that you don't have to maintain a live reference to blob in your script, but that you can still use/load it.
URL.revokeObjectURL will then break the link between the blob URI and the Blob in memory. It will not free up the memory occupied by blob directly, it will just remove its own protection regarding the GC, [and won't point to anywhere anymore].
So if you have multiple blob URI pointing to the same Blob object, revoking only one won't break the other blob URIs.
Now, the memory will be freed only when the GC will kick in, and this in only decided by the browser internals, when it thinks it is the best time, or when it sees it has no other options (generally when it misses memroy space).
So it is quite normal that you don't see your memory being freed up instantly, and by experience, I would say that FF doesn't care about using a lot of memory, when it is available, making GC kick not so often, whihc is good for user-experience (GCing often results in lags).
For your download question, indeed, web APIs don't provide a way to know if a download has been successful or failed, nor even if it has just ended.
For the revoking part, it really depends on when you do it.
If you do it directly in the click handler, then the browser won't have done the pre-fetch request yet, so when the default action of the click (the download) will happen, there won't be anything linked by the URI anymore.
Now, if you do revoke the blob URI after the "save" prompt, the browser will have done a pre-fetch request, and thus might be able to mark by itself that the Blob resource should not be cleared. But I don't think this behavior is tied by any specs, and it might be better to wait at least for the window's focus event, at which point the downloading of the resource should already have started.
const blob = new Blob(['bar']);
const uri = URL.createObjectURL(blob);
anchor.href = uri;
anchor.onclick = e => {
window.addEventListener('focus', e=>{
URL.revokeObjectURL(uri);
console.log("Blob URI revoked, you won't be able to download it anymore");
}, {once: true});
};
<a id="anchor" download="foo.txt">download</a>
In my Vue app I receive a PDF as a blob, and want to display it using the browser's PDF viewer.
I convert it to a file, and generate an object url:
const blobFile = new File([blob], `my-file-name.pdf`, { type: 'application/pdf' })
this.invoiceUrl = window.URL.createObjectURL(blobFile)
Then I display it by setting that URL as the data attribute of an object element.
<object
:data="invoiceUrl"
type="application/pdf"
width="100%"
style="height: 100vh;">
</object>
The browser then displays the PDF using the PDF viewer. However, in Chrome, the file name that I provide (here, my-file-name.pdf) is not used: I see a hash in the title bar of the PDF viewer, and when I download the file using either 'right click -> Save as...' or the viewer's controls, it saves the file with the blob's hash (cda675a6-10af-42f3-aa68-8795aa8c377d or similar).
The viewer and file name work as I'd hoped in Firefox; it's only Chrome in which the file name is not used.
Is there any way, using native Javascript (including ES6, but no 3rd party dependencies other than Vue), to set the filename for a blob / object element in Chrome?
[edit] If it helps, the response has the following relevant headers:
Content-Type: application/pdf; charset=utf-8
Transfer-Encoding: chunked
Content-Disposition: attachment; filename*=utf-8''Invoice%2016246.pdf;
Content-Description: File Transfer
Content-Encoding: gzip
Chrome's extension seems to rely on the resource name set in the URI, i.e the file.ext in protocol://domain/path/file.ext.
So if your original URI contains that filename, the easiest might be to simply make your <object>'s data to the URI you fetched the pdf from directly, instead of going the Blob's way.
Now, there are cases it can't be done, and for these, there is a convoluted way, which might not work in future versions of Chrome, and probably not in other browsers, requiring to set up a Service Worker.
As we first said, Chrome parses the URI in search of a filename, so what we have to do, is to have an URI, with this filename, pointing to our blob:// URI.
To do so, we can use the Cache API, store our File as Request in there using our URL, and then retrieve that File from the Cache in the ServiceWorker.
Or in code,
From the main page
// register our ServiceWorker
navigator.serviceWorker.register('/sw.js')
.then(...
...
async function displayRenamedPDF(file, filename) {
// we use an hard-coded fake path
// to not interfere with legit requests
const reg_path = "/name-forcer/";
const url = reg_path + filename;
// store our File in the Cache
const store = await caches.open( "name-forcer" );
await store.put( url, new Response( file ) );
const frame = document.createElement( "iframe" );
frame.width = 400
frame.height = 500;
document.body.append( frame );
// makes the request to the File we just cached
frame.src = url;
// not needed anymore
frame.onload = (evt) => store.delete( url );
}
In the ServiceWorker sw.js
self.addEventListener('fetch', (event) => {
event.respondWith( (async () => {
const store = await caches.open("name-forcer");
const req = event.request;
const cached = await store.match( req );
return cached || fetch( req );
})() );
});
Live example (source)
Edit: This actually doesn't work in Chrome...
While it does set correctly the filename in the dialog, they seem to be unable to retrieve the file when saving it to the disk...
They don't seem to perform a Network request (and thus our SW isn't catching anything), and I don't really know where to look now.
Still this may be a good ground for future work on this.
And an other solution, I didn't took the time to check by myself, would be to run your own pdf viewer.
Mozilla has made its js based plugin pdf.js available, so from there we should be able to set the filename (even though once again I didn't dug there yet).
And as final note, Firefox is able to use the name property of a File Object a blobURI points to.
So even though it's not what OP asked for, in FF all it requires is
const file = new File([blob], filename);
const url = URL.createObjectURL(file);
object.data = url;
In Chrome, the filename is derived from the URL, so as long as you are using a blob URL, the short answer is "No, you cannot set the filename of a PDF object displayed in Chrome." You have no control over the UUID assigned to the blob URL and no way to override that as the name of the page using the object element. It is possible that inside the PDF a title is specified, and that will appear in the PDF viewer as the document name, but you still get the hash name when downloading.
This appears to be a security precaution, but I cannot say for sure.
Of course, if you have control over the URL, you can easily set the PDF filename by changing the URL.
I believe Kaiido's answer expresses, briefly, the best solution here:
"if your original URI contains that filename, the easiest might be to simply make your object's data to the URI you fetched the pdf from directly"
Especially for those coming from this similar question, it would have helped me to have more description of a specific implementation (working for pdfs) that allows the best user experience, especially when serving files that are generated on the fly.
The trick here is using a two-step process that perfectly mimics a normal link or button click. The client must (step 1) request the file be generated and stored server-side long enough for the client to (step 2) request the file itself. This requires you have some mechanism supporting unique identification of the file on disk or in a cache.
Without this process, the user will just see a blank tab while file-generation is in-progress and if it fails, then they'll just get the browser's ERR_TIMED_OUT page. Even if it succeeds, they'll have a hash in the title bar of the PDF viewer tab, and the save dialog will have the same hash as the suggested filename.
Here's the play-by-play to do better:
You can use an anchor tag or a button for the "download" or "view in browser" elements
Step 1 of 2 on the client: that element's click event can make a request for the file to be generated only (not transmitted).
Step 1 of 2 on the server: generate the file and hold on to it. Return only the filename to the client.
Step 2 of 2 on the client:
If viewing the file in the browser, use the filename returned from the generate request to then invoke window.open('view_file/<filename>?fileId=1'). That is the only way to indirectly control the name of the file as shown in the tab title and in any subsequent save dialog.
If downloading, just invoke window.open('download_file?fileId=1').
Step 2 of 2 on the server:
view_file(filename, fileId) handler just needs to serve the file using the fileId and ignore the filename parameter. In .NET, you can use a FileContentResult like File(bytes, contentType);
download_file(fileId) must set the filename via the Content-Disposition header as shown here. In .NET, that's return File(bytes, contentType, desiredFilename);
client-side download example:
download_link_clicked() {
// show spinner
ajaxGet(generate_file_url,
{},
(response) => {
// success!
// the server-side is responsible for setting the name
// of the file when it is being downloaded
window.open('download_file?fileId=1', "_blank");
// hide spinner
},
() => { // failure
// hide spinner
// proglem, notify pattern
},
null
);
client-side view example:
view_link_clicked() {
// show spinner
ajaxGet(generate_file_url,
{},
(response) => {
// success!
let filename = response.filename;
// simplest, reliable method I know of for controlling
// the filename of the PDF when viewed in the browser
window.open('view_file/'+filename+'?fileId=1')
// hide spinner
},
() => { // failure
// hide spinner
// proglem, notify pattern
},
null
);
I'm using the library pdf-lib, you can click here to learn more about the library.
I solved part of this problem by using api Document.setTitle("Some title text you want"),
Browser displayed my title correctly, but when click the download button, file name is still previous UUID. Perhaps there is other api in the library that allows you to modify download file name.
I have an application that hosts videos, and we recently migrated to Azure.
On our old application we gave the ability for users to either play or download the video. However on Azure it seems like I have to pick between which functionality I want, as the content disposition has to be set on the file and not on the request.
So far I have came up with two very poor solutions.
The first solution is streaming the download through my MVC server.
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(ConfigurationManager.AppSettings["StorageConnectionString"]);
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
CloudBlobContainer container = blobClient.GetContainerReference("videos");
string userFileName = service.FirstName + service.LastName + "Video.mp4";
Response.AddHeader("Content-Disposition", "attachment; filename=" + userFileName); // force download
container.GetBlobReference(service.Video.ConvertedFilePath).DownloadToStream(Response.OutputStream);
return new EmptyResult();
This option works okay for smaller videos, but it is very taxing on my server. For larger videos the operation times out.
The second option is hosting every video twice.
This option is obviously bad, as I will have to pay double the storage cost.
However on Azure it seems like I have to pick between which
functionality I want, as the content disposition has to be set on the
file and not on the request.
There's a workaround for that. As you may know there's a Content-Disposition property that you can define on a blob. However when you define a value for this property, it will always be applied on that blob. When you want to selectively apply this property on a blob (say on a per request basis), what you do is create a Shared Access Signature (SAS) on that blob and override this request header there. Then you can serve the blob via SAS URL.
Here's the sample code for this:
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(ConfigurationManager.AppSettings["StorageConnectionString"]);
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
CloudBlobContainer container = blobClient.GetContainerReference("videos");
string userFileName = service.FirstName + service.LastName + "Video.mp4";
CloudBlockBlob blob = container.GetBlockBlobReference(userFileName);
SharedAccessBlobPolicy policy = new SharedAccessBlobPolicy()
{
Permissions = SharedAccessBlobPermissions.Read,
SharedAccessExpiryTime = DateTime.UtcNow.AddHours(1)
};
SharedAccessBlobHeaders blobHeaders = new SharedAccessBlobHeaders()
{
ContentDisposition = "attachment; filename=" + userFileName
};
string sasToken = blob.GetSharedAccessSignature(policy, blobHeaders);
var sasUrl = blob.Uri.AbsoluteUri + sasToken;//This is the URL you will use. It will force the user to download the video.
I wrote a blog post about the same long time ago that you may find useful: http://gauravmantri.com/2013/11/28/new-changes-to-windows-azure-storage-a-perfect-thanksgiving-gift/.
As far as I know, azure blob storage doesn't support add the custom header to the special container.
I suggest you could follow and vote this feedback to push the azure develop team to support this feature.
Here is a workaround, you could compression the video file firstly, then uploaded to the azure blob storage.
It will not be opened by the browser.
I need to create a small browser-based application that helps users download/save, and possibly print to the default printer, a large number of files from a webserver we have no control over (but we have all the URIs beforehand).
These files are hidden behind a "single sign-on" (SSO) that can only be performed via a browser and requires a user. Hence, it must be a browser-based solution, where we piggyback on to the session established by the SSO.
The users' platform is Windows 7.
The point is to save the users from going through a lot of clicks per file (download, where to save, etc.) when they need to perform this operation (daily).
At this point all the files are PDF, but that might change in the future.
A browser-agnostic solution is preferred (and I assume more robust when it comes to future browser updates).
But we can base it on a particular browser if needed.
How would you do this from Javascript?
As the comments to my question says, this isn't really allowed by the browsers for security reasons.
My workaround for now (only tested using IE11) is to manually change the security settings of the users browser, and then download the files as a blob into a javascript variable using AJAX, followed by upload of same blob to my own server again using AJAX.
"My own server" is a Django site created for this purpose, that also knows which files to download for the day, and provide the javascript needed. The user goes to this site to initiate the daily download after performing the SSO in a separate browser tab.
On the server I can then perform whatever operations needed for said files.
Many thanks to this post https://stackoverflow.com/a/13887220/833320 for the handling of binary data in AJAX.
1) In IE, add the involved sites to the "Local Intranet Zone", and enable "Access data sources across domains" for this zone to overcome the CORS protection otherwise preventing this.
Of course, consider the security consequences involved in this...
2) In javascript (browser), download the file as a blob and POST the resulting data to my own server:
var x = new XMLHttpRequest();
x.onload = function() {
// Create a form
var fd = new FormData();
fd.append('csrfmiddlewaretoken', '{{ csrf_token }}'); // Needed by Django
fd.append('file', x.response); // x.response is a Blob object
// Upload to your server
var y = new XMLHttpRequest();
y.onload = function() {
alert('File uploaded!');
};
y.open('POST', '/django/upload/');
y.send(fd);
};
x.open('GET', 'https://external.url', true);
x.responseType = 'blob'; // <-- This is necessary!
x.send();
3) Finally (in Django view for '/django/upload/'), receive the uploaded data and save as file - or whatever...
filedata = request.FILES['file'].read()
with open('filename', 'wb') as f:
f.write(filedata)
Thanks all, for your comments.
And yes, the real solution would be to overcome the SSO (that requieres the user), so it all could be done by the server itself.
But at least I learned a little about getting/posting binary data using modern XMLHttpRequests. :)
Actually, I had a problem like it, I wanted to download a binary file(an image) and store it and then use it when I need it, So I decided to download it with Fetch API Get call:
const imageAddress = 'an-address-to-my-image.jpg'; // sample address
fetch(imageAddress)
.then(res => res.blob) // <-- This is necessary!
.then(blobFileToBase64)
.then(base64FinalAnswer => console.log(base64FinalAnswer))
The blobFileToBase64 is a helper function that converts blob binary file to a base64 data string:
const blobToBase64 = blob => {
const reader = new FileReader();
reader.readAsDataURL(blob);
return new Promise(resolve => {
reader.onloadend = () => {
resolve(reader.result);
};
});
};
In the end, I have the base64FinalAnswer and I can do anything with it.
Heres the scenario:
User comes to my website and opens a webpage with some javascript functionality.
User edits the data through javascript
User clicks on a save button to save the data, thing is, it seems like they shouldn't need to download this data because its already in javascript on the local machine.
Is it possible to save data from javascript (executing from a foreign webpage) without downloading a file from the server?
Any help would be much appreciated!
For saving data on the client-side, without any server interaction, the best I've seen is Downloadify, is a small JavaScript + Flash library allows you to generate and save files on the fly, directly in the browser...
Check this demo.
I came across this scenario when I wanted to initiate a download without using a server. I wrote this jQuery plugin that wraps up the content of a textarea/div in a Blob, then initiates a download of the Blob. Allows you to specify both file name and type..
jQuery.fn.downld = function (ops) {
this.each(function () {
var _ops = ops || {},
file_name = _ops.name || "downld_file",
file_type = _ops.type || "txt",
file_content = $(this).val() || $(this).html();
var _file = new Blob([file_content],{type:'application/octet-stream'});
window.URL = window.URL || window.webkitURL;
var a = document.createElement('a');
a.href = window.URL.createObjectURL(_file);
a.download = file_name+"."+file_type;
document.body.appendChild(a);
a.click(); $('a').last().remove();
});
}
Default Use : $("#element").downld();
Options : $("#element").downld({ name:"some_file_name", type:"html" });
Codepen example http://codepen.io/anon/pen/cAqzE
JavaScript is run in a sandboxed environment, meaning it only has access to specific browser resources. Specifically, it doesn't have access to the filesystem, or dynamic resources from other domains (web pages, javascript etc). Well, there are other things (I/O, devices), but you get the point.
You will need to post the data to the server which can invoke a file download, or use another technology such as flash, java applets, or silverlight. (i'm not sure about the support for this in the last 2, and I also wouldn't recommend using them, depends what it's for...)
The solution to download local/client-side contents via javascript is not straight forward. I have implemented one solution using smartclient-html-jsp.
Here is the solution:
I am in the project build on SmartClient. We need to download/export data of a grid
(table like structure).
We were using RESTish web services to serve the data from Server side. So I could not hit the url two times; one for grid and second time for export/transform the data to download.
What I did is made two JSPs namely: blank.jsp and export.jsp.
blank.jsp is literally blank, now I need to export the grid data
that I already have on client side.
Now when ever user asks to export the grid data, I do below:
a. Open a new window with url blank.jsp
b. using document.write I create a form in it with one field name text in it and set data to export inside it.
c. Now POST that form to export.jsp of same heirarchy.
d. Contents of export.jsp I am pasting below are self explanatory.
// code start
<%# page import="java.util.*,java.io.*,java.util.Enumeration"%>
<%
response.setContentType ("text/csv");
//set the header and also the Name by which user will be prompted to save
response.setHeader ("Content-Disposition", "attachment;filename=\"data.csv\"");
String contents = request.getParameter ("text");
if (!(contents!= null && contents!=""))
contents = "No data";
else
contents = contents.replaceAll ("NEW_LINE", "\n");
//Open an input stream to the file and post the file contents thru the
//servlet output stream to the client m/c
InputStream in = new ByteArrayInputStream(contents.getBytes ());
ServletOutputStream outs = response.getOutputStream();
int bit = 256;
int i = 0;
try {
while ((bit) >= 0) {
bit = in.read();
outs.write(bit);
}
//System.out.println("" +bit);
} catch (IOException ioe) {
ioe.printStackTrace(System.out);
}
outs.flush();
outs.close();
in.close();
%>
<HTML>
<HEAD>
</HEAD>
<BODY>
<script type="text/javascript">
try {window.close ();} catch (e) {alert (e);}
</script>
</BODY>
</HTML>
// code end
This code is tested and deployed/working in production environment, also this is cross-browser functionality.
Thanks
Shailendra
You can save a small amount of data in cookies.