573 Stimmen

Erkennen, wenn ein Browser einen Dateidownload erhält

Ich habe eine Seite, die es dem Benutzer ermöglicht, eine dynamisch generierte Datei herunterzuladen. Die Generierung dauert lange, daher möchte ich eine "Warte"-Anzeige einblenden. Das Problem ist, ich kann nicht herausfinden, wie zu erkennen, wenn der Browser die Datei erhalten hat, so dass ich die Anzeige ausblenden kann.

Ich beantrage ein verstecktes Formular, das POSTs an den Server und zielt auf einen versteckten Iframe für seine Ergebnisse. Dies ist, so dass ich nicht das gesamte Browserfenster mit dem Ergebnis ersetzen. Ich höre auf ein "load"-Ereignis auf dem iframe, in der Hoffnung, dass es ausgelöst wird, wenn der Download abgeschlossen ist.

Ich gebe ein " Content-Disposition: attachment "Dies veranlasst den Browser, das Dialogfeld "Speichern" anzuzeigen. Aber der Browser löst kein "Load"-Ereignis im Iframe aus.

Ein Ansatz, den ich ausprobiert habe, ist die Verwendung einer multi-part Antwort. Es würde also sowohl eine leere HTML-Datei als auch die angehängte herunterladbare Datei gesendet.

Zum Beispiel:

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

Dies funktioniert in Firefox; er empfängt die leere HTML-Datei, feuert die "laden" Ereignis und zeigt dann die "Speichern" Dialog für die herunterladbare Datei. Aber es schlägt fehl bei Internet Explorer y Safari Internet Explorer löst das Ereignis "Laden" aus, lädt die Datei aber nicht herunter, und Safari Downloads die Datei (mit dem falschen Namen und Inhaltstyp) und löst nicht die "laden" Veranstaltung.

Ein anderer Ansatz könnte darin bestehen, die Erstellung der Datei zu starten, den Server abzufragen, bis sie fertig ist, und dann die bereits erstellte Datei herunterzuladen. Aber ich würde es lieber vermeiden, temporäre Dateien auf dem Server zu erstellen.

Was sollte ich tun?

502voto

bulltorious Punkte 7879

Eine mögliche Lösung verwendet JavaScript auf dem Client.

Der Client-Algorithmus:

  1. Generieren Sie ein zufälliges eindeutiges Token.
  2. Senden Sie die Download-Anforderung ab, und fügen Sie das Token in ein GET/POST-Feld ein.
  3. Zeigen Sie den Indikator "Warten" an.
  4. Starten Sie einen Timer, und suchen Sie etwa jede Sekunde nach einem Cookie mit dem Namen "fileDownloadToken" (oder wie immer Sie sich entscheiden).
  5. Wenn das Cookie vorhanden ist und sein Wert mit dem Token übereinstimmt, wird der Indikator "Warten" ausgeblendet.

Der Server-Algorithmus:

  1. Achten Sie auf das Feld GET/POST in der Anfrage.
  2. Wenn es einen nicht leeren Wert hat, legen Sie ein Cookie ab (z. B. "fileDownloadToken") und setzen dessen Wert auf den Wert des Tokens.

Client-Quellcode (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;
}

