Detectar quando o navegador recebe o download do arquivo

488

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

Estou fazendo a solicitação em um formulário oculto, que POSTs para o servidor, e direciona um iframe oculto para seus resultados. Isso é para não substituir a janela inteira do navegador pelo resultado. Eu escuto um evento "load" no iframe, na esperança de que ele seja acionado quando o download for concluído.

Retorno um cabeçalho "Disposição de conteúdo: anexo" com o arquivo, o que faz com que o navegador mostre a caixa de diálogo "Salvar". Mas o navegador não dispara um evento "load" no iframe.

Uma abordagem que tentei é usar uma resposta de várias partes. Portanto, ele 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, aciona o evento "load" e mostra a caixa de diálogo "Save" do arquivo para download. Mas falha no IE e no Safari; O IE aciona o evento "load", mas não baixa o arquivo, e o Safari baixa o arquivo (com o nome e o tipo de conteúdo incorretos) e não aciona 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 fazer o download do arquivo já criado. Mas prefiro evitar criar arquivos temporários no servidor.

Alguém tem uma idéia melhor?

JW.
fonte
4
Nenhuma versão do IE suporta multipart / x-mixed-replace.
9119 EricLaw
Obrigado Eric - é bom saber. Não vou perder mais tempo com essa abordagem.
JW.
A única maneira confiável parece ser a notificação por push do servidor (pessoal do SignalR for ASP.NET).
precisa saber é o seguinte
1
bennadel.com/blog/... - esta é uma solução simples
Mateen
1
@mateen obrigado cara! realmente simples
Fai Zal Dong

Respostas:

451

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

O algoritmo do cliente:

  1. Gere um token exclusivo aleatório.
  2. Envie a solicitação de download e inclua o token em um campo GET / POST.
  3. Mostrar o indicador "em espera".
  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 "em espera".

O algoritmo do servidor:

  1. Procure o campo GET / POST na solicitação.
  2. Se tiver um valor não vazio, solte 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
    );
}
torturante
fonte
4
Impressionante ideia, eu usei-o como uma estrutura básica para esta resposta sobre o download de vários arquivos com jQuery / C #
Greg
7
Um alerta para os outros: se document.cookies não incluir o downloadToken, verifique o caminho do cookie. No meu caso, eu tive que definir o caminho para "/" no lado do servidor (por exemplo, cookie.setPath ("/") em Java), mesmo que o caminho estivesse em branco por padrão. Por algum tempo, pensei que o problema era o tratamento especial de cookies do domínio 'localhost' ( stackoverflow.com/questions/1134290/… ), mas esse não foi o problema no final. Pode ser que, para outros, valha a pena ler.
jlpp
2
@ bulltorious antes de aprofundar sua solução, gostaria de saber se funcionará com uma solicitação de download de arquivos entre domínios. Você acha que sim, ou as restrições de cookies o comprometerão?
kiks73
5
Brilhante - não me ocorreria em 100 anos que você poderia incluir cookies como parte de um download de arquivo. Obrigado!!
freefaller
8
Como outros já apontaram, esta solução resolve apenas parte do problema, a espera pelo servidor para preparar o tempo do arquivo. A outra parte do problema, que pode ser considerável, dependendo do tamanho do arquivo e da velocidade da conexão, é o tempo que leva para realmente obter o arquivo inteiro no cliente. E isso não é resolvido com esta solução.
AsGoodAsItGets
27

Uma solução muito simples (e esfarrapada) de uma linha é usar o window.onblur()evento para fechar a caixa de diálogo de carregamento. Obviamente, se levar muito tempo e o usuário decidir fazer outra coisa (como ler e-mails), a caixa de diálogo de carregamento será fechada.

