2009-10-08 15 views
14

Ich arbeite an einer Webanwendung in Python/Twisted.HTTP Download sehr große Datei

Ich möchte der Benutzer eine sehr große Datei (> 100 Mb) herunterladen können. Ich möchte natürlich nicht alle Dateien im Speicher (des Servers) laden.

Server-Seite ich diese Idee haben:

... 
request.setHeader('Content-Type', 'text/plain') 
fp = open(fileName, 'rb') 
try: 
    r = None 
    while r != '': 
     r = fp.read(1024) 
     request.write(r) 
finally: 
    fp.close() 
    request.finish() 

ich dies erwartet zu arbeiten, aber ich habe Probleme: ich mit FF Testen bin ... Es scheint der Browser mich warten lassen, bis die Datei fertig heruntergeladen, und dann habe ich das Öffnen/Speichern-Dialogfeld.

erwartete ich das Dialogfeld sofort, und dann den Fortschrittsbalken in Aktion ...

Vielleicht muss ich etwas in der Http-Header ... So etwas wie die Größe der Datei hinzufügen?

+1

Sie werden wahrscheinlich einen besseren Durchsatz und weniger Last auf dem Server erhalten, indem das Lesen und Senden größere Stücke ... experimentiere mit Werten um 4-16k, um herauszufinden, was für deine Umstände am besten geeignet ist. – dcrosta

+0

Möchten Sie eine der Antworten akzeptieren? –

Antwort

3

Ja, die Content-Length-Kopfzeile gibt Ihnen den Fortschrittsbalken, den Sie wünschen!

+0

Da ich nur den Inhalt der Datei an den Browser sende, entspricht Content-Lenth genau der Größe der Datei in Bytes? –

+0

Ja, laut http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13 –

3

Wenn dies wirklich text/plain Inhalt ist, sollten Sie ernsthaft in Betracht ziehen, es mit Content-Encoding: gzip senden, wenn ein Client angibt, sie können damit umgehen. Sie sollten große Bandbreiteneinsparungen sehen. Wenn es sich um eine statische Datei handelt, möchten Sie außerdem unbedingt sendfile(2) verwenden. Für Browser, die nicht das tun, was Sie in Bezug auf das Herunterladen von Dingen erwarten, sollten Sie sich den Header Content-Disposition ansehen. So jedenfalls geht die Logik wie folgt aus:

Wenn der Kunde angibt, können sie gzip Codierung über den Accept-Encoding Header verarbeiten (zB Accept-Encoding: compress;q=0.5, gzip;q=1.0 oder Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0 oder ähnlich), dann die Datei komprimieren, zwischenzuspeichern das komprimierte Ergebnis irgendwo, schreiben die richtigen Header für B. die Antwort (Content-Encoding: gzip, Content-Length: n, Content-Type: text/plain, usw.) und dann sendfile(2) (die in Ihrer Umgebung verfügbar sein kann oder nicht), um den Inhalt aus dem geöffneten Dateideskriptor in Ihren Antwortstream zu kopieren.

Wenn sie nicht akzeptieren gzip, das gleiche tun, aber ohne zuerst Gzipping.

Alternativ, wenn Sie Apache haben, Lighttpd, oder ähnlich, der als transparenter Proxy vor dem Server, können Sie den Header verwenden, die äußerst schnell:

response.setHeader('Content-Type', 'text/plain') 
response.setHeader(
    'Content-Disposition', 
    'attachment; filename="' + os.path.basename(fileName) + '"' 
) 
response.setHeader('X-Sendfile', fileName) 
response.setHeader('Content-Length', os.stat(fileName).st_size) 
35

Zwei große Probleme mit der Beispielcode, den Sie gepostet haben, ist, dass es nicht kooperativ ist und es die gesamte Datei in den Speicher lädt, bevor es gesendet wird.

while r != '': 
    r = fp.read(1024) 
    request.write(r) 

