2009-10-08 19 views
14

Estoy trabajando en una aplicación web en Python/Twisted.HTTP Descargar archivo muy grande

Quiero que el usuario pueda descargar un archivo muy grande (> 100 Mb). No quiero cargar todo el archivo en la memoria (del servidor), por supuesto.

lado del servidor tengo esta 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() 

que esperaba que esto funcione, pero tengo problemas: estoy probando con FF ... Parece que el navegador me hagas esperar hasta que el archivo es completado descargado, y luego tengo el cuadro de diálogo abrir/guardar.

Yo esperaba que el cuadro de diálogo de inmediato, y luego la barra de progreso en la acción ...

Tal vez tengo que añadir algo en la cabecera HTTP ... Algo así como el tamaño del archivo?

+1

Probablemente obtendrá un mejor rendimiento y menos carga en el servidor mediante la lectura y el envío trozos más grandes ... experimente con valores alrededor de 4-16k para encontrar lo que funciona mejor para sus circunstancias. – dcrosta

+0

¿Desea aceptar una de las respuestas? –

Respuesta

3

Sí, el encabezado Content-Length le dará la barra de progreso que desee.

+0

Dado que estoy enviando al navegador solo el contenido del archivo, Content-Lenth tiene exactamente el tamaño del archivo en Bytes. –

+0

Sí, según http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13 –

3

Si este es realmente el contenido text/plain, debe considerar seriamente enviarlo con Content-Encoding: gzip cada vez que un cliente indique que puede manejarlo. Deberías ver grandes ahorros de ancho de banda. Además, si se trata de un archivo estático, lo que realmente desea hacer es usar sendfile(2). En cuanto a los navegadores que no hacen lo que espera en términos de descarga, es posible que desee consultar el encabezado Content-Disposition. Así que de todos modos, la lógica es la siguiente:

Si el cliente indica que pueden manejar gzip codificación a través de la cabecera Accept-Encoding (por ejemplo Accept-Encoding: compress;q=0.5, gzip;q=1.0 o Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0 o similar) y luego comprimir el archivo, almacenar en caché el resultado comprimido en algún lugar, escribir los encabezados correctos para la respuesta (Content-Encoding: gzip, Content-Length: n, Content-Type: text/plain, etc.), y luego use sendfile(2) (sin embargo, esto puede o no estar disponible en su entorno) para copiar el contenido del descriptor de archivo abierto en su flujo de respuesta.

Si no aceptan gzip, hagan lo mismo, pero sin gzipping primero.

Alternativamente, si usted tiene Apache, Lighttpd, o de actuar similar a un proxy transparente delante de su servidor, puede utilizar la cabecera , que es muy rápido:

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

Dos grandes problemas con la El código de muestra que publicó es que no es cooperativo y carga todo el archivo en la memoria antes de enviarlo.

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

Recuerde que Twisted utiliza la multitarea cooperativa para lograr cualquier tipo de concurrencia. Entonces, el primer problema con este fragmento es que es un ciclo de tiempo sobre el contenido de un archivo completo (que usted dice que es grande). Esto significa que el archivo completo se leerá en la memoria y se escribirá en la respuesta antes de cualquier cosa que ocurra en el proceso. En este caso, sucede que "cualquier cosa" también incluye insertar los bytes del búfer en memoria en la red, por lo que su código también mantendrá todo el archivo en la memoria de una vez y solo comenzará a deshacerse de él cuando esto ocurra. lazo termina.

Por lo tanto, como regla general, no debe escribir código para usarlo en una aplicación basada en Twisted que utiliza un bucle como este para hacer un gran trabajo. En cambio, necesita hacer cada pequeña parte del gran trabajo de una manera que cooperará con el ciclo de eventos. Para enviar un archivo a través de la red, la mejor manera de abordar esto es con productores y consumidores. Estas son dos API relacionadas para mover grandes cantidades de datos en torno al uso de eventos de búfer vacío para hacerlo de manera eficiente y sin desperdiciar cantidades irrazonables de memoria.

puede encontrar alguna documentación de estas API aquí:

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

Afortunadamente, en este caso muy común, hay también un productor ya escrito que se puede utilizar, en lugar de aplicar su propia:

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

es posible que desee utilizarlo especie de la siguiente manera:

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 

Puede leer más sobre NOT_DONE_YET y otras ideas relacionadas con la serie "Twisted Web in 60 Seconds" en mi blog, http://jcalderone.livejournal.com/50562.html (ver las entradas de "respuestas asincrónicas" en particular).

+0

+1 ¡Guau! ¡Una respuesta fantástica! –

+0

Gracias por la sugerencia del productor/consumidor. –

1

Aquí es un ejemplo de la descarga de archivos en trozos usando urllib2, que se puede utilizar desde el interior de una llamada de función trenzado

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