Вопрос по http, python – Создание и потоковая передача большого архива без сохранения его в памяти или на диске

14

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

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

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

В общем, утилиты сжатия, такие как zip или tar, должны прочитать весь входной файл, чтобы определить, что можно и нужно сжимать. Так что я думаю, что ваша архитектурная идея ошибочна. jedwards
@ jedwards Я никогда не упоминал сжатие. Сжатие может быть обработано apache mod_gzip. Также я не знаю, что вы имеете в виду под "сохранением данных". Цель здесь состоит в том, чтобы уменьшить использование памяти на сервере, позволяя ему просто передавать данные из одного места в другое, не удерживая слишком много данных одновременно. Nick Retallack
@jedwards: совершенно неправильно;tar это просто контейнер, без сжатия. Он был разработан для работы сtapes - о том, где читать все сначала не может быть и речи. А такжеzlib будутhappily сжать поток данных. Ты можешь получитьbetter сжатие с полным знанием файла, но это ни в коем случае не обязательно. sarnold
@jedwards, чем больше нюансов отклика, тем лучше, но учтите, поступают ли данные с радиоантенны или микрофона - может быть достаточно возможностей для сохранения потоковых данных в формате, более удобном для передачи и распаковки, без предварительного сохранения всех данных для анализа. sarnold
@sarnold, я предположил, что он имел в виду сжатый тарбол, поскольку говорил о сжатии. И zlib по-прежнему необходимо кэшировать несколько входных байтов перед генерацией вывода, потому что остается то же самое требование анализа входных данных. Так что я согласен, что я неправильно сказал «целую» но я по-прежнему считаю, что в этом нет особого смысла, поскольку объем данных, которые вы будете сохранять путем сжатия небольших сегментов потока, будет незначительным для объема работы, затраченной на написание. jedwards

Ваш Ответ

4   ответа
8
Это код GPLv3.
7

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

Этот код объединяет 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-файлами).

Вы, вероятно, правы, но StringIO не является существенным для этой реализации, он прост в использовании. Вы можете создать файл как объект, который всегда имеет дело только с последним фрагментом.
Одна вещь, о которой я беспокоюсь, это то, что, когда вы используете StringIO, в итоге все данные будут храниться в памяти? Освобождается ли материал, который уже был прочитан из StringIO? Nick Retallack
7

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

Этот пример передаетsys.stdout, Для пилонов использоватьresponse.body_file, для Джанго вы можете использовать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()
Но что вы делаете, когдаthis is hello{0} contents\n размер 10GB?
3

но с исправлением, чтобы избежать сбора всех данных в памяти (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
        zi,nfo.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()

пример длясокол:

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(fi,les_pathes)
Писать данные в zip-файл в последовательных порциях следуетeasy, Объекты ZipInfo должны просто поддерживать & quot; запись & quot; операции, если они находятся в конце (последний добавленный) архива.

Похожие вопросы