Detect when a browser receives a file download
Asked Answered
U

24

583

I have a page that allows the user to download a dynamically-generated file. It takes a long time to generate, so I'd like to show a "waiting" indicator. The problem is, I can't figure out how to detect when the browser has received the file so that I can hide the indicator.

I'm requesting a hidden form, which POSTs to the server, and targets a hidden iframe for its results. This is, so I don't replace the entire browser window with the result. I listen for a "load" event on the iframe, hoping that it will fire when the download is complete.

I return a "Content-Disposition: attachment" header with the file, which causes the browser to show the "Save" dialog. But the browser doesn't fire a "load" event in the iframe.

One approach I tried is using a multi-part response. So it would send an empty HTML file, as well as the attached downloadable file.

For example:

Content-type: multipart/x-mixed-replace;boundary="abcde"

--abcde
Content-type: text/html

--abcde
Content-type: application/vnd.fdf
Content-Disposition: attachment; filename=foo.fdf

file-content
--abcde

This works in Firefox; it receives the empty HTML file, fires the "load" event, and then shows the "Save" dialog for the downloadable file. But it fails on Internet Explorer and Safari; Internet Explorer fires the "load" event, but it doesn't download the file, and Safari downloads the file (with the wrong name and content-type) and doesn't fire the "load" event.

A different approach might be to call to start the file creation, poll the server until it's ready, and then download the already-created file. But I'd rather avoid creating temporary files on the server.

What should I do?

Unconstitutional answered 9/7, 2009 at 20:51 Comment(7)
No version of IE supports multipart/x-mixed-replace.Garthgartner
Thanks Eric -- that's good to know. I won't waste any more time with that approach.Unconstitutional
Only reliable way seems to be server push notification (SignalR for ASP.NET folks).Roseberry
bennadel.com/blog/… -- this is a simple solutionCorella
I wish browser makers would just make it more obvious that a request was in progress.Unaneled
cant you use $.ajax().done()?Hanky
If I want to use a multipart approach will that require special processing in the client? Or the html part will go to the form and attachments will be downoaded by the Browser?Moa
P
507

One possible solution uses JavaScript on the client.

The client algorithm:

  1. Generate a random unique token.
  2. Submit the download request, and include the token in a GET/POST field.
  3. Show the "waiting" indicator.
  4. Start a timer, and every second or so, look for a cookie named "fileDownloadToken" (or whatever you decide).
  5. If the cookie exists, and its value matches the token, hide the "waiting" indicator.

The server algorithm:

  1. Look for the GET/POST field in the request.
  2. If it has a non-empty value, drop a cookie (e.g. "fileDownloadToken"), and set its value to the token's value.

Client source code (JavaScript):

function getCookie( name ) {
  var parts = document.cookie.split(name + "=");
  if (parts.length == 2) return parts.pop().split(";").shift();
}

function expireCookie( cName ) {
    document.cookie = 
        encodeURIComponent(cName) + "=deleted; expires=" + new Date( 0 ).toUTCString();
}

function setCursor( docStyle, buttonStyle ) {
    document.getElementById( "doc" ).style.cursor = docStyle;
    document.getElementById( "button-id" ).style.cursor = buttonStyle;
}

function setFormToken() {
    var downloadToken = new Date().getTime();
    document.getElementById( "downloadToken" ).value = downloadToken;
    return downloadToken;
}

var downloadTimer;
var attempts = 30;

// Prevents double-submits by waiting for a cookie from the server.
function blockResubmit() {
    var downloadToken = setFormToken();
    setCursor( "wait", "wait" );

    downloadTimer = window.setInterval( function() {
        var token = getCookie( "downloadToken" );

        if( (token == downloadToken) || (attempts == 0) ) {
            unblockSubmit();
        }

        attempts--;
    }, 1000 );
}

function unblockSubmit() {
  setCursor( "auto", "pointer" );
  window.clearInterval( downloadTimer );
  expireCookie( "downloadToken" );
  attempts = 30;
}

Example server code (PHP):

$TOKEN = "downloadToken";

// Sets a cookie so that when the download begins the browser can
// unblock the submit button (thus helping to prevent multiple clicks).
// The false parameter allows the cookie to be exposed to JavaScript.
$this->setCookieToken( $TOKEN, $_GET[ $TOKEN ], false );

$result = $this->sendFile();

Where:

public function setCookieToken(
    $cookieName, $cookieValue, $httpOnly = true, $secure = false ) {

    // See: https://mcmap.net/q/55813/-php-_server-39-http_host-39-vs-_server-39-server_name-39-am-i-understanding-the-manual-pages-correctly
    // See: http://shiflett.org/blog/2006/mar/server-name-versus-http-host
    // See: https://mcmap.net/q/56417/-set-a-cookie-to-never-expire
    setcookie(
        $cookieName,
        $cookieValue,
        2147483647,            // expires January 1, 2038
        "/",                   // your path
        $_SERVER["HTTP_HOST"], // your domain
        $secure,               // Use true over HTTPS
        $httpOnly              // Set true for $AUTH_COOKIE_NAME
    );
}
Pyrotechnics answered 12/11, 2010 at 20:54 Comment(19)
Awesome idea, I used it as a basic framework for this answer about downloading multiple files with jQuery/C#Insured
alas, not possible with files on s3 since you cannot set cookies.Smoothtongued
A heads up for others: if document.cookies does not include the downloadToken, check the cookie path. In my case, I had to set the path to "/" on server side (e.g. cookie.setPath("/") in Java) even though path was blank by default. For some time I thought the issue was the special 'localhost' domain cookie handling (#1134790) but that wasn't the issue in the end. May be that for others though so worth the read.Galvez
@Pyrotechnics before going into more depth with your solution, I wonder if it will work with a cross domain file download request. Do you think that it will, or cookies restrictions will compromise it?Aerometry
Brilliant - it wouldn't have occurred to me in 100 years that you could include cookies as part of a file download. Thank you!!Babu
@Galvez What if I cannot control the cookie path , and it is set to same domain, but a different subpath than my current path. Is there anything other than cookies I can check?Dotty
Can a cookie be written by a download process? I thought the cookie is only sent with the headers and the headers are already written before the writing to output startsUnglue
@Unglue I think idea is in waiting for download start, not for download end. Your server renders content in 30 seconds => we need to show some loader for this 30 seconds of waiting. When download starts browser handles downloading progress bar.Lanthanum
I read the solution is setting a cookie at the end of downloadUnglue
instead of submitting the token in form, it can be sent as a cookie. In the server you could do a known operation (example multiply by 100) to the token and poll cookie value to be changed to the new value in the client.Pili
I did this using TypeScript and an ASPX file used for downloading and it works flawlessly in Chrome and IE. Thank you!!!Rafat
Really good idea. We tried to use it for file download confirmation (we need it for legal reasons) and found out that this soluton indicates that file was downloaded when user presses Cancel in download dialog box at least at Chrome. This happens (probably) because Chrome downloads file in the background when dialog box is open. That is, cookie is set even when file download is canceled. For most cases this might be inessential, but you should be aware of this.Playboy
As others have pointed out, this solution only solves part of the problem, the waiting for the server to prepare the file time. The other part of the problem, which can be considerable depending on the size of the file and the connection speed, is how long it takes to actually get the whole file on the client. And that is not solved with this solution.Condescending
I've tried this approach and it works great! I just want to suggest js-cookie for more convenient work with cookies.Airsick
How does his JavaScript get called? He defines some functions and two vars, but they never seem start running. Also, it looks like the JS needs to get some IDs on the page ("document.getElementById( "downloadToken" ).value = downloadToken;") but he doesn't include the HTML to show how used. I have this problem and need this to work.Pellegrino
My cookie wasn't being deleted, so I've removed encodeURIComponent from the equation and that solved the issue (Chrome 65)Dell
Actually, this approach doesn't work when your download server is on another domain (say api.foo.com) from your app (say app.bar.com), because the cookie that the server sets will not be readable by the client app.Dell
the reference link to the possible solution seems to be dead.. can anyone provide me the new link?Asynchronism
In Chrome, pressing the ESC will cancel the request in the browser, then you are stuck for the remainder 30sLessielessing
S
30

The core problem is that the web browser does not have an event that fires when page navigation is cancelled but does have an event that fires when a page completes loading. Anything outside of a direct browser event is going to be a hack with pros and cons.

There are four known approaches to dealing with detecting when a browser download starts:

  1. Call fetch(), retrieve the entire response, attach an a tag with a download attribute, and trigger a click event. Modern web browsers will then offer the user the option to save the already retrieved file. There are several downsides with this approach:
  • The entire data blob is stored in RAM, so if the file is large, it will consume that much RAM. For small files, this probably isn't a deal breaker.
  • The user has to wait for the entire file to download before they can save it. They also can't leave the page until it completes.
  • The built-in web browser file downloader is not used.
  • A cross-domain fetch will probably fail unless CORS headers are set.
  1. Use an iframe + a server-side cookie. The iframe fires a load event if a page loads in the iframe instead of starting a download but it does not fire any events if the download starts. Setting a cookie with the web server can then be detected by JavaScript in a loop. There are several downsides with this approach:
  • The server and client have to work in concert. The server has to set a cookie. The client has to detect the cookie.
  • Cross-domain requests won't be able to set the cookie.
  • There are limits to how many cookies can be set per domain.
  • Can't send custom HTTP headers.
  1. Use an iframe with URL redirection. The iframe starts a request and once the server has prepared the file, it dumps a HTML document that performs a meta refresh to a new URL, which triggers the download 1 second later. The load event on the iframe happens when the HTML document loads. There are several downsides with this approach:
  • The server has to maintain storage for the content being downloaded. Requires a cron job or similar to regularly clean up the directory.
  • The server has to dump out special HTML content when the file is ready.
  • The client has to guess as to when the iframe has actually made the second request to the server and when the download has actually started before removing the iframe from the DOM. This could be overcome by just leaving the iframe in the DOM.
  • Can't send custom HTTP headers.
  1. Use an iframe + XHR. The iframe triggers the download request. As soon as the request is made via the iframe, an identical request via XHR is made. If the load event on the iframe fires, an error has occurred, abort the XHR request, and remove the iframe. If a XHR progress event fires, then downloading has probably started in the iframe, abort the XHR request, wait a few seconds, and then remove the iframe. This allows for larger files to be downloaded without relying on a server-side cookie. There are several downsides with this approach:
  • There are two separate requests made for the same information. The server can distinguish the XHR from the iframe by checking the incoming headers.
  • A cross-domain XHR request will probably fail unless CORS headers are set. However, the browser won't know if CORS is allowed or not until the server sends back the HTTP headers. If the server waits to send headers until the file data is ready, the XHR can roughly detect when the iframe has started to download even without CORS.
  • The client has to guess as to when the download has actually started to remove the iframe from the DOM. This could be overcome by just leaving the iframe in the DOM.
  • Can't send custom headers on the iframe.

Without an appropriate built-in web browser event, there aren't any perfect solutions here. However, one of the four methods above will likely be a better fit than the others depending on your use-case.

Whenever possible, stream responses to the client on the fly instead of generating everything first on the server and then sending the response. Various file formats can be streamed such as CSV, JSON, XML, ZIP, etc. It really depends on finding a library that supports streaming content. When streaming the response as soon as the request starts, detecting the start of the download won't matter as much because it will start almost right away.

Another option is to just output the download headers up front instead of waiting for all of the content to be generated first. Then generate the content and finally start sending to the client. The user's built-in downloader will patiently wait until the data starts arriving. The downside is that the underlying network connection could timeout waiting for data to start flowing (either on the client or server side).

Scold answered 20/6, 2020 at 15:30 Comment(1)
excellent answer mate, thanks for listing all the downside of each solution clearly, very nicely put.Frederigo
M
29

A very simple (and lame) one line solution is to use the window.onblur() event to close the loading dialog. Of course, if it takes too long and the user decides to do something else (like reading emails) the loading dialog will close.

Melodee answered 4/6, 2011 at 17:25 Comment(5)
This is a simple approach which is ideal for getting rid of a loading overlay for a file download which was triggered using onbeforeunload Thank you.Yarrow
This doesn't work in all browsers (some do not leave/blur the current window as part of the download workflow, e.g. Safari, some IE versions, etc).Vannie
Chrome and other such browsers auto-download the files where this condition will fail.Pedant
@Pedant that is only by default. It is entirely possible a user of Chrome will specify where downloads should be saved and hence see the dialog boxTalkative
bad idea because you activate the blur on tabchange, or any action outside the windowFrost
C
21

This solution is very simple, yet reliable. And it makes it possible to display real progress messages (and can be easily plugged in to existing processes):

The script that processes (my problem was: retrieving files via HTTP and deliver them as ZIP) writes the status to the session.

The status is polled and displayed every second. That’s all (OK, it’s not. You have to take care of a lot of details (for example, concurrent downloads), but it’s a good place to start.

The download page:

<a href="download.php?id=1" class="download">DOWNLOAD 1</a>
<a href="download.php?id=2" class="download">DOWNLOAD 2</a>

...

<div id="wait">
    Please wait...
    <div id="statusmessage"></div>
</div>

<script>

    // This is jQuery
    $('a.download').each(function()
    {
        $(this).click(
            function() {
                $('#statusmessage').html('prepare loading...');
                $('#wait').show();
                setTimeout('getstatus()', 1000);
            }
            );
        });
    });

    function getstatus() {
        $.ajax({
            url: "/getstatus.php",
            type: "POST",
            dataType: 'json',
            success: function(data) {
                $('#statusmessage').html(data.message);
                if(data.status == "pending")
                    setTimeout('getstatus()', 1000);
                else
                    $('#wait').hide();
                }
        });
    }
