Detectar cuando el navegador recibe la descarga del archivo

488

Tengo una página que permite al usuario descargar un archivo generado dinámicamente. Se tarda mucho tiempo en generar, por lo que me gustaría mostrar un indicador de "espera". El problema es que no puedo descubrir cómo detectar cuándo el navegador ha recibido el archivo, así que puedo ocultar el indicador.

Estoy haciendo la solicitud en una forma oculta, que POSTs al servidor, y apunta a un iframe oculto por sus resultados. Esto es para que no reemplace toda la ventana del navegador con el resultado. Escucho un evento de "carga" en el iframe, con la esperanza de que se active cuando se complete la descarga.

Devuelvo un encabezado "Content-Disposition: adjunto" con el archivo, lo que hace que el navegador muestre el cuadro de diálogo "Guardar". Pero el navegador no dispara un evento de "carga" en el iframe.

Un enfoque que probé es usar una respuesta de varias partes. Por lo tanto, enviaría un archivo HTML vacío, así como el archivo descargable adjunto. Por ejemplo:

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

Esto funciona en Firefox; recibe el archivo HTML vacío, activa el evento "cargar", luego muestra el cuadro de diálogo "Guardar" para el archivo descargable. Pero falla en IE y Safari; IE dispara el evento "cargar" pero no descarga el archivo, y Safari descarga el archivo (con el nombre y tipo de contenido incorrectos) y no dispara el evento "cargar".

Un enfoque diferente podría ser hacer una llamada para comenzar la creación del archivo, luego sondear el servidor hasta que esté listo, luego descargar el archivo ya creado. Pero prefiero evitar crear archivos temporales en el servidor.

¿Alguien tiene una idea mejor?

JW
fuente
44
Ninguna versión de IE es compatible con multipart / x-mixed-replace.
EricLaw
Gracias Eric, es bueno saberlo. No perderé más tiempo con ese enfoque.
JW.
La única forma confiable parece ser la notificación push del servidor (SignalR para la gente de ASP.NET)
dudeNumber4
1
bennadel.com/blog/… - esta es una solución simple
Mateen
1
@mateen gracias amigo! es realmente simple
Fai Zal Dong

Respuestas:

451

Una posible solución utiliza JavaScript en el cliente.

El algoritmo del cliente:

  1. Genera una ficha única aleatoria.
  2. Envíe la solicitud de descarga e incluya el token en un campo GET / POST.
  3. Mostrar el indicador de "espera".
  4. Inicie un temporizador, y cada segundo más o menos, busque una cookie llamada "fileDownloadToken" (o lo que decida).
  5. Si la cookie existe y su valor coincide con el token, oculte el indicador de "espera".

El algoritmo del servidor:

  1. Busque el campo GET / POST en la solicitud.
  2. Si tiene un valor no vacío, suelte una cookie (por ejemplo, "fileDownloadToken") y establezca su valor en el valor del token.

Código fuente del 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;
}

Ejemplo de código de 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();

Dónde:

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
    );
}
tormentoso
fuente
44
Impresionante idea, lo utilicé como marco básico para esta respuesta sobre la descarga de múltiples archivos con jQuery / C #
Greg
77
Un aviso para otros: si document.cookies no incluye downloadToken, verifique la ruta de la cookie. En mi caso, tuve que establecer la ruta a "/" en el lado del servidor (por ejemplo, cookie.setPath ("/") en Java) aunque la ruta estaba en blanco de forma predeterminada. Durante algún tiempo pensé que el problema era el manejo especial de cookies de dominio 'localhost' ( stackoverflow.com/questions/1134290/… ) pero ese no fue el problema al final. Puede ser eso para otros, aunque vale la pena leerlo.
jlpp
2
@bulltorious antes de profundizar en su solución, me pregunto si funcionará con una solicitud de descarga de archivos de dominio cruzado. ¿Crees que lo hará, o las restricciones de cookies lo comprometerán?
kiks73
55
Brillante: no se me habría ocurrido en 100 años que podría incluir cookies como parte de una descarga de archivos. ¡¡Gracias!!
Freefaller
8
Como otros han señalado, esta solución solo resuelve parte del problema, la espera de que el servidor prepare el tiempo del archivo. La otra parte del problema, que puede ser considerable dependiendo del tamaño del archivo y la velocidad de conexión, es cuánto tiempo lleva realmente obtener todo el archivo en el cliente. Y eso no se resuelve con esta solución.
AsGoodAsItGets
27

Una solución de una línea muy simple (y poco convincente) es usar el window.onblur()evento para cerrar el diálogo de carga. Por supuesto, si lleva demasiado tiempo y el usuario decide hacer otra cosa (como leer correos electrónicos), se cerrará el cuadro de diálogo de carga.

