Detect when browser receives file download

2018-12-31 00:39发布

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 I can hide the indicator.

I'm making the request in 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, in the hope 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, then shows the "Save" dialog for the downloadable file. But it fails on IE and Safari; IE fires the "load" event but 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 make a call to start the file creation, then poll the server until it's ready, then download the already-created file. But I'd rather avoid creating temporary files on the server.

Does anyone have a better idea?

16条回答
路过你的时光
2楼-- · 2018-12-31 01:16

Based on Elmer's example I've prepared my own solution. After elements click with defined download class it lets to show custom message on the screen. I've used 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 message your report is creating, please wait...

查看更多
萌妹纸的霸气范
3楼-- · 2018-12-31 01:17

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.

查看更多
路过你的时光
4楼-- · 2018-12-31 01:17

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);
查看更多
君临天下
5楼-- · 2018-12-31 01:21

I'm very late to the party but I'll put this up here if anyone else would like to know my solution:

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 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>

On 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.

查看更多
有味是清欢
6楼-- · 2018-12-31 01:21

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. Tested on IE7 and Firefox.

查看更多
心情的温度
7楼-- · 2018-12-31 01:21

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'");
        }
    }
查看更多
登录 后发表回答