</script>

File getstatus.php

<?php
    session_start();
    echo json_encode($_SESSION['downloadstatus']);
?>

File download.php

<?php
    session_start();
    $processing = true;
    while($processing) {
        $_SESSION['downloadstatus'] = array("status" =>"pending", "message" => "Processing".$someinfo);
        session_write_close();
        $processing = do_what_has_2Bdone();
        session_start();
    }

    $_SESSION['downloadstatus'] = array("status" => "finished", "message" => "Done");
    // And spit the generated file to the browser
?>
Candescent answered 10/9, 2010 at 22:16 Comment(5)
but if the user has multiple windows or downloads open? also you get here a redundant call to the serverSialkot
If you have multiple connections from one user they will be all waiting for other connections to end because session_start() locks session for user and prevents all other processes to access it.Selfsealing
you don't need to use .each() for event registrations. just say $('a.download').click()Vedette
Don't eval code inside setTimeout('getstatus()', 1000);. Use the fn directly: setTimeout(getstatus, 1000);Hughhughes
session_start(): Cannot start session when headers already sentExaction
L
19

Based on Elmer's example, I've prepared my own solution. After clicking on an item with a defined "download" class, a custom message is displayed in the browser window. I used the focus trigger to hide the message. I've used the focus trigger to hide the message.

JavaScript

$(function(){$('.download').click(function() { ShowDownloadMessage(); }); })

function ShowDownloadMessage()
{
     $('#message-text').text('Your report is creating. Please wait...');
     $('#message').show();
     window.addEventListener('focus', HideDownloadMessage, false);
}

function HideDownloadMessage(){
    window.removeEventListener('focus', HideDownloadMessage, false);                   
    $('#message').hide();
}

HTML

<div id="message" style="display: none">
    <div id="message-screen-mask" class="ui-widget-overlay ui-front"></div>
    <div id="message-text" class="ui-dialog ui-widget ui-widget-content ui-corner-all ui-front ui-draggable ui-resizable waitmessage">please wait...</div>
</div>

Now you should implement any element to download:

<a class="download" href="file://www.ocelot.com.pl/prepare-report">Download report</a>

or

<input class="download" type="submit" value="Download" name="actionType">

After each download click you will see the message:
Your report is creating. Please wait...

Lum answered 15/10, 2014 at 13:25 Comment(9)
What if the user clicks the window?Pirandello
this is exactly what I was looking for, thaks a lot!!Buckjump
The hide() is not getting called in my caseGauge
Great. Worked for me for a pdf download with only some line of codesHokkaido
Great. Thank you very much !Monosyllabic
My case is working on JSP and click to download csv. It works. Thanks.Vinegarette
The sentence starting with "After elements click" is incomprehensible. Can you fix it?Bounteous
The hide() part is not working in my case if I trigger the click event from JS code.Buryat
Initially I upvoted this because it worked great when combined with a "Sweet Alert2". What I've come to find out though is that if the user's "download location" is set to automatically download to a specific folder instead of asking where to go, it seems to not fire the focus event needed to close my modal. Unfortunately still looking for a solution.Quintana
I
12

