Обнаруживать, когда браузер загружает файлы

У меня есть страница, которая позволяет пользователю загружать динамически сгенерированный файл. Для создания требуется много времени, поэтому я бы хотел показать индикатор ожидания. Проблема в том, что я не могу понять, как определить, когда браузер получил файл, поэтому я могу скрыть индикатор.

Я делаю запрос в скрытой форме, которая отправляется на сервер и нацелена на скрытый iframe для его результатов. Поэтому я не заменяю все окно браузера на результат. Я слушаю событие "load" на iframe, в надежде, что оно будет срабатывать, когда загрузка будет завершена.

Я возвращаю заголовок "Content-Disposition: attachment" с файлом, что заставляет браузер отображать диалог "Сохранить". Но браузер не запускает событие "load" в iframe.

Один из подходов, который я пробовал, - это использование многочастного ответа. Таким образом, он отправит пустой HTML файл, а также прикрепленный загружаемый файл. Например:

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

Это работает в Firefox; он получает пустой HTML файл, запускает событие "load" , затем показывает диалоговое окно "Сохранить" для загружаемого файла. Но он не работает на IE и Safari; IE запускает событие "load" , но не загружает файл, и Safari загружает файл (с неправильным именем и типом содержимого) и не запускает событие "load" .

Другой подход может заключаться в том, чтобы позвонить, чтобы начать создание файла, а затем опросить сервер, пока он не будет готов, а затем загрузите уже созданный файл. Но я бы предпочел не создавать временные файлы на сервере.

Есть ли у кого-то лучшее представление?

Ответ 1

Одно возможное решение использует JavaScript на клиенте.

Клиентский алгоритм:

  • Создайте случайный уникальный токен.
  • Отправьте запрос на загрузку и включите токен в поле GET/POST.
  • Покажите индикатор ожидания.
  • Запустите таймер и каждую секунду или около того найдите файл cookie с именем "fileDownloadToken" (или что вы решите).
  • Если файл cookie существует и его значение соответствует токену, скройте индикатор ожидания.

Алгоритм сервера:

  • В поле запроса найдите поле GET/POST.
  • Если у него есть непустое значение, отбросьте файл cookie (например, "fileDownloadToken" ) и установите его значение в значение токена.

Исходный код клиента (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;
}

Пример кода сервера (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();

Где:

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
    );
}

Ответ 2

Очень простое (и хромое) однострочное решение - использовать событие window.onblur(), чтобы закрыть диалоговое окно загрузки. Конечно, если это займет слишком много времени, и пользователь решает сделать что-то еще (например, читать электронные письма), диалог загрузки закроется.

Ответ 3

Старый поток, я знаю...

но те, которые приводят сюда google, могут быть заинтересованы в моем решении. это очень просто, но надежно. и позволяет отображать реальные сообщения о ходе работы (и их можно легко подключить к существующим процессам):

script, который обрабатывает (моя проблема: получение файлов через http и доставка их как zip) записывает статус в сеанс.

статус проверяется и отображается каждую секунду. это все (хорошо, его нет, вам нужно позаботиться о многом (например, одновременные загрузки), но это хорошее место для начала; -)).

страница загрузки:

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

getstatus.php

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

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

Ответ 4

Я использую следующее, чтобы загрузить blobs и аннулировать URL-адрес объекта после загрузки. он работает в chrome и 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;
}

после того, как диалоговое окно загрузки файла будет закрыто, окно снова начнет фокусироваться, чтобы вызвать событие фокусировки.

Ответ 5

Я написал простой класс JavaScript, который реализует технику, аналогичную той, которая описана в бытовом answer. Надеюсь, это может быть полезно кому-то здесь. Проект GitHub называется response-monitor.js

По умолчанию он использует spin.js в качестве индикатора ожидания, но также предоставляет набор обратных вызовов для реализации пользовательского индикатора.

JQuery поддерживается, но не требуется.

Известные функции

  • Простая интеграция
  • Без зависимостей
  • Плагин JQuery (необязательно)
  • Интеграция Spin.js(необязательно)
  • Конфигурируемые обратные вызовы для мониторинга событий
  • Обрабатывает несколько одновременных запросов
  • Обнаружение ошибок на стороне сервера
  • Определение тайм-аута
  • Кросс-браузер

Использование примера

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>

