Detect when browser receives file download

前端 未结 22 1663
陌清茗
陌清茗 2020-11-21 04:55

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,

相关标签:
22条回答
  • 2020-11-21 05:44

    "How to detect when browser receives file download?"
    I faced the same problem with that config:
    struts 1.2.9
    jquery-1.3.2.
    jquery-ui-1.7.1.custom
    IE 11
    java 5


    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 500ms whether a cookie is coming from server.

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

    If the cookie is found, stop checking every 500ms, 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);
    

    I hope to help someone!

    0 讨论(0)
  • 2020-11-21 05:45

    Create an iframe when 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 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);
    
    0 讨论(0)
  • 2020-11-21 05:46

    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: http://stackoverflow.com/a/1459794/59087
        // See: http://shiflett.org/blog/2006/mar/server-name-versus-http-host
        // See: http://stackoverflow.com/a/3290474/59087
        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
        );
    }
    
    0 讨论(0)
  • 2020-11-21 05:46

    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; //ex: response-monitor_1419642741528
    
    //this value is passed to the client through the ResponseMonitor.onResponse callback
    $cookieValue = 1; //for ex, "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 on the repository.

    0 讨论(0)
  • 2020-11-21 05:46

    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 JS can miss it, too long and it might cancel the download screens for other downloads. Using JS 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 IE/Edge and a download link (like this one) in Firefox/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.
    0 讨论(0)
  • 2020-11-21 05:46

    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 todo 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 IE 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 IE 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 of the caching could add up. But really, if your sites that busy the long running process will starve you of threads anyways.

    Here is what the codebehind 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'");
            }
        }
    
    0 讨论(0)
提交回复
热议问题