Beispiel für Server-Code (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();

Wo:

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

29voto

birdman Punkte 321

Eine sehr einfache (und langweilige) Ein-Zeilen-Lösung ist die Verwendung der window.onblur() Ereignis, um den Ladedialog zu schließen. Wenn es zu lange dauert und der Benutzer beschließt, etwas anderes zu tun (z. B. E-Mails zu lesen), wird der Ladedialog natürlich geschlossen.

23voto

CubicleSoft Punkte 1977

Das Kernproblem besteht darin, dass der Webbrowser nicht über ein Ereignis verfügt, das ausgelöst wird, wenn die Seitennavigation abgebrochen wird, wohl aber über ein Ereignis, das ausgelöst wird, wenn das Laden einer Seite abgeschlossen ist. Alles, was über ein direktes Browser-Ereignis hinausgeht, wird ein Hack mit Vor- und Nachteilen sein.

Es gibt vier bekannte Ansätze, um zu erkennen, wann ein Browser-Download beginnt:

  1. Rufen Sie fetch() auf, rufen Sie die gesamte Antwort ab, fügen Sie eine a Tag mit einer download Attribut und lösen ein Klick-Ereignis aus. Moderne Webbrowser bieten dem Benutzer dann die Möglichkeit, die bereits abgerufene Datei zu speichern. Dieser Ansatz hat mehrere Nachteile:
  • Der gesamte Datenblob wird im RAM gespeichert. Wenn die Datei also groß ist, verbraucht sie entsprechend viel RAM. Bei kleinen Dateien ist dies wahrscheinlich kein Grund zur Sorge.
  • Der Nutzer muss warten, bis die gesamte Datei heruntergeladen ist, bevor er sie speichern kann. Außerdem kann er die Seite nicht verlassen, bevor der Download abgeschlossen ist.
  • Der integrierte Datei-Downloader des Webbrowsers wird nicht verwendet.
  • Ein domänenübergreifender Abruf wird wahrscheinlich fehlschlagen, wenn keine CORS-Header gesetzt sind.
  1. Verwenden Sie einen iframe + ein Server-seitiges Cookie. Der iframe feuert eine load Ereignis, wenn eine Seite im Iframe geladen wird, anstatt einen Download zu starten, aber es werden keine Ereignisse ausgelöst, wenn der Download beginnt. Das Setzen eines Cookies beim Webserver kann dann von JavaScript in einer Schleife erkannt werden. Dieser Ansatz hat mehrere Nachteile:
  • Der Server und der Client müssen zusammenarbeiten. Der Server muss ein Cookie setzen. Der Client muss das Cookie erkennen.
  • Domänenübergreifende Anfragen können das Cookie nicht setzen.
  • Die Anzahl der Cookies, die pro Domain gesetzt werden können, ist begrenzt.
  • Benutzerdefinierte HTTP-Header können nicht gesendet werden.
  1. Verwenden Sie einen iframe mit URL-Weiterleitung. Der iframe startet eine Anfrage, und sobald der Server die Datei vorbereitet hat, gibt er ein HTML-Dokument aus, das eine Meta-Aktualisierung zu einer neuen URL durchführt, die den Download eine Sekunde später auslöst. Die Website load Ereignis für den iframe tritt ein, wenn das HTML-Dokument geladen wird. Dieser Ansatz hat mehrere Nachteile:
  • Der Server muss den Speicherplatz für die heruntergeladenen Inhalte vorhalten. Erfordert einen Cron-Job oder ähnliches, um das Verzeichnis regelmäßig aufzuräumen.
  • Der Server muss einen speziellen HTML-Inhalt ausgeben, wenn die Datei fertig ist.
  • Der Client muss erraten, wann der iframe die zweite Anfrage an den Server gestellt hat und wann der Download tatsächlich begonnen hat, bevor er den iframe aus dem DOM entfernt. Dies könnte umgangen werden, indem der iframe einfach im DOM belassen wird.
  • Benutzerdefinierte HTTP-Header können nicht gesendet werden.
  1. Verwenden Sie einen iframe + XHR . Der iframe löst die Download-Anforderung aus. Sobald die Anfrage über den iframe erfolgt, wird eine identische Anfrage über XHR gestellt. Wenn die load Ereignis auf dem iframe auslöst, ist ein Fehler aufgetreten, die XHR-Anforderung wird abgebrochen und der iframe entfernt. Wenn eine XHR progress Ereignis ausgelöst wird, hat das Herunterladen im iframe wahrscheinlich begonnen. Brechen Sie die XHR-Anforderung ab, warten Sie einige Sekunden und entfernen Sie dann den iframe. Auf diese Weise können größere Dateien heruntergeladen werden, ohne auf ein serverseitiges Cookie angewiesen zu sein. Dieser Ansatz hat mehrere Nachteile:
  • Es handelt sich um zwei getrennte Anträge für dieselben Informationen. Der Server kann die XHR von dem iframe unterscheiden, indem er die eingehenden Header überprüft.
  • Eine domänenübergreifende XHR-Anfrage wird wahrscheinlich fehlschlagen, wenn CORS Kopfzeilen gesetzt sind. Der Browser weiß jedoch erst dann, ob CORS erlaubt ist oder nicht, wenn der Server die HTTP-Header zurücksendet. Wenn der Server mit dem Senden der Header wartet, bis die Dateidaten fertig sind, kann die XHR auch ohne CORS ungefähr erkennen, wann der iframe mit dem Download begonnen hat.
  • Der Client muss erraten, wann der Download tatsächlich begonnen hat, um den iframe von der Website zu entfernen. DOM . Dies könnte überwunden werden, indem man den iframe einfach im DOM belässt.
  • Kann keine benutzerdefinierten Kopfzeilen an den Iframe senden.

Ohne ein entsprechendes eingebautes Webbrowser-Ereignis gibt es hier keine perfekten Lösungen. Je nach Anwendungsfall wird jedoch eine der vier oben genannten Methoden wahrscheinlich besser geeignet sein als die anderen.

Wann immer möglich, sollten Sie Antworten direkt an den Client streamen, anstatt alles erst auf dem Server zu generieren und dann die Antwort zu senden. Es können verschiedene Dateiformate gestreamt werden, z. B. CSV, JSON, XML, ZIP usw. Es kommt wirklich darauf an, eine Bibliothek zu finden, die Streaming-Inhalte unterstützt. Wenn die Antwort gestreamt wird, sobald die Anfrage beginnt, ist es nicht so wichtig, den Beginn des Downloads zu erkennen, da dieser fast sofort beginnt.

Eine andere Möglichkeit besteht darin, nur die Kopfzeilen für das Herunterladen auszugeben, anstatt darauf zu warten, dass zuerst der gesamte Inhalt generiert wird. Generieren Sie dann den Inhalt und beginnen Sie schließlich mit dem Senden an den Client. Der eingebaute Downloader des Benutzers wird geduldig warten, bis die Daten ankommen. Der Nachteil ist, dass die zugrunde liegende Netzwerkverbindung eine Zeitüberschreitung erleiden könnte, während sie auf den Beginn des Datenflusses wartet (entweder auf der Client- oder der Serverseite).

21voto

bytepirate Punkte 309

Diese Lösung ist sehr einfach, aber zuverlässig. Und sie ermöglicht es, echte Fortschrittsmeldungen anzuzeigen (und lässt sich leicht in bestehende Prozesse einbinden):

Das Skript, das die Verarbeitung durchführt (mein Problem war: Dateien über HTTP abrufen und als ZIP-Datei bereitstellen), schreibt den Status in die Sitzung.

Der Status wird jede Sekunde abgefragt und angezeigt. Das ist alles (OK, ist es nicht. Man muss sich um eine Menge Details kümmern (z.B. gleichzeitige Downloads), aber es ist ein guter Anfang ;-)).

Die Download-Seite:

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

Datei getstatus.php

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

Datei herunterladen.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
?>

19voto

Jerzy Gebler Punkte 869

Basierend auf Elmers Beispiel habe ich meine eigene Lösung vorbereitet. Nach einem Klick auf ein Element mit einem definierten " descargar "Klasse wird eine benutzerdefinierte Nachricht im Browserfenster angezeigt. Ich habe die Fokus Auslöser, um die Nachricht zu verbergen. Ich habe die Fokus auslösen, um die Nachricht zu verbergen.

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>

Jetzt sollten Sie ein beliebiges Element zum Herunterladen implementieren:

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

ou

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

Nach jedem descargar klicken, wird die Meldung angezeigt:
Ihr Bericht wird erstellt. Bitte warten Sie...

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X