18 votes

Créez et diffusez des archives volumineuses sans les stocker en mémoire ou sur disque.

Je souhaite permettre aux utilisateurs de télécharger en une seule fois une archive contenant plusieurs fichiers volumineux. Cependant, les fichiers et l'archive peuvent être trop volumineux pour être stockés en mémoire ou sur le disque de mon serveur (ils sont envoyés en continu depuis d'autres serveurs). J'aimerais générer l'archive au moment où je la transmets à l'utilisateur.

Je peux utiliser Tar ou Zip ou ce qui est le plus simple. J'utilise django, qui me permet de renvoyer un générateur ou un objet de type fichier dans ma réponse. Cet objet pourrait être utilisé pour faire avancer le processus. Cependant, j'ai du mal à comprendre comment construire ce genre de chose autour des bibliothèques zipfile ou tarfile, et j'ai peur qu'elles ne supportent pas la lecture des fichiers au fur et à mesure, ou la lecture de l'archive au fur et à mesure qu'elle est construite.

Cette réponse sur conversion d'un itérateur en un objet de type fichier pourrait aider. tarfile#addfile prend un itérable, mais il semble qu'il le passe immédiatement à shutil.copyfileobj donc ce n'est peut-être pas aussi convivial que je l'espérais.

9voto

Nick Retallack Points 5994

J'ai fini par utiliser SpiderOak ZipStream .

7voto

Pedro Werneck Points 3744

Vous pouvez le faire en générant et en diffusant un fichier zip sans compression, ce qui consiste essentiellement à ajouter les en-têtes avant le contenu de chaque fichier. Vous avez raison, les bibliothèques ne supportent pas cela, mais vous pouvez les contourner pour que cela fonctionne.

Ce code enveloppe zipfile.ZipFile avec une classe qui gère le flux et crée des instances de zipfile.ZipInfo pour les fichiers à mesure qu'ils arrivent. Le CRC et la taille peuvent être définis à la fin. Vous pouvez y insérer des données provenant du flux d'entrée avec put_file(), write() et flush(), et lire des données à partir de ce flux vers le flux de sortie avec 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()

Gardez à l'esprit que ce code n'était qu'une rapide preuve de concept et que je n'ai pas fait de développement ou de test supplémentaire une fois que j'ai décidé de laisser le serveur http lui-même gérer ce problème. Si vous décidez de l'utiliser, vous devriez vérifier que les dossiers imbriqués sont archivés correctement, ainsi que l'encodage des noms de fichiers (ce qui est toujours un problème avec les fichiers zip).

7voto

rectalogic Points 416

Vous pouvez transmettre un ZipFile à un fileobj de réponse Pylons ou Django en enveloppant le fileobj dans quelque chose de semblable à un fichier qui implémente tell() . Cela permettra de mettre en mémoire tampon chaque fichier individuel du zip, mais de diffuser le zip lui-même. Nous l'utilisons pour télécharger en continu un fichier zip rempli d'images, de sorte que nous ne mettons jamais en mémoire tampon plus d'une seule image.

Cet exemple est un flux vers sys.stdout . Pour les pylônes, utilisez response.body_file pour Django, vous pouvez utiliser l'option HttpResponse lui-même comme un fichier.

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

3voto

dm2013 Points 676

Voici la solution de Pedro Werneck (ci-dessus) mais avec une correction pour éviter de collecter toutes les données en mémoire ( read est un peu corrigée) :

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

alors vous pouvez utiliser stream_generator fonction comme un flux pour un fichier zip

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

exemple pour 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)

0voto

Michal Charemza Points 6269

Une option consiste à utiliser stream-zip (divulgation complète : écrit par moi)

En modifiant légèrement son exemple :

from datetime import datetime
from stream_zip import stream_zip, ZIP_64

def non_zipped_files():
    modified_at = datetime.now()
    perms = 0o600

    # Hard coded in this example, but in real cases could
    # for example yield data from a remote source
    def file_1_data():
        for i in range(0, 1000):
            yield b'Some bytes'

    def file_2_data():
        for i in range(0, 1000):
            yield b'Some bytes'

    yield 'my-file-1.txt', modified_at, perms, ZIP64, file_1_data()
    yield 'my-file-2.txt', modified_at, perms, ZIP64, file_2_data()

zipped_chunks = stream_zip(non_zipped_files())

# Can print each chunk, or return them to a client,
# say using Django's StreamingHttpResponse
for zipped_chunk in zipped_chunks:
    print(zipped_chunk)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X