homem Pássaro
fonte
Essa é uma abordagem simples, ideal para se livrar de uma sobreposição de carregamento para um download de arquivo que foi acionado usando onbeforeunloadObrigado.
wf4 14/02
5
Isso não funciona em todos os navegadores (alguns não deixam / desfocam a janela atual como parte do fluxo de trabalho de download, por exemplo, Safari, algumas versões do IE, etc.).
Hiattp
4
O Chrome e outros navegadores fazem o download automático dos arquivos em que essa condição falhará.
Sorte
@ Sorte que é apenas por padrão. É perfeitamente possível que um usuário do Chrome especifique onde os downloads devem ser salvos e, portanto, veja a caixa de diálogo
ESR
2
má idéia, porque você ativar o borrão no tabchange, ou qualquer ação fora da janela
Michael
14

fio velho, eu sei ...

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

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

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

a página de download:

    <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
    ?>
bytepirate
fonte
3
mas se o usuário tiver várias janelas ou downloads abertos? Também você chegar aqui uma chamada redundante para o servidor
Yuki
3
Se você tiver várias conexões de um usuário, todos estarão aguardando o término de outras conexões, porque session_start () bloqueia a sessão do usuário e impede que todos os outros processos acessem a mesma.
Honza Kuchař
2
você não precisa usar .each()para registros de eventos. apenas diga$('a.download').click()
robisrob
Não avalie o código por dentro setTimeout('getstatus()', 1000);. Use o fn diretamente:setTimeout(getstatus, 1000);
Roko C. Buljan
11

Eu uso o seguinte para baixar blobs e revogar o objeto-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 caixa de diálogo de download do arquivo é fechada, a janela recupera o foco para que o evento de foco seja acionado.

Elmer
fonte
Ainda tem o problema de mudar de janela e retornar, o que fará com que o modal oculte.
precisa saber é o seguinte
9
Navegadores como o Chrome, que são baixados na bandeja inferior, nunca desfocam / redirecionam a janela.
Coleman
10

Com base no exemplo de Elmer, preparei minha própria solução. Depois que os elementos clicam com a classe de download definida, ele mostra uma mensagem personalizada na tela. Eu usei o foco gatilho de para ocultar 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

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

Agora você deve implementar qualquer elemento para fazer o download:

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

ou

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

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

Jerzy Gebler
fonte
2
E se o usuário clicar na janela?
quer
isto é exatamente o que eu estava procurando, isso é muito !!
Sergio
O hide () não está sendo chamado no meu caso
Prashant Pimpale
8

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

Por padrão, ele usa spin.js como o 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.

Recursos notáveis

  • Integração simples
  • Sem dependências
  • Plug-in JQuery (opcional)
  • Integração Spin.js (opcional)
  • Retornos de chamada configuráveis ​​para monitorar eventos
  • Lida com várias solicitações simultâneas
  • Detecção de erro no servidor
  • Detecção de tempo limite
  • Navegador cruzado

Exemplo de uso

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>

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 repositório.

Jorge Paulo
fonte
5

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

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

Eu tinha uma página html que lançava um script php separado que gerava o arquivo e o baixava. Na página html, usei o seguinte jquery no cabeçalho html (você também precisará incluir uma biblioteca 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>

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 assim que o arquivo é gerado. Em seguida, o jquery inicia o script novamente com uma variável de solicitação informando ao script para baixar o arquivo.

O motivo pelo qual você não pode fazer o download e a geração de arquivos de uma só vez é devido à função header () do php. Se você usa o header (), está alterando o script para algo diferente de uma página da Web e o jquery nunca reconhecerá o script de download como 'carregado'. Sei que isso pode não estar necessariamente detectando quando um navegador recebe um arquivo, mas o problema foi semelhante ao meu.

Walker Boh
fonte
5

Se você estiver transmitindo um arquivo que está gerando dinamicamente e também tiver uma biblioteca de mensagens de servidor para 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.). Após a conclusão do script do servidor, a geração do arquivo que está sendo transmitido para download e sua última linha nesse script pode emitir uma mensagem para o Socket.io, que envia uma notificação ao cliente. No cliente, o Socket.io escuta as mensagens recebidas emitidas pelo servidor e permite que você as atue. O benefício de usar esse método sobre outros é que você é capaz de detectar um evento de conclusão "verdadeiro" após a conclusão da transmissão.

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

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

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

Art Geigel
fonte
5