Клиент (обычный 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

Клиент (JQuery)

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

Клиент с обратными вызовами (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);

Сервер (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

Для получения дополнительных примеров проверьте папку examples в репозитории.

Ответ 6

Основываясь на примере Элмера, я подготовил собственное решение. После того, как элементы щелкнули с определенным классом загрузки, он позволяет отображать пользовательские сообщения на экране. Я использовал триггер фокуса, чтобы скрыть сообщение.

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>

Теперь вы должны реализовать любой элемент для загрузки:

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

или

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

После каждого щелчка загрузки вы увидите сообщение , создаваемый вашим отчетом, подождите...

Ответ 7

Когда пользователь запускает генерацию файла, вы можете просто назначить уникальный идентификатор этой "загрузке" и отправить пользователя на страницу, которая обновляется (или проверяется с помощью AJAX) каждые несколько секунд. Как только файл будет завершен, сохраните его под тем же уникальным идентификатором и...

  • Если файл готов, выполните загрузку.
  • Если файл не готов, покажите ход выполнения.

Затем вы можете пропустить весь беспорядок iframe/waiting/browserwindow, но иметь действительно элегантное решение.

Ответ 8

Я очень опаздываю на вечеринку, но я расскажу об этом здесь, если кто-нибудь еще захочет узнать мое решение:

У меня была настоящая борьба с этой точной проблемой, но я нашел жизнеспособное решение, использующее iframes (я знаю, я знаю. Это ужасно, но это работает для простой проблемы, которая у меня была)

У меня была html-страница, которая запустила отдельный php script, который сгенерировал файл, а затем загрузил его. На html-странице я использовал следующий jquery в заголовке html (вам также нужно включить библиотеку jquery):

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

На странице your_download_script.php укажите следующее:

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

Чтобы прервать это, jquery сначала запускает ваш php script в iframe. Iframe загружается после создания файла. Затем jquery снова запускает script с переменной запроса, сообщающей script, чтобы загрузить файл.

Причина, по которой вы не можете выполнить загрузку и создание файлов за один раз, связана с функцией php header(). Если вы используете header(), вы меняете script на что-то другое, кроме веб-страницы, и jquery никогда не узнает загрузку script как "загруженную". Я знаю, что это может не обязательно обнаруживать, когда браузер получает файл, но ваша проблема была похожа на мою.

Ответ 9

Если вы не хотите создавать и хранить файл на сервере, вы хотите сохранить статус, например. файл в процессе, полный файл? Ваша страница ожидания может опросить сервер, чтобы узнать, когда будет завершено создание файла. Вы не знали бы наверняка, что браузер начал загрузку, но у вас будет определенная уверенность.

Ответ 10

У меня была такая же проблема. Мое решение заключалось в использовании временных файлов, так как я уже создавал кучу временных файлов. Форма отправляется с:

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...');

В конце script, который генерирует файл, я:

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

Это приведет к тому, что событие load на iframe будет запущено. Затем сообщение ожидания будет закрыто, и загрузка файла начнется. Протестировано на IE7 и Firefox.

Ответ 11

Если вы потоковеете файл, который генерируется динамически, а также имеете встроенную библиотеку обмена сообщениями между серверами, вы можете легко оповестить своего клиента.

Библиотека обмена сообщениями между серверами, которую мне нравится и рекомендую, - Socket.io(через Node.js). После того, как ваш сервер script будет создан, создавая файл, который передается для загрузки вашей последней строки в том, что script может выдать сообщение Socket.io, которое отправляет уведомление клиенту. На клиенте Socket.io прослушивает входящие сообщения, исходящие от сервера, и позволяет вам действовать на них. Преимущество использования этого метода над другими заключается в том, что вы можете обнаружить "истинное" событие завершения после завершения потоковой передачи.

Например, вы можете показать индикатор занятости после того, как щелкнули ссылку на загрузку, потоковой поток вашего файла, выпустить сообщение на Socket.io с сервера в последней строке потоковой передачи script, прослушать на клиенте для уведомлять, получать уведомление и обновлять свой пользовательский интерфейс, скрывая индикатор занятости.

Я понимаю, что большинство людей, читающих ответы на этот вопрос, возможно, не имеют такого типа настройки, но я использовал это точное решение для отличного эффекта в своих проектах и ​​прекрасно работает.

Socket.io невероятно прост в установке и использовании. Подробнее: http://socket.io/

Ответ 12

Вопрос заключается в том, чтобы иметь индикатор ожидания во время создания файла, а затем вернуться к нормальному состоянию после загрузки файла. То, как мне нравится делать это, - это использовать скрытый iFrame и подключить событие onload для фреймов, чтобы моя страница знала, когда начнется загрузка. НО onload не запускается в IE для загрузки файлов (например, с токеном заголовка вложения). Опрос сервера работает, но мне не нравится дополнительная сложность. Итак, вот что я делаю:

  • Задайте скрытый iFrame, как обычно.
  • Сгенерировать содержимое. Загрузите его с помощью абсолютный таймаут через 2 минуты.
  • Отправьте javascript redirect обратно в вызывающий клиент, по существу вызывающий генератор страницы второй раз. ПРИМЕЧАНИЕ. Это приведет к тому, что событие onload будет запущено в IE, потому что оно действует как обычная страница.
  • Удалить содержимое из кеша и отправьте его клиенту.

Отказ от ответственности, не делайте этого на загруженном сайте, из-за того, что кеширование может складываться. Но на самом деле, если ваши сайты, занятые длительным запуском процесса, в любом случае будут голодать от ваших потоков.

Вот что выглядит codebehind, и это все, что вам действительно нужно.

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'");
        }
    }

Ответ 13

Быстрое решение, если вы хотите отображать сообщение или gif загрузчика до тех пор, пока не появится диалоговое окно загрузки, нужно поместить сообщение в скрытый контейнер, и когда вы нажимаете кнопку, которая генерирует загружаемый файл, вы делаете контейнер видимый. Затем используйте jquery или javascript, чтобы поймать событие фокуса кнопки, чтобы скрыть контейнер, содержащий сообщение

Ответ 14

Если Xmlhttprequest с blob не является вариантом, вы можете открыть файл в новом окне и проверить, заполняются ли элементы eny в этом теле окна с интервалом.

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 error page configured your application for failed requests, check for those dom elemets 
	}
}, 1000)
//Better if you specify maximun no of intervals

Ответ 15

Если вы загрузили файл, который был сохранен, в отличие от того, чтобы быть в документе, нет способа определить, когда загрузка будет завершена, поскольку она не входит в объем текущего документа, а отдельный процесс в браузера.

Ответ 16

Создайте iframe при нажатии кнопки/ссылки и добавьте его в тело.

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

Создайте iframe с задержкой и удалите его после загрузки.

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