我想允许用户一次下载多个大文件的存档。但是,文件和存档可能太大而无法存储在我的服务器的内存或磁盘上(它们可以动态地从其他服务器流入)。我想在将其传输给用户时生成存档。
我可以使用Tar或Zip或其他最简单的东西。我正在使用django,它允许我在我的响应中返回一个生成器或类文件对象。该对象可用于泵送过程。但是,我无法弄清楚如何围绕zipfile或tarfile库构建这种东西,我担心它们可能不支持在文件读取时读取文件,或者在构建时读取存档。
converting an iterator to a file-like object上的这个答案可能有所帮助。 tarfile#addfile
采用可迭代的方式,但似乎会立即将其传递给shutil.copyfileobj
,因此这可能不像我希望的那样对生成器友好。
答案 0 :(得分:8)
我最终使用了SpiderOak ZipStream。
答案 1 :(得分:7)
你可以通过生成和流式传输没有压缩的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服务器本身处理这个问题,我就没有进行进一步的开发或测试。如果你决定使用它,你应该考虑的一些事情是检查嵌套文件夹是否正确存档,以及文件名编码(无论如何,这总是令人痛苦的拉链文件)。
答案 2 :(得分:7)
您可以通过将fileobj包装在类似于实现tell()
的文件中,将ZipFile流式传输到Pylons或Django响应文件框架。这将缓冲内存中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()
答案 3 :(得分:3)
以下是Pedro Werneck的解决方案(来自上方),但有一个修复程序可以避免收集内存中的所有数据(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)
答案 4 :(得分:0)
对于python 3,您可以使用Zip-Streaming https://github.com/BuzonIO/zip_streaming
"start": "next start -p $PORT"
和
pip3 install zip-streaming==1.4