Ответ 1
В итоге я использовал SpiderOak ZipStream.
Я хочу разрешить пользователям сразу скачивать архив из нескольких больших файлов. Однако файлы и архив могут быть слишком большими для хранения в памяти или на диске на моем сервере (они передаются с других серверов "на лету" ). Я хотел бы сгенерировать архив, когда я передам его пользователю.
Я могу использовать Tar или Zip или что-то самое простое. Я использую django, который позволяет мне возвращать генератор или файл-подобный объект в моем ответе. Этот объект можно использовать для накачки процесса. Тем не менее, мне трудно понять, как создавать подобные вещи в библиотеках zipfile или tarfile, и я боюсь, что они могут не поддерживать чтение файлов по мере их поступления или чтение архива по мере его создания.
Этот ответ на преобразование итератора в файл-подобный объект может помочь. tarfile#addfile
принимает итерабельность, но, похоже, она сразу передает это значение в shutil.copyfileobj
, поэтому это может быть не так, как хотелось бы, для генератора.
В итоге я использовал SpiderOak ZipStream.
Вы можете сделать это, создав и потоковой передачи 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 файлами).
Вы можете передать 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()
Вот решение от Педро Вернека (сверху), но с исправлением, чтобы избежать сбора всех данных в памяти (метод 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)