Detectar quando o navegador recebe o download do arquivo

Eu tenho uma página que permite ao usuário baixar um arquivo gerado dinamicamente. Demora muito tempo para gerar, por isso gostaria de mostrar um indicador de “espera”. O problema é que não consigo descobrir como detectar quando o navegador recebeu o arquivo, então posso ocultar o indicador.

Estou fazendo a solicitação de forma oculta, que publica POSTs no servidor e segmenta um iframe oculto para seus resultados. Assim, não substituo a janela inteira do navegador pelo resultado. Eu ouço um evento “load” no iframe, na esperança de que ele seja triggersdo quando o download for concluído.

Devolvo um header “Content-Disposition: attachment” com o arquivo, que faz com que o navegador mostre a checkbox de diálogo “Salvar”. Mas o navegador não triggers um evento “load” no iframe.

Uma abordagem que tentei é usar uma resposta de várias partes. Por isso, enviaria um arquivo HTML vazio, bem como o arquivo para download anexado. Por exemplo:

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 

Isso funciona no Firefox; ele recebe o arquivo HTML vazio, triggers o evento “load” e mostra a checkbox de diálogo “Salvar” para o arquivo que pode ser baixado. Mas falha no IE e no Safari; O IE triggers o evento “load”, mas não faz o download do arquivo, e o Safari faz o download do arquivo (com o nome e o tipo de conteúdo errados) e não triggers o evento “load”.

Uma abordagem diferente pode ser fazer uma chamada para iniciar a criação do arquivo, pesquisar o servidor até que esteja pronto e, em seguida, fazer o download do arquivo já criado. Mas eu prefiro evitar a criação de arquivos temporários no servidor.

Alguém tem uma idéia melhor?

Uma possível solução usa JavaScript no cliente.

O algoritmo do cliente:

  1. Gere um token exclusivo random.
  2. Envie a solicitação de download e inclua o token em um campo GET / POST.
  3. Mostrar o indicador “aguardando”.
  4. Inicie um cronômetro e, a cada segundo, procure um cookie chamado “fileDownloadToken” (ou o que você decidir).
  5. Se o cookie existir e seu valor corresponder ao token, oculte o indicador “aguardando”.

O algoritmo do servidor:

  1. Procure o campo GET / POST na solicitação.
  2. Se ele tiver um valor não vazio, elimine um cookie (por exemplo, “fileDownloadToken”) e defina seu valor como o valor do token.

Código-fonte do cliente (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; } 

Exemplo de código do servidor (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(); 

Onde:

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

Uma solução de linha simples muito simples (e coxo) é usar o evento window.onblur() para fechar o diálogo de carregamento. Naturalmente, se demorar muito e o usuário decidir fazer outra coisa (como ler e-mails), a checkbox de diálogo de carregamento será fechada.

fio velho, eu sei …

mas aqueles que são liderados aqui pelo google podem estar interessados ​​na minha solução. é muito simples, mas confiável. e torna possível exibir mensagens de progresso real (e pode ser facilmente conectado a processos existentes):

o script que processa (meu problema foi: recuperar arquivos via http e entregá-los como zip) grava o status na session.

o status é pesquisado e exibido a cada segundo. isso é tudo (ok, não é. você tem que cuidar de muitos detalhes [ex .: downloads simultâneos], mas é um bom lugar para começar ;-)).

o downloadpage:

  DOWNLOAD 1 DOWNLOAD 2 ... 
Please wait...

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

Eu uso o seguinte para baixar blobs e revogar o object-url após o download. funciona em chrome e 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; } 

depois que a checkbox de diálogo de download de arquivos é fechada, a janela recupera o foco para que o evento de foco seja acionado.

Eu escrevi uma class JavaScript simples que implementa uma técnica semelhante à descrita na resposta bulltorious. Espero que possa ser útil para alguém aqui. O projeto do GitHub é chamado de response-monitor.js

Por padrão, ele usa o spin.js como indicador de espera, mas também fornece um conjunto de retornos de chamada para a implementação de um indicador personalizado.

JQuery é suportado, mas não é obrigatório.

Características notáveis

  • Integração simples
  • Sem dependencies
  • Plug-in JQuery (opcional)
  • Integração Spin.js (opcional)
  • Retornos de chamada configuráveis ​​para events de monitoramento
  • Lida com vários pedidos simultâneos
  • Detecção de erro do lado do servidor
  • Detecção de tempo limite
  • Navegador cruzado

Exemplo de uso

HTML

     Link 1 (Timeout: 30s) Link 2 (Timeout: 10s) 

Cliente (JavaScript simples)

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

Cliente (JQuery)

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

Cliente com retornos de chamada (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); 

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

Para mais exemplos, verifique a pasta de exemplos no repository.

Com base no exemplo de Elmer, preparei minha própria solução. Depois que os elementos clicam com a class de download definida, ele permite mostrar uma mensagem personalizada na canvas. Eu usei o gatilho de foco para esconder a mensagem.

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

  

Agora você deve implementar qualquer elemento para baixar:

 Download report 

ou

  

Após cada clique de download, você verá a mensagem que seu relatório está criando. Aguarde …

Quando o usuário aciona a geração do arquivo, você pode simplesmente atribuir um ID único a esse “download” e enviar o usuário para uma página que atualiza (ou verifica com o AJAX) a cada poucos segundos. Quando o arquivo estiver concluído, salve-o sob o mesmo ID exclusivo e …

  • Se o arquivo estiver pronto, faça o download.
  • Se o arquivo não estiver pronto, mostre o progresso.

Então você pode pular toda a bagunça de iframe / waiting / browserwindow, mas ter uma solução realmente elegante.

Estou muito atrasado para a festa, mas vou colocar isso aqui se mais alguém quiser saber minha solução:

Eu tive uma luta real com este problema exato, mas eu encontrei uma solução viável usando iframes (eu sei, eu sei. É terrível, mas funciona para um problema simples que eu tive)

Eu tinha uma página html que lançou um script php separado que gerou o arquivo e, em seguida, baixei-o. Na página html, usei o seguinte jquery no header html (você precisará include também uma biblioteca jquery):

  

Em your_download_script.php, tenha o seguinte:

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

Para quebrar isso, o jquery primeiro lança seu script php em um iframe. O iframe é carregado quando o arquivo é gerado. Em seguida, o jquery inicia o script novamente com uma variável de solicitação informando ao script para fazer o download do arquivo.

A razão pela qual você não pode fazer o download e a geração de arquivos de uma só vez é devido à function php header (). Se você usar header (), você está alterando o script para algo diferente de uma página da web e jquery nunca reconhecerá o script de download como sendo ‘carregado’. Eu sei que isso pode não estar necessariamente detectando quando um navegador recebe um arquivo, mas seu problema soou parecido com o meu.

Eu só tive esse mesmo problema. Minha solução foi usar arquivos temporários desde que eu estava gerando um monte de arquivos temporários já. O formulário é enviado com:

 var microBox = { show : function(content) { $(document.body).append('
' + content + '
'); 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 = $('') .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...');

No final do script que gera o arquivo eu tenho:

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

Isso fará com que o evento de carregamento no iframe seja acionado. Em seguida, a mensagem de espera é fechada e o download do arquivo será iniciado. Testado no IE7 e no Firefox.

Se você não deseja gerar e armazenar o arquivo no servidor, você está disposto a armazenar o status, por exemplo, arquivo em andamento, arquivo completo? Sua página de “espera” pode pesquisar o servidor para saber quando a geração de arquivos está completa. Você não sabe ao certo que o navegador iniciou o download, mas você tem alguma confiança.

Se você estiver transmitindo um arquivo que está gerando dinamicamente e também tiver uma biblioteca de mensagens servidor-cliente em tempo real implementada, poderá alertar seu cliente com bastante facilidade.

A biblioteca de mensagens de servidor para cliente que eu gosto e recomendo é Socket.io (via Node.js). Depois que seu script de servidor terminar de gerar o arquivo que está sendo transmitido para download, sua última linha nesse script poderá emitir uma mensagem para o Socket.io, que envia uma notificação ao cliente. No cliente, o Socket.io ouve as mensagens recebidas do servidor e permite que você as atue. A vantagem de usar esse método sobre os outros é que você é capaz de detectar um evento “verdadeiro” de término depois que o stream é feito.

Por exemplo, você poderia mostrar seu indicador de ocupado depois que um link de download for clicado, transmitir seu arquivo, emitir uma mensagem para Socket.io do servidor na última linha do script de streaming, ouvir no cliente uma notificação, receber a notificação e atualize sua interface do usuário ocultando o indicador de ocupado.

Eu percebo que a maioria das pessoas que lêem respostas para essa pergunta pode não ter esse tipo de configuração, mas eu usei essa solução exata com grande efeito em meus próprios projetos e ela funciona maravilhosamente.

O Socket.io é incrivelmente fácil de instalar e usar. Veja mais: http://socket.io/

A questão é ter um indicador de ‘espera’ enquanto um arquivo é gerado e, em seguida, retornar ao normal uma vez que o arquivo está sendo baixado. O jeito que eu gosto é usar um iFrame oculto e ligar o evento onload do frame para avisar minha página quando o download for iniciado. Mas onload não triggers no IE para downloads de arquivos (como com o token de header de anexo). Polling o servidor funciona, mas eu não gosto da complexidade extra. Então, aqui está o que eu faço:

  • Segmente o iFrame oculto como de costume.
  • Gere o conteúdo. Cache-lo com um tempo limite absoluto em 2 minutos.
  • Envie um redirecionamento de javascript de volta para o cliente de chamada, essencialmente chamando a página do gerador uma segunda vez. NOTA: isso fará com que o evento onload seja triggersdo no IE porque está agindo como uma página normal.
  • Remova o conteúdo do cache e envie-o para o cliente.

Isenção de responsabilidade, não faça isso em um site ocupado, porque o cache pode adicionar. Mas, na verdade, se seus sites que ocupam o processo de longa duração o privarem de threads de qualquer maneira.

Aqui está o que o codebehind parece, que é tudo que você realmente precisa.

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

Uma solução rápida, se você quiser apenas exibir uma mensagem ou um gif do carregador até que a checkbox de diálogo de download seja exibida, coloque a mensagem em um contêiner oculto e, ao clicar no botão que gera o arquivo a ser baixado, torne o contêiner visível. Em seguida, use jquery ou javascript para capturar o evento focusout do botão para ocultar o contêiner que contém a mensagem

Se Xmlhttprequest com blob não for uma opção, você poderá abrir seu arquivo em uma nova janela e verificar se elementos eny são preenchidos nesse corpo de janela com intervalo.

 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 

Se você fez o download de um arquivo salvo em oposição a estar no documento, não há como determinar quando o download foi concluído, pois ele não está no escopo do documento atual, mas em um processo separado no navegador.

Crie um iframe quando clicar no botão / link e anexe-o ao corpo.

  $('') .attr('src', url) .attr('id','iframe_download_report') .hide() .appendTo('body'); 

Crie um iframe com atraso e exclua-o após o download.

  var triggerDelay = 100; var cleaningDelay = 20000; var that = this; setTimeout(function() { var frame = $(''); frame.attr('src', url+"?"+ "Content-Disposition: attachment ; filename="+that.model.get('fileName')); $(ev.target).after(frame); setTimeout(function() { frame.remove(); }, cleaningDelay); }, triggerDelay);