Подтвердить что ты не робот

Создавайте и передавайте большой архив, не сохраняя его в памяти или на диске.

Я хочу разрешить пользователям сразу скачивать архив из нескольких больших файлов. Однако файлы и архив могут быть слишком большими для хранения в памяти или на диске на моем сервере (они передаются с других серверов "на лету" ). Я хотел бы сгенерировать архив, когда я передам его пользователю.

Я могу использовать Tar или Zip или что-то самое простое. Я использую django, который позволяет мне возвращать генератор или файл-подобный объект в моем ответе. Этот объект можно использовать для накачки процесса. Тем не менее, мне трудно понять, как создавать подобные вещи в библиотеках zipfile или tarfile, и я боюсь, что они могут не поддерживать чтение файлов по мере их поступления или чтение архива по мере его создания.

Этот ответ на преобразование итератора в файл-подобный объект может помочь. tarfile#addfile принимает итерабельность, но, похоже, она сразу передает это значение в shutil.copyfileobj, поэтому это может быть не так, как хотелось бы, для генератора.

4b9b3361

Ответ 2

Вы можете сделать это, создав и потоковой передачи zip файла без сжатия, что в основном просто добавляет заголовки перед каждым содержимым файла. Вы правы, библиотеки не поддерживают это, но вы можете взломать их, чтобы заставить его работать.

Этот код обертывает zipfile.ZipFile классом, который управляет потоком и создает экземпляры zipfile.ZipInfo для файлов по мере их поступления. CRC и размер могут быть установлены в конце. Вы можете передавать данные из входного потока в него с помощью put_file(), write() и flush() и считывать данные из него в выходной поток с помощью 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()

Имейте в виду, что этот код был просто кратким доказательством концепции, и я больше не разрабатывал и не тестировал, как только я решил позволить самому серверу http справиться с этой проблемой. Несколько вещей, на которые вы должны обратить внимание, если вы решите использовать его, - проверить правильность архивирования вложенных папок и кодировку имени файла (это всегда больно с zip файлами).

Ответ 3

Вы можете передать ZipFile в файл pylons или Django fileobj, обернув fileobj в нечто вроде файла, которое реализует tell(). Это будет буферизовать каждый отдельный файл в zip в памяти, но поток самого zip. Мы используем его для потоковой загрузки zip файла, полного изображений, поэтому мы никогда не буферируем больше одного изображения в памяти.

В этом примере поток переходит к sys.stdout. Для Pylons используйте response.body_file, для Django вы можете использовать HttpResponse как файл.

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

Ответ 4

Вот решение от Педро Вернека (сверху), но с исправлением, чтобы избежать сбора всех данных в памяти (метод read исправлен немного):

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

то вы можете использовать функцию stream_generator как поток для 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()

пример для 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)