"Como detectar quando o navegador recebe o download de arquivos?"
Eu enfrentei o mesmo problema com essa configuração:
struts 1.2.9
jquery-1.3.2.
jquery-ui-1.7.1.custom
IE 11
java 5


Minha solução com um cookie:
- Lado do cliente:
ao enviar seu formulário, chame sua função javascript para ocultar sua página e carregar seu botão giratório em espera

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

Em seguida, chame uma função que verificará a cada 500ms se um cookie é proveniente do servidor.

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

Se o cookie for encontrado, pare de verificar a cada 500 ms, expire o cookie e chame sua função para voltar à sua página e remover o botão giratório em espera ( removeWaitingSpinner () ). É importante expirar o cookie se você deseja poder baixar outro arquivo novamente!

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

- Lado do servidor:
no final do processo do servidor, adicione um cookie à resposta. Esse cookie será enviado ao cliente quando seu arquivo estiver pronto para download.

Cookie waitCookie = new Cookie("waitingCookie", "done");
response.addCookie(waitCookie);

Espero ajudar alguém!

MB33
fonte
Funciona perfeitamente. Obrigado por esta bela amostra.
Sedat Kumcu 9/10/19
4

Quando o usuário aciona a geração do arquivo, você pode simplesmente atribuir um ID exclusivo 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 terminar, salve-o com o mesmo ID exclusivo e ...

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

Em seguida, você pode pular toda a confusão de iframe / espera / janela do navegador e ainda ter uma solução realmente elegante.

gahooa
fonte
Parece a abordagem de arquivo temporário que mencionei acima. Eu poderia fazer algo assim se a minha ideia fosse impossível, mas esperava evitá-la.
JW.
3

Se você não deseja gerar e armazenar o arquivo no servidor, deseja armazenar o status, por exemplo, arquivo em andamento ou arquivo completo? Sua página "em espera" pode consultar o servidor para saber quando a geração do arquivo está concluída. Você não saberia ao certo que o navegador iniciou o download, mas você teria alguma confiança.

bmb
fonte
2

Eu apenas tive exatamente esse mesmo problema. Minha solução foi usar arquivos temporários, já que eu já estava gerando vários arquivos temporários. O formulário é enviado com:

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

No final do script que gera o arquivo, eu tenho:

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

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

grom
fonte
2

Na minha experiência, existem duas maneiras de lidar com isso:

  1. Defina um cookie de curta duração no download e faça com que o JavaScript verifique continuamente sua existência. O único problema real é acertar a vida útil do cookie - muito curto e o JS pode perdê-lo, muito tempo e pode cancelar as telas de download para outros downloads. O uso de JS para remover o cookie após a descoberta geralmente corrige isso.
  2. Faça o download do arquivo usando buscar / XHR. Você não apenas sabe exatamente quando o download do arquivo termina, se você usa o XHR, pode usar eventos de progresso para mostrar uma barra de progresso! Salve o blob resultante com o msSaveBlob no IE / Edge e um link de download ( como este ) no Firefox / Chrome. O problema com esse método é que o iOS Safari parece não lidar com os blobs de download corretamente - você pode converter o blob em um URL de dados com um FileReader e abri-lo em uma nova janela, mas isso é abrir o arquivo e não salvá-lo.
Sora2455
fonte
2

Saudações, sei que o tópico é antigo, mas deixo uma solução que vi em outro lugar e funcionou:

/**
 *  download file, show modal
 *
 * @param uri link
 * @param name file name
 */
function downloadURI(uri, name) {
// <------------------------------------------       Do someting (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'));
        })
        .catch(() => alert('An error sorry'));
}

Você pode usar isso:

downloadURI("www.linkToFile.com", "file.name");
Manuel Larrota
fonte
1

Se você fez o download de um arquivo salvo, em vez de constar no documento, não há como determinar quando o download é concluído, pois não está no escopo do documento atual, mas em um processo separado no navegador.

Diodeus - James MacFarlane
fonte
8
Devo esclarecer - I "m não muito preocupado com quando o download for concluído . Se eu conseguir identificar quando o download começa, isso seria suficiente.
. JW
0

