javascript - tipos - Detectar cuando el navegador recibe descarga de archivos.
mime type pdf (16)
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 tengo:
header(''Refresh: 0;url=fetch.php?token='' . $token);
echo ''<html></html>'';
Esto hará que el evento de carga en el iframe se dispare. Luego se cierra el mensaje de espera y se inicia la descarga del archivo. Probado en IE7 y Firefox.
Tengo una página que permite al usuario descargar un archivo generado dinámicamente. Lleva mucho tiempo 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 realizando la solicitud de forma oculta, que se envía al servidor y se dirige a un iframe oculto para 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 "Contenido-Disposición: archivo adjunto" con el archivo, lo que hace que el navegador muestre el 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, dispara 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 de "carga" pero no descarga el archivo, y Safari descarga el archivo (con el nombre y el tipo de contenido incorrectos), y no dispara el evento de "carga".
Un enfoque diferente podría ser realizar una llamada para iniciar la creación del archivo, luego sondear el servidor hasta que esté listo y descargar el archivo ya creado. Pero prefiero evitar la creación de archivos temporales en el servidor.
¿Alguien tiene una idea mejor?
Basándome en el ejemplo de Elmer, he preparado mi propia solución. Después de hacer clic en los elementos con la clase de descarga definida, se puede mostrar un mensaje personalizado en la pantalla. He utilizado el disparador de enfoque 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 deberías 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á el mensaje que está creando su informe, espere ...
Cree un iframe cuando se 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);
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 actualice (o verifique con AJAX) cada pocos segundos. Una vez que el archivo haya finalizado, guárdelo con esa misma ID única y ...
- Si el archivo está listo, haz la descarga.
- Si el archivo no está listo, muestra el progreso.
Luego, puede omitir todo el lema de iframe / waiting / browserwindow, y tener una solución realmente elegante.
Escribí una clase de JavaScript simple que implementa una técnica similar a la descrita en una answer torcida. Espero que pueda ser de utilidad para alguien aquí. El proyecto GitHub se llama response-monitor.js
Por defecto, usa 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
- Plugin de JQuery (opcional)
- Integración Spin.js (opcional)
- Retrollamadas 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 plano)
//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 más ejemplos verifique la carpeta de examples en el repositorio.
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 todo esto es usar un iFrame oculto y enganchar el evento onload del marco para avisar a mi página cuando comience la descarga. PERO onload no se dispara en IE para descargas de archivos (como con el token de encabezado adjunto). El sondeo del servidor funciona, pero no me gusta la complejidad adicional. Así que aquí está lo que hago:
- Apunta al iFrame oculto como de costumbre.
- Generar el contenido. Caché 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 provocará que el evento onload 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 sumarse. Pero realmente, si sus sitios que están ocupados en el proceso de ejecución prolongada le privarán de hilos de todos modos.
Aquí es cómo se ve el código, que es todo lo que realmente necesitas.
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''");
}
}
Llego tarde a la fiesta, pero pondré esto aquí si alguien más quisiera saber mi solución:
Tuve una verdadera lucha con este problema exacto, pero encontré una solución viable utilizando iframes (lo sé, lo sé. Es terrible, pero funciona para un problema simple que tuve)
Tuve una página html que lanzó una secuencia de comandos php separada que generó el archivo y luego lo descargué. En la página html, usé el siguiente jquery en el encabezado html (también necesitarás incluir una biblioteca de 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 inicia su script php en un iframe. El iframe se carga una vez que se genera el archivo. Luego, jquery vuelve a iniciar el script con una variable de solicitud que le indica 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 header (), está cambiando el script a algo que no sea una página web y jquery nunca reconocerá el script de descarga como "cargado". Sé que esto no necesariamente detecta cuando un navegador recibe un archivo, pero su problema sonaba similar al mío.
Si Xmlhttprequest con blob no es una opción, puede abrir su archivo en una nueva ventana y verificar si los elementos eny se rellenan en ese cuerpo de ventana con el 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
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). Una vez que la secuencia de comandos del servidor haya terminado de generar el archivo que se está transmitiendo para descargar, la última línea de esa secuencia de comandos 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 desde el servidor y le permite actuar sobre ellos. La ventaja de utilizar este método sobre otros es que puede detectar un evento de final "verdadero" después de que se realiza la transmisión.
Por ejemplo, puede 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 en el cliente una notificación, recibir la notificación y actualiza tu interfaz de usuario ocultando el indicador de ocupado.
Me doy cuenta de que la mayoría de las personas que leen las respuestas a esta pregunta podrían 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/
Si ha descargado un archivo, que se guarda, en lugar de estar en el documento, no hay manera de determinar cuándo se completa la descarga, ya que no está en el alcance del documento actual, sino en un proceso separado en el navegador.
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" puede sondear al servidor para saber cuándo se completa la generación del archivo. No sabría con seguridad que el navegador inició la descarga, pero tendría algo de confianza.
Una posible solución utiliza JavaScript en el cliente.
El algoritmo del cliente:
- Generar un token único aleatorio.
- Envíe la solicitud de descarga e incluya el token en un campo GET / POST.
- Mostrar el indicador de "espera".
- Inicie un temporizador y, cada segundo o más, busque una cookie llamada "fileDownloadToken" (o lo que decida).
- Si la cookie existe y su valor coincide con el token, oculte el indicador de "espera".
El algoritmo del servidor:
- Busque el campo GET / POST en la solicitud.
- Si tiene un valor no vacío, elimine 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;
}
Código de servidor de ejemplo (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://.com/a/1459794/59087
// See: http://shiflett.org/blog/2006/mar/server-name-versus-http-host
// See: http://.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
);
}
Una solución de una línea muy simple (y coja) es usar el evento window.onblur()
para cerrar el cuadro de diálogo de carga. Por supuesto, si demora demasiado y el usuario decide hacer otra cosa (como leer correos electrónicos), se cerrará el diálogo de carga.
Una solución rápida si solo desea mostrar un mensaje o un gif del cargador hasta que se muestre el cuadro de diálogo de descarga es colocar el mensaje en un contenedor oculto y al hacer clic en el botón que genera el archivo a descargar, hace que el contenedor esté visible. Luego use jquery o javascript para capturar el evento de enfoque del botón para ocultar el contenedor que contiene el mensaje
Uso lo siguiente para descargar blobs y revocar el objeto url 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;
}
Una vez que se cierra el cuadro de diálogo de descarga de archivos, la ventana recupera su enfoque y se activa el evento de enfoque.
hilo viejo, lo sé ...
Pero aquellos que están 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 fue: 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 (bueno, no lo es. tienes que ocuparte de muchos detalles [por ejemplo, descargas concurrentes], 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'']);
?>
descargar.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
?>