Denken Sie daran, dass Twisted kooperatives Multitasking verwendet, um jede Art von Nebenläufigkeit zu erreichen. Also das erste Problem mit diesem Snippet ist, dass es eine while-Schleife über den Inhalt einer ganzen Datei ist (von der Sie sagen, dass sie groß ist). Dies bedeutet, dass die gesamte Datei in den Speicher gelesen und in die Antwort geschrieben wird, bevor irgendetwas sonst kann in dem Prozess passieren. In diesem Fall passiert es, dass "irgendwas" auch das Schieben der Bytes aus dem In-Memory-Puffer auf das Netzwerk beinhaltet, so dass Ihr Code auch die gesamte Datei im Speicher hält und sie nur dann los wird, wenn dies geschieht Schleife wird abgeschlossen.

Also, als allgemeine Regel sollten Sie nicht Code für die Verwendung in einer Twisted-basierten Anwendung schreiben, die eine Schleife wie diese verwendet, um eine große Arbeit zu tun. Stattdessen müssen Sie jedes kleine Stück des großen Auftrags so bearbeiten, dass es mit der Ereignisschleife zusammenarbeitet. Für das Senden einer Datei über das Netzwerk ist der beste Weg, dies zu erreichen, mit Produzenten und Verbraucher. Dies sind zwei verwandte APIs zum Verschieben großer Datenmengen mithilfe leerer Pufferereignisse, um sie effizient und ohne unnötige Speichermengen zu verarbeiten.

Sie eine Dokumentation dieser APIs finden Sie hier:

http://twistedmatrix.com/projects/core/documentation/howto/producers.html

Zum Glück für diese sehr häufigen Fall, gibt es auch ein Hersteller bereits geschrieben, dass Sie verwenden können, anstatt Ihre eigene Implementierung:

http://twistedmatrix.com/documents/current/api/twisted.protocols.basic.FileSender.html

Sie wollen wahrscheinlich es so eine Art verwenden:

from twisted.protocols.basic import FileSender 
from twisted.python.log import err 
from twisted.web.server import NOT_DONE_YET 

class Something(Resource): 
    ... 

    def render_GET(self, request): 
     request.setHeader('Content-Type', 'text/plain') 
     fp = open(fileName, 'rb') 
     d = FileSender().beginFileTransfer(fp, request) 
     def cbFinished(ignored): 
      fp.close() 
      request.finish() 
     d.addErrback(err).addCallback(cbFinished) 
     return NOT_DONE_YET 

Sie können mehr über NOT_DONE_YET und andere verwandte Ideen die "Twisted Web in 60 Sekunden" -Serie auf meinem Blog, http://jcalderone.livejournal.com/50562.html (siehe die "asynchrone Antworten" Einträge insbesondere). Hier

+0

+1 Wow - eine fantastische und durchgehende Antwort! –

+0

Danke für den Hersteller/Verbraucher Hinweis. –

1

ist ein Beispiel für Dateien in Chunks Download mit urllib2, die Sie aus dem Inneren eines verdrehten Funktionsaufruf verwenden könnte

import os 
import urllib2 
import math 

def downloadChunks(url): 
    """Helper to download large files 
     the only arg is a url 
     this file will go to a temp directory 
     the file will also be downloaded 
     in chunks and print out how much remains 
    """ 

    baseFile = os.path.basename(url) 

    #move the file to a more uniq path 
    os.umask(0002) 
    temp_path = "/tmp/" 
    try: 
     file = os.path.join(temp_path,baseFile) 

     req = urllib2.urlopen(url) 
     total_size = int(req.info().getheader('Content-Length').strip()) 
     downloaded = 0 
     CHUNK = 256 * 10240 
     with open(file, 'wb') as fp: 
      while True: 
       chunk = req.read(CHUNK) 
       downloaded += len(chunk) 
       print math.floor((downloaded/total_size) * 100) 
       if not chunk: break 
       fp.write(chunk) 
    except urllib2.HTTPError, e: 
     print "HTTP Error:",e.code , url 
     return False 
    except urllib2.URLError, e: 
     print "URL Error:",e.reason , url 
     return False 

    return file