Force showing the "Save as" dialog box when downloading a file - javascript

Below code does save a file to the user's disk:
function handleSaveImg(event){
const image = canvas.toDataURL();
const saveImg = document.createElement('a');
saveImg.href = image;
saveImg.download= saveAs;
saveImg.click();
}
if(saveMode){
saveMode.addEventListener("click", handleSaveImg);
}
It uses an <a> tag to save some data (in my case, an image exported from a <canvas>).
But this saves directly to the disk, with no prompt asking where to save the file, nor under which name.
I want to force the displaying of the "Save as" dialog box, so that the user has to choose where they'll save that file.
Is there any way?

Yes, and it's called showSaveFilePicker().
This is part of the File System Access API, which is still a draft, but is already exposed in all Chromium browsers.
This API is quite powerful and will give your code direct access to the user's disk, so it is only available in secure contexts.
Once the Promise returned by this method resolve, you'll get access to an handle where you'll be able to access a WritableStream to which you'll be able to write your data.
It's a bit more complicated than download, but it's also a lot more powerful, since you can write as stream, not needing to have the whole data in memory (think recording a video).
In your case this would give
async function handleSaveImg(event){
const image = await new Promise( (res) => canvas.toBlob( res ) );
if( window.showSaveFilePicker ) {
const handle = await showSaveFilePicker();
const writable = await handle.createWritable();
await writable.write( image );
writable.close();
}
else {
const saveImg = document.createElement( "a" );
saveImg.href = URL.createObjectURL( image );
saveImg.download= "image.png";
saveImg.click();
setTimeout(() => URL.revokeObjectURL( saveImg.href ), 60000 );
}
}
Here is a live demo, (and the code).

No.
You can specify a filename by assigning a string to the download property.
There is no way to persuade a browser to show a SaveAs dialog when it is configured to save downloads to a default folder without prompting. That is entirely under the control of the user.

Related

Display file.pdf As a Blob

I am working on different ways of displaying a PDF to get better on a project at work. I am able to insert a url to a PDF into an iframe element and it displays the PDF fine. Sometimes we have a use case where the front end receives a pdf as application/pdf instead of a url in a json object.
For this, I turn the application/pdf into a blob and create a url for the blob. This worked great on everything except Android, so I am testing out some methods with iFrame.
I would like to take this sample pdf http://www.pdf995.com/samples/pdf.pdf, turn it into a blob, and insert the blob url in the src of an iframe element for the purposes of testing blobs as iframe sources on Android Chrome browsers.
function App() {
const samplePdf = "http://www.pdf995.com/samples/pdf.pdf"
const blob = new Blob([samplePdf], { type: 'application/pdf' });
const url = URL.createObjectURL(blob)
return (
<>
<h1>iFrame Rendering of PDF Blob</h1>
<iframe title="pdf" src={url} style={{ height: '1250px', width: '100%' }}></iframe>
</>
);
}
export default App;
This is what renders in the React app
What am I missing to get the content of the pdf to display? React is not a requirement, just seemed an easy way to start a quick practice project.
You need the data to construct the blob while the URL just points to the data you need. Let's go & get it:
const getLocalPdfUrl = async () => {
const url = 'http://www.pdf995.com/samples/pdf.pdf';
const response = await fetch(url);
const blob = await response.blob();
return URL.createObjectURL(blob);
};
This function returns a Promise that will (hopefully) resolve with the URL you can use to construct the iframe. It's async, so, don't forget to wait for the promise to resolve.
Testing note
Fetching external resources from the frontend is restricted by CORS, so, pdf995.com's link will not work. It's also not a trivial task to find a dummy PDF document that would allow fetching itself from the FE.
To test if it works, I'd propose to place the PDF file in the /public folder (or similar) & serve it on the same locslhost as the app.

Read a local text file constantly

I'm a little lost on how I can read a local text file constantly. Nothing is going to a webserver, everything is done locally and its just something basic.
Essentially I have a text file that is getting updated constantly (A different program is writing to the text file), and I want to be able to read that text file. I just want to do something basic where a div gets auto updated with text file content without refreshing the page.
Thanks to the File System Access API we can now keep live links to resources on the disk and request their content when we wish. (Previously, Files only kept a snapshot of a file on disk).
So in modern Chrome we can now request access to a file on disk using the window.showOpenFilePicker method.
This will return a list of handles, from which we will be able to call the getFile() method to get an up-to-date snapshot of the file on disk.
// must come from an user gesture
onclick = async () => {
if( !("showOpenFilePicker" in self) ) {
throw new Error( "unsupported browser" );
}
const handles = await showOpenFilePicker();
setInterval( async () => {
const file = await handles[0].getFile();
document.getElementById( "log" ).textContent = await file.text();
}, 1000 );
};
Since this API is over-protected, it can't run in cross-domain iframes, so here is an outsourced glitch demo, which currently works only in Chrome.

