2009-10-08 16 views
14

Sto lavorando a un'applicazione Web in Python/Twisted.HTTP Download file molto grande

Desidero che l'utente sia in grado di scaricare un file molto grande (> 100 Mb). Non voglio caricare tutto il file in memoria (del server), ovviamente.

lato server ho questa idea:

... 
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() 

mi aspettavo questo lavoro, ma non ho problemi: sto testando con FF ... Sembra il browser farmi aspettare fino a quando il file è completato scaricato, e quindi ho la finestra di dialogo Apri/Salva.

mi aspettavo la finestra di dialogo immediatamente, e quindi la barra di avanzamento in azione ...

Forse devo aggiungere qualcosa nell'intestazione HTTP ... Qualcosa come la dimensione del file?

+1

Probabilmente otterrete una migliore produttività e meno carico sul server attraverso la lettura e l'invio di pezzi più grandi ... sperimenta valori intorno a 4-16k per trovare ciò che funziona meglio per le tue circostanze. – dcrosta

+0

Vuoi accettare una delle risposte? –

risposta

3

Sì, l'intestazione Content-Length ti darà la barra di progresso che desideri!

+0

Poiché sto inviando al browser solo il contenuto del file, Content-Lenth è esattamente la dimensione del file in byte? –

+0

Sì, come per http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13 –

3

Se questo è davvero il contenuto di text/plain, si dovrebbe prendere seriamente in considerazione l'invio con Content-Encoding: gzip ogni volta che un client indica che possono gestirlo. Dovresti vedere enormi risparmi sulla larghezza di banda. Inoltre, se si tratta di un file statico, ciò che si vuole veramente fare è usare sendfile(2). Per quanto riguarda i browser che non fanno quello che ci si aspetta in termini di download di cose, si potrebbe voler guardare l'intestazione Content-Disposition. Quindi in ogni caso, la logica è questa:

Se il cliente indica che possono gestire gzip codifica tramite l'intestazione Accept-Encoding (ad es Accept-Encoding: compress;q=0.5, gzip;q=1.0 o Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0 o simile), poi comprimere il file, memorizzare nella cache il risultato compressa da qualche parte, scrivere le intestazioni corrette per la risposta (Content-Encoding: gzip, Content-Length: n, Content-Type: text/plain, ecc.) e quindi utilizzare sendfile(2) (che tuttavia potrebbe non essere stato reso disponibile nel proprio ambiente) per copiare il contenuto dal descrittore di file aperto nel flusso di risposta.

Se non accettano gzip, fare la stessa cosa, ma senza prima gzip.

In alternativa, se si dispone di Apache, Lighttpd, o che agisce simile come un proxy trasparente di fronte al vostro server, è possibile utilizzare il intestazione, che è estremamente veloce:

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

Due grossi problemi con la codice di esempio che hai postato è che non è cooperativo e carica l'intero file in memoria prima di inviarlo.

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

Ricordare che Twisted utilizza il multitasking cooperativo per raggiungere qualsiasi tipo di concorrenza. Quindi il primo problema con questo snippet è che è un ciclo while sul contenuto di un intero file (che tu dici è grande). Ciò significa che l'intero file verrà letto in memoria e scritto nella risposta prima dello qualsiasi cosa possa accadere nel caso. In questo caso, accade che "qualsiasi cosa" include anche il push dei byte dal buffer in memoria sulla rete, quindi il tuo codice terrà anche l'intero file in memoria in una volta e inizierà solo a sbarazzarsi di esso quando questo ciclo completo.

Quindi, come regola generale, non si dovrebbe scrivere codice per l'uso in un'applicazione basata su Twisted che utilizza un ciclo come questo per fare un grande lavoro. Invece, devi fare ogni piccola parte del grande lavoro in un modo che coopererà con il ciclo degli eventi. Per inviare un file attraverso la rete, il modo migliore per avvicinarsi a questo è con produttori e consumatori. Queste sono due API correlate per lo spostamento di grandi quantità di dati attorno all'utilizzo di eventi buffer-empty per farlo in modo efficiente e senza sprecare quantità irragionevoli di memoria.

È possibile trovare della documentazione di queste API qui:

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

Fortunatamente, per questo caso molto comune, c'è anche un produttore scritto già che è possibile utilizzare, piuttosto che attuare il proprio:

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

probabilmente si desidera utilizzare un po 'come questo:

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 

È possibile leggere ulteriori informazioni su NOT_DONE_YET e altre idee correlate sulla serie "Twisted Web in 60 Seconds" sul mio blog, http://jcalderone.livejournal.com/50562.html (vedere le voci "risposte asincrone" in particolare).

+0

+1 Wow - una risposta fantastica e completa! –

+0

Grazie per il suggerimento produttore/consumatore. –

1

Ecco un esempio di download dei file in blocchi usando urllib2, che è possibile utilizzare all'interno di una funzione di chiamata contorto

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