2012-05-01 18 views
12

Quiero permitir a los usuarios descargar un archivo de varios archivos grandes al mismo tiempo. Sin embargo, los archivos y el archivo pueden ser demasiado grandes para almacenarlos en la memoria o en el disco de mi servidor (se transmiten desde otros servidores sobre la marcha). Me gustaría generar el archivo mientras lo transmito al usuario.Crear y transmitir un archivo grande sin almacenarlo en la memoria o en el disco

Puedo usar Tar o Zip o lo que sea más simple. Estoy usando django, que me permite devolver un generador o un objeto similar a un archivo en mi respuesta. Este objeto podría usarse para impulsar el proceso. Sin embargo, tengo problemas para descubrir cómo crear este tipo de cosas en las bibliotecas de archivo zip o tarfile, y me temo que es posible que no sean compatibles con la lectura de archivos a medida que avanzan o al leer el archivo tal como está desarrollado.

Esta respuesta en converting an iterator to a file-like object podría ayudar. tarfile#addfile toma un iterable, pero parece pasarlo inmediatamente al shutil.copyfileobj, por lo que puede que no sea tan favorable para el generador como esperaba.

+0

En general, las herramientas de compresión como zip o tar necesitan leer todo el archivo de entrada para determinar qué se puede y se debe comprimir. Entonces creo que tu idea de arquitectura es defectuosa. – jedwards

+11

@jedwards: bastante mal; 'tar' es solo un contenedor, sin compresión. Fue diseñado para trabajar con _tapes_ - donde leer la cosa completa primero estaba fuera de discusión. Y 'zlib' comprimirá _happy_ una secuencia de datos. Puede obtener una mejor compresión con un archivo completo, pero eso de ninguna manera es obligatorio. – sarnold

+0

@sarnold, supuse que se refería a un archivo comprimido ya que estaba hablando de compresión. Y zlib aún necesita almacenar en caché una cantidad de bytes de entrada antes de generar resultados porque el mismo requerimiento de analizar los datos de entrada permanece. Así que estoy de acuerdo en que me equivoqué con "todo", pero sigo sosteniendo que esto tiene poco sentido ya que la cantidad de datos que ahorrará al comprimir segmentos de flujo pequeños será insignificante para la cantidad de trabajo dedicado a la escritura. – jedwards

Respuesta

5

Puede hacerlo generando y transmitiendo un archivo zip sin compresión, que básicamente consiste en agregar los encabezados antes del contenido de cada archivo. Tienes razón, las bibliotecas no son compatibles con esto, pero puedes hackearlas para que funcione.

Este código envuelve zipfile.ZipFile con una clase que gestiona la secuencia y crea instancias de zipfile.ZipInfo para los archivos tal como vienen. CRC y tamaño pueden establecerse al final. Puede insertar datos de la secuencia de entrada en él con put_file(), write() y flush(), y leer datos de la misma en la secuencia de salida con read().

import struct  
import zipfile 
import time 

from StringIO import StringIO 

class ZipStreamer(object): 
    def __init__(self): 
     self.out_stream = StringIO() 

     # write to the stringIO with no compression 
     self.zipfile = zipfile.ZipFile(self.out_stream, 'w', zipfile.ZIP_STORED) 

     self.current_file = None 

     self._last_streamed = 0 

    def put_file(self, name, date_time=None): 
     if date_time is None: 
      date_time = time.localtime(time.time())[:6] 

     zinfo = zipfile.ZipInfo(name, date_time) 
     zinfo.compress_type = zipfile.ZIP_STORED 
     zinfo.flag_bits = 0x08 
     zinfo.external_attr = 0600 << 16 
     zinfo.header_offset = self.out_stream.pos 

     # write right values later 
     zinfo.CRC = 0 
     zinfo.file_size = 0 
     zinfo.compress_size = 0 

     self.zipfile._writecheck(zinfo) 

     # write header to stream 
     self.out_stream.write(zinfo.FileHeader()) 

     self.current_file = zinfo 

    def flush(self): 
     zinfo = self.current_file 
     self.out_stream.write(struct.pack("<LLL", zinfo.CRC, zinfo.compress_size, zinfo.file_size)) 
     self.zipfile.filelist.append(zinfo) 
     self.zipfile.NameToInfo[zinfo.filename] = zinfo 
     self.current_file = None 

    def write(self, bytes): 
     self.out_stream.write(bytes) 
     self.out_stream.flush() 
     zinfo = self.current_file 
     # update these... 
     zinfo.CRC = zipfile.crc32(bytes, zinfo.CRC) & 0xffffffff 
     zinfo.file_size += len(bytes) 
     zinfo.compress_size += len(bytes) 

    def read(self): 
     i = self.out_stream.pos 

     self.out_stream.seek(self._last_streamed) 
     bytes = self.out_stream.read() 

     self.out_stream.seek(i) 
     self._last_streamed = i 

     return bytes 

    def close(self): 
     self.zipfile.close() 