Streaming a client-side generated response as a download, without service worker

Suppose I have a large file I generate client-side that I want to allow the user to save to their hard drive.
The usual method would be to create a Blob, and then create an object URL for it:
const blob = new Blob([chunks], {type: 'application/example'});
linkEl.href = URL.createObjectUrl(blob);
This works, but isn't very efficient as it can quickly exhaust all available memory, since the resulting file has to remain in memory.
A better method would enable streaming. Something like this:
const outputStream = new WritableStream();
linkEl.href = URL.createObjectUrl(outputStream);
while (let chunk = await getChunk()) {
outputStream.write();
}
outputStream.end();
Is there a direct way to do this today?
The only method I've seen for streaming like this is to use a Service Worker. Unfortunately, there are many contexts in which a Service Worker isn't available. Privacy modes may bypass all service workers. Hard-refreshing the page disables them. Opening browser tools can reset the service worker state. The worker can be killed off at any time, and attempts to keep it alive with messaging are not guaranteed to work. All of these hacks have been implemented in an excellent project here: https://github.com/jimmywarting/StreamSaver.js But, at the end of the day, it's unreliable due to these browser constraints.
Does a proper API exist for streaming a "download" client-side without the use of a service worker?
There is one being defined... File System Access.
It's still an early draft and only Chrome has implementing it.
You would be particularly interested in the FileSystemWritableFileStream interface which will allow to write on disk after the user chooses where you can mess with their data ;-)
Non live code since "Sandboxed documents aren't allowed to show a file picker."...
onclick = async () => {
if( !("showSaveFilePicker" in self) ) {
throw new Error( "unsupported browser" );
}
const handle = await showSaveFilePicker();
const filestream = await handle.createWritable();
const writer = await filestream.getWriter();
// here we have a WritableStream, with direct access to the user's disk
await writer.write( "hello" );
await writer.write( " world" );
writer.close();
};
Here is a live glitch poject.

Blob name issue with new tab in chrome and firefox [duplicate]

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.

How to download entire website from inside the website

I'm making a website, in which I want to offer the user to download the whole website (CSS and images included) for them to modify. I know I can download individual resources with
Click Me
but like I said, this only downloads one file, whereas I would like to download the entire website.
If it helps you visualise what I mean: in chrome, IE and Firefox you can press ctrl+s to download the entire website (make sure you save it as Web page, Complete.
Edit: I know I can create a .zip file that it will download, however doing so requires me to update it every time I make a change, which is something I'd rather not do, as I could potentially be making a lot of changes.
As I mention, it is better that you will have a cron job or something like this that once in a while will create you a zip file of all the desired static content.
If you insist doing it in javascript at the client side have a look at JSZip .
You still have to find a way to get the list of static files of the server to save.
For instance, you can create a txt file with each line is a link to a webpage static file.
you will have to iterate over this file and use $.get to get it's content.
something like this:
// Get list of files to save (either by GET request or hardcoded)
filesList = ["f1.json /echo/jsonp?name=1", "inner/f2.json /echo/jsonp?name=2"];
function createZip() {
zip = new JSZip();
// make bunch of requests to get files content
var requests = [];
// for scoping the fileName
_then = (fname) => data => ({ fileName: fname, data });
for (var file of filesList) {
[fileName, fileUrl] = file.split(" ");
requests.push($.get(fileUrl).then(_then(fileName)));
}
// When all finished
$.when(...requests).then(function () {
// Add each result to the zip
for (var arg of arguments) {
zip.file(arg.fileName, JSON.stringify(arg.data));
}
// Save
zip.generateAsync({ type: "blob" })
.then(function (blob) {
saveAs(blob, "site.zip");
});
});
}
$("#saver").click(() => {
createZip();
});
JSFiddle
Personally, I don't like this approach. But do as you prefer.

Categories

Resources