I use the following to download blobs and revoke the object URL after the download. It works in Chrome and Firefox!

function download(blob){
    var url = URL.createObjectURL(blob);
    console.log('create ' + url);

    window.addEventListener('focus', window_focus, false);
    function window_focus(){
        window.removeEventListener('focus', window_focus, false);
        URL.revokeObjectURL(url);
        console.log('revoke ' + url);
    }
    location.href = url;
}

After the file download dialog is closed, the window gets its focus back, so the focus event is triggered.

Intelligence answered 26/3, 2013 at 13:46 Comment(2)
Still has the issue of switching window and returning which will cause the modal to hide.Roseberry
Browsers like Chrome that download into the bottom tray never blur/refocus the window.Secession
G
10

A solution from elsewhere that worked:

/**
 *  download file, show modal
 *
 * @param uri link
 * @param name file name
 */
function downloadURI(uri, name) {
// <------------------------------------------       Do something (show loading)
    fetch(uri)
        .then(resp => resp.blob())
        .then(blob => {
            const url = window.URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.style.display = 'none';
            a.href = url;
            // the filename you want
            a.download = name;
            document.body.appendChild(a);
            a.click();
            window.URL.revokeObjectURL(url);
            // <----------------------------------------  Detect here (hide loading)
            alert('File detected');
            a.remove(); // remove element
        })
        .catch(() => alert('An error sorry'));
}

You can use it:

downloadURI("www.linkToFile.com", "file.name");

Gean answered 13/5, 2020 at 15:48 Comment(2)
Works but basically transforms data to Base64 on the memory before reconverting to binary and downloading. Not recommended for large filesKongo
How can I set the final downloaded filename to the filename which is fetched from url?Skysail
L
9

I faced the same problem with that configuration:

My solution with a cookie:

Client side:

When submitting your form, call your JavaScript function to hide your page and load your waiting spinner

function loadWaitingSpinner() {
    ... hide your page and show your spinner ...
}

Then, call a function that will check every 500 ms whether a cookie is coming from server.

function checkCookie() {
    var verif = setInterval(isWaitingCookie, 500, verif);
}

If the cookie is found, stop checking every 500 ms, expire the cookie and call your function to come back to your page and remove the waiting spinner (removeWaitingSpinner()). It is important to expire the cookie if you want to be able to download another file again!

function isWaitingCookie(verif) {
    var loadState = getCookie("waitingCookie");
    if (loadState == "done") {
        clearInterval(verif);
        document.cookie = "attenteCookie=done; expires=Tue, 31 Dec 1985 21:00:00 UTC;";
        removeWaitingSpinner();
    }
}

function getCookie(cookieName) {
    var name = cookieName + "=";
    var cookies = document.cookie
    var cs = cookies.split(';');
    for (var i = 0; i < cs.length; i++) {
        var c = cs[i];
        while(c.charAt(0) == ' ') {
            c = c.substring(1);
        }
        if (c.indexOf(name) == 0) {
            return c.substring(name.length, c.length);
        }
    }
    return "";
}

function removeWaitingSpinner() {
    ... come back to your page and remove your spinner ...
}

Server side:

At the end of your server process, add a cookie to the response. That cookie will be sent to the client when your file will be ready for download.

Cookie waitCookie = new Cookie("waitingCookie", "done");
response.addCookie(waitCookie);
Lithe answered 6/3, 2019 at 15:2 Comment(1)
Should this line document.cookie = "attenteCookie=done; expires=Tue, 31 Dec 1985 21:00:00 UTC;"; where it is expiring the cookie, be waitingCookie instead of attenteCookie ?Petigny
I
8

I wrote a simple JavaScript class that implements a technique similar to the one described in bulltorious' answer. I hope it can be useful to someone here.

The GitHub project is called response-monitor.js.

By default it uses spin.js as the waiting indicator, but it also provides a set of callbacks for implementation of a custom indicator.

jQuery is supported, but not required.

Notable features

  • Simple integration
  • No dependencies
  • jQuery plug-in (optional)
  • Spin.js Integration (optional)
  • Configurable callbacks for monitoring events
  • Handles multiple simultaneous requests
  • Server-side error detection
  • Timeout detection
  • Cross browser

Example usage

HTML

<!-- The response monitor implementation -->
<script src="response-monitor.js"></script>

<!-- Optional jQuery plug-in -->
<script src="response-monitor.jquery.js"></script>

<a class="my_anchors" href="/report?criteria1=a&criteria2=b#30">Link 1 (Timeout: 30s)</a>
<a class="my_anchors" href="/report?criteria1=b&criteria2=d#10">Link 2 (Timeout: 10s)</a>

<form id="my_form" method="POST">
    <input type="text" name="criteria1">
    <input type="text" name="criteria2">
    <input type="submit" value="Download Report">
</form>

Client (plain JavaScript)

// Registering multiple anchors at once
var my_anchors = document.getElementsByClassName('my_anchors');
ResponseMonitor.register(my_anchors); // Clicking on the links initiates monitoring

// Registering a single form
var my_form = document.getElementById('my_form');
ResponseMonitor.register(my_form); // The submit event will be intercepted and monitored

Client (jQuery)

$('.my_anchors').ResponseMonitor();
$('#my_form').ResponseMonitor({timeout: 20});

Client with callbacks (jQuery)