Tenga en cuenta que este código fue sólo una prueba rápida del concepto y lo hice sin ulterior desarrollo o de prueba una vez que decidí dejar que el propio servidor HTTP frente a este problema. Algunas cosas que debe considerar si decide usarlas es verificar si las carpetas anidadas están archivadas correctamente, y la codificación de los nombres de archivo (que siempre es un problema con los archivos zip de todos modos).

+0

Una cosa que me preocupa es que cuando utilizas StringIO, ¿terminará recopilando todos los datos en la memoria? ¿Las cosas que ya han sido leídas de StringIO alguna vez se liberan? –

+0

Probablemente tengas razón, pero StringIO no es esencial para esta implementación, solo la más fácil de usar. Puede hacer que un archivo como un objeto trate siempre con el último fragmento. –

4

Puede transmitir un archivo ZipFile a un archivo de respuestas de Pylons o Django al envolver el archivoobj en un archivo similar al que implementa tell(). Esto almacenará en búfer cada archivo individual en el archivo comprimido en la memoria, pero transmite el mismo zip. Lo usamos para transmitir un archivo zip lleno de imágenes, por lo que nunca almacenamos más de una imagen en la memoria.

Este ejemplo se transmite a sys.stdout. Para Pylons use response.body_file, para Django puede usar el HttpResponse mismo como un archivo.

import zipfile 
import sys 


class StreamFile(object): 
    def __init__(self, fileobj): 
     self.fileobj = fileobj 
     self.pos = 0 

    def write(self, str): 
     self.fileobj.write(str) 
     self.pos += len(str) 

    def tell(self): 
     return self.pos 

    def flush(self): 
     self.fileobj.flush() 


# Wrap a stream so ZipFile can use it 
out = StreamFile(sys.stdout) 
z = zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) 

for i in range(5): 
    z.writestr("hello{0}.txt".format(i), "this is hello{0} contents\n".format(i) * 3) 

z.close() 
0

Aquí está la solución de Pedro Werneck (desde arriba) pero con un arreglo para evitar la recogida de todos los datos en la memoria (read método se fija un poco):

class ZipStreamer(object): 
    def __init__(self): 
     self.out_stream = StringIO.StringIO() 

     # write to the stringIO with no compression 
     self.zipfile = zipfile.ZipFile(self.out_stream, 'w', zipfile.ZIP_STORED) 

     self.current_file = None 

     self._last_streamed = 0 

    def put_file(self, name, date_time=None): 
     if date_time is None: 
      date_time = time.localtime(time.time())[:6] 

     zinfo = zipfile.ZipInfo(name, date_time) 
     zinfo.compress_type = zipfile.ZIP_STORED 
     zinfo.flag_bits = 0x08 
     zinfo.external_attr = 0600 << 16 
     zinfo.header_offset = self.out_stream.pos 

     # write right values later 
     zinfo.CRC = 0 
     zinfo.file_size = 0 
     zinfo.compress_size = 0 

     self.zipfile._writecheck(zinfo) 

     # write header to mega_streamer 
     self.out_stream.write(zinfo.FileHeader()) 

     self.current_file = zinfo 

    def flush(self): 
     zinfo = self.current_file 
     self.out_stream.write(
      struct.pack("<LLL", zinfo.CRC, zinfo.compress_size, 
         zinfo.file_size)) 
     self.zipfile.filelist.append(zinfo) 
     self.zipfile.NameToInfo[zinfo.filename] = zinfo 
     self.current_file = None 

    def write(self, bytes): 
     self.out_stream.write(bytes) 
     self.out_stream.flush() 
     zinfo = self.current_file 
     # update these... 
     zinfo.CRC = zipfile.crc32(bytes, zinfo.CRC) & 0xffffffff 
     zinfo.file_size += len(bytes) 
     zinfo.compress_size += len(bytes) 

    def read(self): 
     self.out_stream.seek(self._last_streamed) 
     bytes = self.out_stream.read() 
     self._last_streamed = 0 

     # cleaning up memory in each iteration 
     self.out_stream.seek(0) 
     self.out_stream.truncate() 
     self.out_stream.flush() 

     return bytes 

    def close(self): 
     self.zipfile.close() 

continuación, puede utilizar stream_generator función como una corriente de un archivo zip de

def stream_generator(files_paths): 
    s = ZipStreamer() 
    for f in files_paths: 
     s.put_file(f) 
     with open(f) as _f: 
      s.write(_f.read()) 
     s.flush() 
     yield s.read() 
    s.close() 

ejemplo para Falcon:

class StreamZipEndpoint(object): 
    def on_get(self, req, resp): 
     files_pathes = [ 
      '/path/to/file/1', 
      '/path/to/file/2', 
     ] 
     zip_filename = 'output_filename.zip' 
     resp.content_type = 'application/zip' 
     resp.set_headers([ 
      ('Content-Disposition', 'attachment; filename="%s"' % (
       zip_filename,)) 
     ]) 

     resp.stream = stream_generator(files_pathes) 
Cuestiones relacionadas