A questão é ter um indicador de 'espera' enquanto um arquivo é gerado e retornar ao normal assim que o arquivo estiver sendo baixado. O jeito que eu gosto disso é usar um iFrame oculto e conectar o evento onload do quadro para informar minha página quando o download for iniciado. MAS onload não é acionado no IE para downloads de arquivos (como no token do cabeçalho do anexo). A pesquisa do servidor funciona, mas 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. Coloque em cache com um tempo limite absoluto em 2 minutos.
  • Envie um redirecionamento javascript de volta para o cliente que está chamando, essencialmente chamando a página do gerador pela segunda vez. NOTA: isso fará com que o evento onload seja acionado no IE porque está agindo como uma página comum.
  • Remova o conteúdo do cache e envie para o cliente.

Isenção de responsabilidade, não faça isso em um site ocupado, pois o armazenamento em cache pode aumentar. Mas, na verdade, se seus sites que ocupam o longo processo de privá-lo de threads de qualquer maneira.

Aqui está a aparência do código, que é tudo o 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'");
        }
    }
MarcLawrence
fonte
0

Uma solução rápida, se você deseja exibir apenas uma mensagem ou um gif do carregador até a caixa de diálogo de download ser exibida, é colocar a mensagem em um contêiner oculto e, quando você clica no botão que gera o arquivo a ser baixado, torna o contêiner visível. Em seguida, use jquery ou javascript para capturar o evento de foco do botão e ocultar o contêiner que contém a mensagem

Mihaela Rada
fonte
0

Se Xmlhttprequest com blob não for uma opção, você poderá abrir seu arquivo em uma nova janela e verificar se os elementos eny são preenchidos nesse corpo da 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

aniketKumkar
fonte
0

Este exemplo Java / Spring detecta o final de um Download, quando oculta o indicador "Carregando ...".

Abordagem: no lado JS, defina um cookie com uma idade máxima de expiração de 2 min e pesquise a cada segundo a expiração do cookie . Em seguida, o lado do servidor substitui esse cookie por um anterior idade de vencimento - a conclusão do processo do servidor. Assim que a expiração do cookie é detectada na pesquisa JS, "Carregando ..." fica oculto.

JS 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 JS 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 JS timer detects the expired cookie earlier than 2 min. 
// (coming from the server), or the initial JS-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 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 timer to check for 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 JS 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();
    }
}

Servidor Java / Spring

    @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 JS-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 sec. for any flushing concerns
        myCookie.setPath("/");
        response.addCookie(myCookie);
        //... -- presumably the download is writing to the Output Stream...
        return null;
}
gene b.
fonte
O cookie é criado pelo script JS, mas não é atualizado pelo controlador, mantém o valor original (0). Como posso atualizar o valor do cookie sem atualizar a página?
Shessuky 28/02
Isso é estranho - você pode garantir que o nome esteja exatamente correto? Ele substituirá o cookie se o nome corresponder. Deixe-me saber
gene b.
O valor original não é 0. O valor original definido em JS é 2 min. O novo valor com o qual o servidor deve modificar é 0.
gene b.
Além disso, você está fazendo isso: myCookie.setPath("/"); response.addCookie(myCookie);
gene b.
Eu descobri (por algum motivo) que deveria adicionar cookies antes de fazer response.getOutputStream (); (obtendo o fluxo de saída de resposta para anexar arquivos de download), ele não foi levado em consideração quando o fiz depois dessa etapa
Shessuky 03/03
0

O Primefaces também usa pesquisa de cookie

https://github.com/primefaces/primefaces/blob/32bb00299d00e50b2cba430638468a4145f4edb0/src/main/resources/META-INF/resources/primefaces/core/core.js#L458

    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);
        }
    },
wutzebaer
fonte
-2

Crie um iframe quando o botão / link for clicado e anexe-o ao corpo.

                  $('<iframe />')
                 .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 = $('<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);
Pir Abdul
fonte
Faltam informações e não solucionam o problema "quando ocultar o carregamento".
quer