// When options are defined, the default spin.js integration is bypassed
var options = {
    onRequest: function(token) {
        $('#cookie').html(token);
        $('#outcome').html('');
        $('#duration').html('');
    },
    onMonitor: function(countdown) {
        $('#duration').html(countdown);
    },
    onResponse: function(status) {
        $('#outcome').html(status==1 ? 'success' : 'failure');
    },
    onTimeout: function() {
        $('#outcome').html('timeout');
    }
};

// Monitor all anchors in the document
$('a').ResponseMonitor(options);

Server (PHP)

$cookiePrefix = 'response-monitor'; // Must match the one set on the client options
$tokenValue = $_GET[$cookiePrefix];
$cookieName = $cookiePrefix.'_'.$tokenValue; // Example: response-monitor_1419642741528

// This value is passed to the client through the ResponseMonitor.onResponse callback
$cookieValue = 1; // For example, "1" can interpret as success and "0" as failure

setcookie(
    $cookieName,
    $cookieValue,
    time() + 300,          // Expire in 5 minutes
    "/",
    $_SERVER["HTTP_HOST"],
    true,
    false
);

header('Content-Type: text/plain');
header("Content-Disposition: attachment; filename=\"Response.txt\"");

sleep(5); // Simulate whatever delays the response
print_r($_REQUEST); // Dump the request in the text file

For more examples, check the examples folder in the repository.

Intercurrent answered 29/12, 2014 at 13:12 Comment(0)
J
8

If you're streaming a file that you're generating dynamically, and also have a realtime server-to-client messaging library implemented, you can alert your client pretty easily.

The server-to-client messaging library I like and recommend is Socket.io (via Node.js). After your server script is done generating the file that is being streamed for download your last line in that script can emit a message to Socket.io which sends a notification to the client. On the client, Socket.io listens for incoming messages emitted from the server and allows you to act on them. The benefit of using this method over others is that you are able to detect a "true" finish event after the streaming is done.

For example, you could show your busy indicator after a download link is clicked, stream your file, emit a message to Socket.io from the server in the last line of your streaming script, listen on the client for a notification, receive the notification and update your UI by hiding the busy indicator.

I realize most people reading answers to this question might not have this type of a setup, but I've used this exact solution to great effect in my own projects and it works wonderfully.

Socket.io is incredibly easy to install and use. See more: http://socket.io/

Jylland answered 6/11, 2016 at 16:31 Comment(0)
D
5