hombre pájaro
fuente
Este es un enfoque simple que es ideal para deshacerse de una superposición de carga para una descarga de archivos que se activó con onbeforeunloadGracias.
wf4
55
Esto no funciona en todos los navegadores (algunos no dejan / desenfocan la ventana actual como parte del flujo de trabajo de descarga, por ejemplo, Safari, algunas versiones de IE, etc.).
hiattp
44
Chrome y otros navegadores similares descargan automáticamente los archivos donde esta condición fallará.
Lucky
@Lucky es solo por defecto. Es completamente posible que un usuario de Chrome especifique dónde se deben guardar las descargas y, por lo tanto, vea el cuadro de diálogo
ESR
2
mala idea porque activas el desenfoque en el cambio de tabulación, o cualquier acción fuera de la ventana
Michael
14

hilo viejo, lo sé ...

pero aquellos que están dirigidos aquí por Google podrían estar interesados ​​en mi solución. Es muy simple, pero confiable. y permite mostrar mensajes de progreso real (y se puede conectar fácilmente a los procesos existentes):

el script que procesa (mi problema era: recuperar archivos a través de http y entregarlos como zip) escribe el estado en la sesión.

el estado se sondea y se muestra cada segundo. eso es todo (está bien, no. Tienes que ocuparte de muchos detalles [por ejemplo, descargas simultáneas], pero es un buen lugar para comenzar ;-)).

la página de descarga:

    <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
fuente
3
pero si el usuario tiene múltiples ventanas o descargas abiertas? también recibes aquí una llamada redundante al servidor
Yuki
3
Si tiene varias conexiones de un usuario, todas estarán esperando que finalicen otras conexiones porque session_start () bloquea la sesión para el usuario y evita que todos los demás procesos accedan a ella.
Honza Kuchař
2
no es necesario que lo use .each()para inscribirse en eventos. solo diga$('a.download').click()
robisrob
No evalúes el código dentro setTimeout('getstatus()', 1000);. Use el fn directamente:setTimeout(getstatus, 1000);
Roko C. Buljan
11

Utilizo lo siguiente para descargar blobs y revocar la URL del objeto después de la descarga. Funciona en Chrome y 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;
}

después de que se cierra el cuadro de diálogo de descarga de archivos, la ventana recupera su enfoque para que se active el evento de enfoque.

Elmer
fuente
Todavía tiene el problema de cambiar la ventana y regresar, lo que hará que el modal se oculte.
dudeNumber4
99
Los navegadores como Chrome que se descargan en la bandeja inferior nunca desenfocan / reenfocan la ventana.
Coleman
10

Basado en el ejemplo de Elmer, he preparado mi propia solución. Después de hacer clic en los elementos con una clase de descarga definida , permite mostrar un mensaje personalizado en la pantalla. He usado desencadenador de foco para ocultar el mensaje.

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>

Ahora debe implementar cualquier elemento para descargar:

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

o

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

Después de cada clic de descarga , verá un mensaje que está creando su informe, espere ...

Jerzy Gebler
fuente
2
¿Qué pasa si el usuario hace clic en la ventana?
Tom Roggero
esto es exactamente lo que estaba buscando, ¡muchas gracias!
Sergio
El hide () no se llama en mi caso
Prashant Pimpale
8

Escribí una clase simple de JavaScript que implementa una técnica similar a la descrita en una respuesta tormentosa . Espero que pueda ser útil para alguien aquí. El proyecto GitHub se llama response-monitor.js

De forma predeterminada, utiliza spin.js como indicador de espera, pero también proporciona un conjunto de devoluciones de llamada para la implementación de un indicador personalizado.

JQuery es compatible pero no es obligatorio.

Características notables

  • Integración simple
  • Sin dependencias
  • Complemento JQuery (opcional)
  • Integración Spin.js (opcional)
  • Callbacks configurables para monitorear eventos
  • Maneja múltiples solicitudes simultáneas
  • Detección de errores del lado del servidor
  • Detección de tiempo de espera
  • Navegador cruzado

Ejemplo 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 simple)

//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 con devoluciones de llamada (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 obtener más ejemplos, consulte la carpeta de ejemplos en el repositorio.

Jorge Paulo
fuente
5

Llego muy tarde a la fiesta, pero pondré esto aquí si alguien más quiere saber mi solución:

Tuve una verdadera lucha con este problema exacto, pero encontré una solución viable usando iframes (lo sé, lo sé. Es terrible, pero funciona para un problema simple que tenía)