I had a real struggle with this exact problem, but I found a viable solution using iframes (I know, I know. It's terrible, but it works for a simple problem that I had.)

I had an HTML page that launched a separate PHP script that generated the file and then downloaded it. On the HTML page, I used the following jQuery code in the html header (you'll need to include a jQuery library as well):

<script>
    $(function(){
        var iframe = $("<iframe>", {name: 'iframe', id: 'iframe',}).appendTo("body").hide();
        $('#click').on('click', function(){
            $('#iframe').attr('src', 'your_download_script.php');
        });
        $('iframe').load(function(){
            $('#iframe').attr('src', 'your_download_script.php?download=yes'); <!-- On first iframe load, run script again but download file instead -->
            $('#iframe').unbind(); <!-- Unbinds the iframe. Helps prevent against infinite recursion if the script returns valid html (such as echoing out exceptions) -->
        });
    });
</script>

In file your_download_script.php, have the following:

function downloadFile($file_path) {
    if (file_exists($file_path)) {
        header('Content-Description: File Transfer');
        header('Content-Type: text/csv');
        header('Content-Disposition: attachment; filename=' . basename($file_path));
        header('Expires: 0');
        header('Cache-Control: must-revalidate');
        header('Pragma: public');
        header('Content-Length: ' . filesize($file_path));
        ob_clean();
        flush();
        readfile($file_path);
        exit();
    }
}

$_SESSION['your_file'] = path_to_file; // This is just how I chose to store the filepath

if (isset($_REQUEST['download']) && $_REQUEST['download'] == 'yes') {
    downloadFile($_SESSION['your_file']);
} else {
    // Execute logic to create the file
}

To break this down, jQuery first launches your PHP script in an iframe. The iframe is loaded once the file is generated. Then jQuery launches the script again with a request variable telling the script to download the file.

The reason that you can't do the download and file generation all in one go is due to the php header() function. If you use header(), you're changing the script to something other than a web page and jQuery will never recognize the download script as being 'loaded'. I know this may not necessarily be detecting when a browser receives a file, but your issue sounded similar to mine.

Dorr answered 9/12, 2014 at 17:30 Comment(0)
A
4

When the user triggers the generation of the file, you could simply assign a unique ID to that "download", and send the user to a page which refreshes (or checks with AJAX) every few seconds. Once the file is finished, save it under that same unique ID and...

  • If the file is ready, do the download.
  • If the file is not ready, show the progress.

Then you can skip the whole iframe/waiting/browserwindow mess, yet have a really elegant solution.

Ashil answered 9/7, 2009 at 21:37 Comment(2)
That sounds like the temporary-file approach I mentioned above. I might do something like this if it turns out my idea is impossible, but I was hoping to avoid it.Unconstitutional
ajax cant be used to download file most of the time, especially if you are using framework like flaskBobodioulasso
C
3

If you don't want to generate and store the file on the server, are you willing to store the status, e.g. file-in-progress, file-complete? Your "waiting" page could poll the server to know when the file generation is complete. You wouldn't know for sure that the browser started the download but you'd have some confidence.

Cassilda answered 9/7, 2009 at 21:48 Comment(0)
A
3

In my experience, there are two ways to handle this:

  1. Set a short-lived cookie on the download, and have JavaScript continually check for its existence. Only real issue is getting the cookie lifetime right - too short and the JavaScript can miss it, too long and it might cancel the download screens for other downloads. Using JavaScript to remove the cookie upon discovery usually fixes this.
  2. Download the file using fetch/XHR. Not only do you know exactly when the file download finishes, if you use XHR you can use progress events to show a progress bar! Save the resulting blob with msSaveBlob in Internet Explorer or Edge and a download link (like this one) in Firefox and Chrome. The problem with this method is that iOS Safari doesn't seem to handle downloading blobs right - you can convert the blob into a data URL with a FileReader and open that in a new window, but that's opening the file, not saving it.
Argosy answered 9/11, 2019 at 1:57 Comment(0)
E
2

I just had this exact same problem. My solution was to use temporary files since I was generating a bunch of temporary files already. The form is submitted with:

var microBox = {
    show : function(content) {
        $(document.body).append('<div id="microBox_overlay"></div><div id="microBox_window"><div id="microBox_frame"><div id="microBox">' +
        content + '</div></div></div>');
        return $('#microBox_overlay');
    },

    close : function() {
        $('#microBox_overlay').remove();
        $('#microBox_window').remove();
    }
};

$.fn.bgForm = function(content, callback) {
    // Create an iframe as target of form submit
    var id = 'bgForm' + (new Date().getTime());
    var $iframe = $('<iframe id="' + id + '" name="' + id + '" style="display: none;" src="about:blank"></iframe>')
        .appendTo(document.body);
    var $form = this;
    // Submittal to an iframe target prevents page refresh
    $form.attr('target', id);
    // The first load event is called when about:blank is loaded
    $iframe.one('load', function() {
        // Attach listener to load events that occur after successful form submittal
        $iframe.load(function() {
            microBox.close();
            if (typeof(callback) == 'function') {
                var iframe = $iframe[0];
                var doc = iframe.contentWindow.document;
                var data = doc.body.innerHTML;
                callback(data);
            }
        });
    });

    this.submit(function() {
        microBox.show(content);
    });

    return this;
};

$('#myForm').bgForm('Please wait...');

At the end of the script that generates the file I have:

header('Refresh: 0;url=fetch.php?token=' . $token);
echo '<html></html>';

This will cause the load event on the iframe to be fired. Then the wait message is closed and the file download will then start. It was tested on Internet Explorer 7 and Firefox.

Ezzo answered 21/7, 2009 at 22:41 Comment(0)
B
2

You can rely on the browser's cache and trigger a second download of the same file when the file is loaded to the cache.

$('#link').click(function(e) {
    e.preventDefault();

    var url = $(this).attr('href');
    var request = new XMLHttpRequest();
    request.responseType = "blob";
    request.open("GET", url);

    var self = this;
    request.onreadystatechange = function () {
        if (request.readyState === 4) {
            var file = $(self).data('file');
            var anchor = document.createElement('a');
            anchor.download = file;
            console.log(file);
            console.log(request);
            anchor.href = window.URL.createObjectURL(request.response);
            anchor.click();
            console.log('Completed. Download window popped up.');
        }
    };
    request.send();
});
Bremble answered 14/7, 2021 at 20:24 Comment(0)
D
1

If you have downloaded a file, which is saved, as opposed to being in the document, there isn't any way to determine when the download is complete, since it is not in the scope of the current document, but a separate process in the browser.

Dragnet answered 9/7, 2009 at 20:54 Comment(2)
I should clarify -- I"m not too concerned with when the download completes. If I can just identify when the download starts, that would be enough.Unconstitutional
you need to read the title as OP saysBobodioulasso
D
0

The question is to have a ‘waiting’ indicator while a file is generated and then return to normal once the file is downloading. The way I like to do this is using a hidden iFrame and hook the frame’s onload event to let my page know when download starts.

But onload does not fire in Internet Explorer for file downloads (like with the attachment header token). Polling the server works, but I dislike the extra complexity. So here is what I do:

  • Target the hidden iFrame as usual.
  • Generate the content. Cache it with an absolute timeout in 2 minutes.
  • Send a JavaScript redirect back to the calling client, essentially calling the generator page a second time. Note: this will cause the onload event to fire in Internet Explorer because it's acting like a regular page.
  • Remove the content from the cache and send it to the client.

Disclaimer: Don’t do this on a busy site, because the caching could add up. But really, if your sites are that busy, the long running process will starve you of threads anyway.

Here is what the code-behind looks like, which is all you really need.

public partial class Download : System.Web.UI.Page
{
    protected System.Web.UI.HtmlControls.HtmlControl Body;

    protected void Page_Load( object sender, EventArgs e )
    {
        byte[ ] data;
        string reportKey = Session.SessionID + "_Report";

        // Check is this page request to generate the content
        //    or return the content (data query string defined)
        if ( Request.QueryString[ "data" ] != null )
        {
            // Get the data and remove the cache
            data = Cache[ reportKey ] as byte[ ];
            Cache.Remove( reportKey );

            if ( data == null )
                // send the user some information
                Response.Write( "Javascript to tell user there was a problem." );
            else
            {
                Response.CacheControl = "no-cache";
                Response.AppendHeader( "Pragma", "no-cache" );
                Response.Buffer = true;

                Response.AppendHeader( "content-disposition", "attachment; filename=Report.pdf" );
                Response.AppendHeader( "content-size", data.Length.ToString( ) );
                Response.BinaryWrite( data );
            }
            Response.End();
        }
        else
        {
            // Generate the data here. I am loading a file just for an example
            using ( System.IO.FileStream stream = new System.IO.FileStream( @"C:\1.pdf", System.IO.FileMode.Open ) )
                using ( System.IO.BinaryReader reader = new System.IO.BinaryReader( stream ) )
                {
                    data = new byte[ reader.BaseStream.Length ];
                    reader.Read( data, 0, data.Length );
                }

            // Store the content for retrieval
            Cache.Insert( reportKey, data, null, DateTime.Now.AddMinutes( 5 ), TimeSpan.Zero );

            // This is the key bit that tells the frame to reload this page
            //   and start downloading the content. NOTE: Url has a query string
            //   value, so that the content isn't generated again.
            Body.Attributes.Add("onload", "window.location = 'binary.aspx?data=t'");
        }
    }
Dixil answered 13/9, 2009 at 4:0 Comment(0)
C
0

A quick solution if you only want to display a message or a loader GIF image until the download dialog is displayed is to put the message in a hidden container and when you click on the button that generate the file to be downloaded, you make the container visible.

Then use jQuery or JavaScript to catch the focusout event of the button to hide the container that contain the message.

Conviction answered 6/8, 2017 at 9:50 Comment(0)
R
0

If XMLHttpRequest with a blob is not an option then you can open your file in a new window and check if any elements get populated in that window body with interval.

var form = document.getElementById("frmDownlaod");
form.setAttribute("action", "downoad/url");
form.setAttribute("target", "downlaod");
var exportwindow = window.open("", "downlaod", "width=800,height=600,resizable=yes");
form.submit();

var responseInterval = setInterval(function() {
    var winBody = exportwindow.document.body
    if(winBody.hasChildNodes()) // Or 'downoad/url' === exportwindow.document.location.href
    {
        clearInterval(responseInterval);
        // Do your work.
        // If there is an error page configured in your application
        // for failed requests, check for those DOM elements.
    }
}, 1000)
// Better if you specify the maximum number of intervals
Rake answered 3/12, 2017 at 18:10 Comment(0)
C
0

This Java/Spring example detects the end of a download, at which point it hides the "Loading..." indicator.

Approach: On the JavaScript side, set a cookie with a maximum expiration age of 2 minutes, and poll every second for cookie expiration. Then the server-side overrides this cookie with an earlier expiration age -- the completion of the server process. As soon as the cookie expiration is detected in the JavaScript polling, "Loading..." is hidden.

JavaScript Side

function buttonClick() { // Suppose this is the handler for the button that starts
    $("#loadingProgressOverlay").show();  // Show loading animation
    startDownloadChecker("loadingProgressOverlay", 120);
    // Here you launch the download URL...
    window.location.href = "myapp.com/myapp/download";
}

// This JavaScript function detects the end of a download.
// It does timed polling for a non-expired Cookie, initially set on the
// client-side with a default max age of 2 min.,
// but then overridden on the server-side with an *earlier* expiration age
// (the completion of the server operation) and sent in the response.
// Either the JavaScript timer detects the expired cookie earlier than 2 min.
// (coming from the server), or the initial JavaScript-created cookie expires after 2 min.
function startDownloadChecker(imageId, timeout) {

    var cookieName = "ServerProcessCompleteChecker";  // Name of the cookie which is set and later overridden on the server
    var downloadTimer = 0;  // Reference to the timer object

    // The cookie is initially set on the client-side with a specified default timeout age (2 min. in our application)
    // It will be overridden on the server side with a new (earlier) expiration age (the completion of the server operation),
    // or auto-expire after 2 min.
    setCookie(cookieName, 0, timeout);

    // Set a timer to check for the cookie every second
    downloadTimer = window.setInterval(function () {

        var cookie = getCookie(cookieName);

        // If cookie expired (NOTE: this is equivalent to cookie "doesn't exist"), then clear "Loading..." and stop polling
        if ((typeof cookie === 'undefined')) {
            $("#" + imageId).hide();
            window.clearInterval(downloadTimer);
        }

    }, 1000); // Every second
}

// These are helper JavaScript functions for setting and retrieving a Cookie
function setCookie(name, value, expiresInSeconds) {
    var exdate = new Date();
    exdate.setTime(exdate.getTime() + expiresInSeconds * 1000);
    var c_value = escape(value) + ((expiresInSeconds == null) ? "" : "; expires=" + exdate.toUTCString());
    document.cookie = name + "=" + c_value + '; path=/';
}

function getCookie(name) {
    var parts = document.cookie.split(name + "=");
    if (parts.length == 2 ) {
        return parts.pop().split(";").shift();
    }
}

Java/Spring Server Side

    @RequestMapping("/download")
    public String download(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //... Some logic for downloading, returning a result ...

        // Create a Cookie that will override the JavaScript-created
        // Max-Age-2min Cookie with an earlier expiration (same name)
        Cookie myCookie = new Cookie("ServerProcessCompleteChecker", "-1");
        myCookie.setMaxAge(0); // This is immediate expiration, but can also
                               // add +3 seconds for any flushing concerns
        myCookie.setPath("/");
        response.addCookie(myCookie);
        //... -- presumably the download is writing to the Output Stream...
        return null;
}
Cornucopia answered 2/1, 2020 at 15:33 Comment(5)
The cookie is created by the JS script but it's not updated by the controller, it maintains the original value (0), how can I update the cookie value without refreshing the page ?Elapse
That's strange - can you ensure the name is exactly correct? It will overwrite the cookie if the name matches. Let me knowCornucopia
The original value is not 0. The original value set in JS is 2 min. The NEW value that the server is supposed to modify with is 0.Cornucopia
Also, are you doing this: myCookie.setPath("/"); response.addCookie(myCookie);Cornucopia
I figured out (for some reason), that I should add cookies before doing response.getOutputStream(); (getting response output stream to append download files), it was not taken into account when I did it after that stepElapse
L
0

PrimeFaces uses cookie polling, too.

monitorDownload():

    monitorDownload: function(start, complete, monitorKey) {
        if(this.cookiesEnabled()) {
            if(start) {
                start();
            }

            var cookieName = monitorKey ? 'primefaces.download_' + monitorKey : 'primefaces.download';
            window.downloadMonitor = setInterval(function() {
                var downloadComplete = PrimeFaces.getCookie(cookieName);

                if(downloadComplete === 'true') {
                    if(complete) {
                        complete();
                    }
                    clearInterval(window.downloadMonitor);
                    PrimeFaces.setCookie(cookieName, null);
                }
            }, 1000);
        }
    },
Lazybones answered 6/4, 2020 at 8:46 Comment(0)
C
0

I have updated the below reference code. Add a proper download URL link and try this out.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <style type="text/css">
            body {
                padding: 0;
                margin: 0;
            }

            svg:not(:root) {
                display: block;
            }

            .playable-code {
                background-color: #F4F7F8;
                border: none;
                border-left: 6px solid #558ABB;
                border-width: medium medium medium 6px;
                color: #4D4E53;
                height: 100px;
                width: 90%;
                padding: 10px 10px 0;
            }

            .playable-canvas {
                border: 1px solid #4D4E53;
                border-radius: 2px;
            }

            .playable-buttons {
                text-align: right;
                width: 90%;
                padding: 5px 10px 5px 26px;
            }
        </style>

        <style type="text/css">
            .event-log {
                width: 25rem;
                height: 4rem;
                border: 1px solid black;
                margin: .5rem;
                padding: .2rem;
            }

            input {
                width: 11rem;
                margin: .5rem;
            }

        </style>

        <title>XMLHttpRequest: progress event - Live_example - code sample</title>
    </head>

    <body>
        <div class="controls">
            <input class="xhr success" type="button" name="xhr" value="Click to start XHR (success)" />
            <input class="xhr error" type="button" name="xhr" value="Click to start XHR (error)" />
            <input class="xhr abort" type="button" name="xhr" value="Click to start XHR (abort)" />
        </div>

        <textarea readonly class="event-log"></textarea>

        <script>
            const xhrButtonSuccess = document.querySelector('.xhr.success');
            const xhrButtonError = document.querySelector('.xhr.error');
            const xhrButtonAbort = document.querySelector('.xhr.abort');
            const log = document.querySelector('.event-log');

            function handleEvent(e) {
                if (e.type == 'progress')
                {
                    log.textContent = log.textContent + `${e.type}: ${e.loaded} bytes transferred Received ${event.loaded} of ${event.total}\n`;
                }
                else if (e.type == 'loadstart')
                {
                    log.textContent = log.textContent + `${e.type}: started\n`;
                }
                else if  (e.type == 'error')
                {
                    log.textContent = log.textContent + `${e.type}: error\n`;
                }
                else if (e.type == 'loadend')
                {
                    log.textContent = log.textContent + `${e.type}: completed\n`;
                }
            }

            function addListeners(xhr) {
                xhr.addEventListener('loadstart', handleEvent);
                xhr.addEventListener('load', handleEvent);
                xhr.addEventListener('loadend', handleEvent);
                xhr.addEventListener('progress', handleEvent);
                xhr.addEventListener('error', handleEvent);
                xhr.addEventListener('abort', handleEvent);
            }

            function runXHR(url) {
                log.textContent = '';

                const xhr = new XMLHttpRequest();

                var request = new XMLHttpRequest();
                addListeners(request);
                request.open('GET', url, true);
                request.responseType = 'blob';
                request.onload = function (e) {
                    var data = request.response;
                    var blobUrl = window.URL.createObjectURL(data);
                    var downloadLink = document.createElement('a');
                    downloadLink.href = blobUrl;
                    downloadLink.download = 'download.zip';
                    downloadLink.click();
                };
                request.send();
                return request
            }

            xhrButtonSuccess.addEventListener('click', () => {
                runXHR('https://abbbbbc.com/download.zip');
            });

            xhrButtonError.addEventListener('click', () => {
                runXHR('http://i-dont-exist');
            });

            xhrButtonAbort.addEventListener('click', () => {
                runXHR('https://raw.githubusercontent.com/mdn/content/main/files/en-us/_wikihistory.json').abort();
            });
        </script>

    </body>
</html>

Return to post

Reference: XMLHttpRequest: progress event, Live example

Catrinacatriona answered 29/1, 2021 at 9:54 Comment(0)
J
-2

Create an iframe when a button/link is clicked and append this to body.

$('<iframe />')
    .attr('src', url)
    .attr('id', 'iframe_download_report')
    .hide()
    .appendTo('body');

Create an iframe with a delay and delete it after download.

var triggerDelay =   100;
var cleaningDelay =  20000;
var that = this;
setTimeout(function() {
    var frame = $('<iframe style="width:1px; height:1px;" class="multi-download-frame"></iframe>');
    frame.attr('src', url + "?" + "Content-Disposition: attachment ; filename=" + that.model.get('fileName'));
    $(ev.target).after(frame);
    setTimeout(function() {
        frame.remove();
    }, cleaningDelay);
}, triggerDelay);
Jamima answered 27/5, 2014 at 9:45 Comment(1)
This is lacking information and it doesn't solve the "when to hide loading" issue.Pirandello

© 2022 - 2024 — McMap. All rights reserved.