Tenía una página html que lanzó un script php separado que generó el archivo y luego lo descargó. En la página html, utilicé el siguiente jquery en el encabezado html (también necesitarás incluir una 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>

En your_download_script.php, tenga lo siguiente:

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 desglosar esto, jquery primero lanza su script php en un iframe. El iframe se carga una vez que se genera el archivo. Luego, jquery lanza el script nuevamente con una variable de solicitud que le dice al script que descargue el archivo.

La razón por la que no puede realizar la descarga y la generación de archivos de una sola vez se debe a la función php header (). Si usa el encabezado (), está cambiando el script a algo diferente a una página web y jquery nunca reconocerá el script de descarga como 'cargado'. Sé que esto no necesariamente se detecta cuando un navegador recibe un archivo, pero su problema sonó similar al mío.

Walker Boh
fuente
5

Si está transmitiendo un archivo que está generando dinámicamente y también tiene implementada una biblioteca de mensajería de servidor a cliente en tiempo real, puede alertar a su cliente con bastante facilidad.

La biblioteca de mensajería de servidor a cliente que me gusta y recomiendo es Socket.io (a través de Node.js). Después de que el script de su servidor haya terminado de generar el archivo que se está transmitiendo para descargar su última línea en ese script, puede emitir un mensaje a Socket.io que envía una notificación al cliente. En el cliente, Socket.io escucha los mensajes entrantes emitidos por el servidor y le permite actuar sobre ellos. El beneficio de usar este método sobre otros es que puede detectar un evento de finalización "verdadero" después de que se realiza la transmisión.

Por ejemplo, podría mostrar su indicador de ocupado después de hacer clic en un enlace de descarga, transmitir su archivo, emitir un mensaje a Socket.io desde el servidor en la última línea de su secuencia de comandos de transmisión, escuchar una notificación en el cliente, recibir la notificación y actualice su IU ocultando el indicador de ocupado.

Me doy cuenta de que la mayoría de las personas que leen respuestas a esta pregunta pueden no tener este tipo de configuración, pero he usado esta solución exacta con gran efecto en mis propios proyectos y funciona de maravilla.

Socket.io es increíblemente fácil de instalar y usar. Ver más: http://socket.io/

Art Geigel
fuente
5

"¿Cómo detectar cuándo el navegador recibe la descarga de archivos?"
Me enfrenté al mismo problema con esa configuración:
struts 1.2.9
jquery-1.3.2.
jquery-ui-1.7.1.custom
IE 11
java 5


Mi solución con una cookie:
- Lado del cliente:
cuando envíe su formulario, llame a su función de JavaScript para ocultar su página y cargar su ruleta en espera

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

Luego, llame a una función que verificará cada 500 ms si una cookie proviene del servidor.

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

Si se encuentra la cookie, deje de verificar cada 500 ms, caduque la cookie y llame a su función para volver a su página y eliminar la rueda de espera ( removeWaitingSpinner () ). ¡Es importante que caduque la cookie si desea poder descargar otro archivo nuevamente!

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 del servidor:
al final del proceso del servidor, agregue una cookie a la respuesta. Esa cookie se enviará al cliente cuando su archivo esté listo para descargar.

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

Espero ayudar a alguien!

MB33
fuente
Funciona perfectamente. Gracias por esta hermosa muestra.
Sedat Kumcu
4

Cuando el usuario activa la generación del archivo, simplemente puede asignar una ID única a esa "descarga" y enviar al usuario a una página que se actualiza (o verifica con AJAX) cada pocos segundos. Una vez que el archivo esté terminado, guárdelo con esa misma ID única y ...

  • Si el archivo está listo, realice la descarga.
  • Si el archivo no está listo, muestre el progreso.

Luego puede omitir todo el desorden de iframe / waiting / browserwindow, y aún así tener una solución realmente elegante.

gahooa
fuente
Eso suena como el enfoque de archivo temporal que mencioné anteriormente. Podría hacer algo como esto si resulta que mi idea es imposible, pero esperaba evitarlo.
JW.
3

Si no desea generar y almacenar el archivo en el servidor, ¿está dispuesto a almacenar el estado, por ejemplo, archivo en progreso, archivo completo? Su página de "espera" podría sondear al servidor para saber cuándo se completa la generación del archivo. No sabría con certeza si el navegador inició la descarga, pero tendría cierta confianza.

bmb
fuente
2

Acabo de tener exactamente el mismo problema. Mi solución fue usar archivos temporales ya que ya estaba generando un montón de archivos temporales. El formulario se envía con:

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

Al final del script que genera el archivo que tengo:

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

Esto hará que se active el evento de carga en el iframe. Luego se cierra el mensaje de espera y se iniciará la descarga del archivo. Probado en IE7 y Firefox.

grom
fuente
2

En mi experiencia, hay dos formas de manejar esto:

  1. Establezca una cookie de corta duración en la descarga y haga que JavaScript compruebe continuamente su existencia. El único problema real es obtener la duración correcta de la cookie: demasiado corta y el JS puede perderla, demasiado tiempo y puede cancelar las pantallas de descarga para otras descargas. El uso de JS para eliminar la cookie después del descubrimiento generalmente soluciona esto.
  2. Descargue el archivo usando fetch / XHR. No solo sabe exactamente cuándo finaliza la descarga del archivo, si usa XHR puede usar eventos de progreso para mostrar una barra de progreso. Guarde el blob resultante con msSaveBlob en IE / Edge y un enlace de descarga ( como este ) en Firefox / Chrome. El problema con este método es que iOS Safari no parece manejar correctamente la descarga de blobs: puede convertir el blob en una URL de datos con un FileReader y abrirlo en una nueva ventana, pero eso es abrir el archivo, no guardarlo.
Sora2455
fuente
2

Saludos, sé que el tema es antiguo pero dejo una solución que vi en otro lugar y funcionó:

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

Puedes usarlo:

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

Si ha descargado un archivo, que se guarda, en lugar de estar en el documento, no hay forma de determinar cuándo se completa la descarga, ya que no está dentro del alcance del documento actual, sino un proceso separado en el navegador.

Diodeus - James MacFarlane
fuente
8
Debo aclarar: no me preocupa demasiado cuándo se completa la descarga . Si puedo identificar cuándo comienza la descarga, sería suficiente.
JW.
0

La pregunta es tener un indicador de 'espera' mientras se genera un archivo y luego volver a la normalidad una vez que el archivo se esté descargando. La forma en que me gusta hacer esto es usar un iFrame oculto y conectar el evento de carga del marco para que mi página sepa cuándo comienza la descarga. PERO la carga no se dispara en IE para descargas de archivos (como con el token del encabezado del archivo adjunto). El sondeo del servidor funciona, pero no me gusta la complejidad adicional. Entonces, esto es lo que hago:

  • Apunte al iFrame oculto como de costumbre.
  • Genera el contenido. Cachéalo con un tiempo de espera absoluto en 2 minutos.
  • Envíe una redirección de JavaScript al cliente que realiza la llamada, esencialmente llamando a la página del generador por segunda vez. NOTA: esto hará que el evento de carga se active en IE porque actúa como una página normal.
  • Elimine el contenido del caché y envíelo al cliente.

Descargo de responsabilidad, no haga esto en un sitio ocupado, ya que el almacenamiento en caché podría acumularse. Pero realmente, si sus sitios que están ocupados con el proceso de larga ejecución lo dejarán sin hilos de todos modos.

Así es como se ve el código subyacente, que es todo lo que realmente necesita.

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

Una solución rápida si solo desea mostrar un mensaje o un gif de cargador hasta que aparezca el cuadro de diálogo de descarga es colocar el mensaje en un contenedor oculto y cuando hace clic en el botón que genera el archivo a descargar, hace que el contenedor sea visible. Luego use jquery o javascript para capturar el evento de enfoque del botón para ocultar el contenedor que contiene el mensaje

Mihaela Rada
fuente
0

Si Xmlhttprequest con blob no es una opción, puede abrir su archivo en una nueva ventana y verificar si los elementos eny se llenan en ese cuerpo de ventana con 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
fuente
0

Este ejemplo de Java / Spring detecta el final de una descarga, en cuyo punto oculta el indicador "Cargando ...".

Enfoque: en el lado de JS, configure una cookie con una edad de caducidad máxima de 2 min y realice una encuesta cada segundo para la caducidad de la cookie . Luego, el lado del servidor anula esta cookie con una edad de vencimiento anterior : la finalización del proceso del servidor. Tan pronto como se detecta la caducidad de la cookie en el sondeo de JS, se oculta "Cargando ...".

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

Java / Spring Server Side

    @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;
}
gen b.
fuente
La cookie es creada por el script JS pero no es actualizada por el controlador, mantiene el valor original (0), ¿cómo puedo actualizar el valor de la cookie sin actualizar la página?
Shessuky
Eso es extraño, ¿puedes asegurarte de que el nombre sea exactamente correcto? Sobrescribirá la cookie si el nombre coincide. Déjame saber
gen b.
El valor original no es 0. El valor original establecido en JS es de 2 min. El NUEVO valor con el que se supone que el servidor debe modificar es 0.
gen b.
Además, ¿estás haciendo esto: myCookie.setPath("/"); response.addCookie(myCookie);
gen b.
Me di cuenta (por alguna razón), que debería agregar cookies antes de hacer response.getOutputStream (); (obteniendo flujo de salida de respuesta para agregar archivos de descarga), no se tuvo en cuenta cuando lo hice después de ese paso
Shessuky
0

Primefaces también utiliza encuestas de cookies

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

Cree un iframe cuando haga clic en el botón / enlace y añádalo al cuerpo.

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

Cree un iframe con retraso y elimínelo después de la descarga.

                            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
fuente
Falta información y no resuelve el problema de "cuándo ocultar la carga".
Tom Roggero