1 # The software in this package is distributed under the GNU General
2 # Public License version 2 (with a special exception described below).
4 # A copy of GNU General Public License (GPL) is included in this distribution,
5 # in the file COPYING.GPL.
7 # As a special exception, if other files instantiate templates or use macros
8 # or inline functions from this file, or you compile this file and link it
9 # with other works to produce a work based on this file, this file
10 # does not by itself cause the resulting work to be covered
11 # by the GNU General Public License.
13 # However the source code for this file must still be made available
14 # in accordance with section (3) of the GNU General Public License.
16 # This exception does not invalidate any other reasons why a work based
17 # on this file might be covered by the GNU General Public License.
19 # Copyright (c) 2016-2018 Intra2net AG <info@intra2net.com>
21 """ Streamable version of zipfile
23 Python's :py:class:`zipfile.ZipFile` can only write to seekable streams
24 since version 3.5 and only implements adding files as wholes. This module
25 implements class :py:class:`ZipStream` which is a subclass of ZipFile that can
26 read from non-seekable input streams and write to non-seekable output streams.
30 from pyi2ncommon.zip_stream import ZipStream
31 with ZipStream(output_stream, 'w') as zip:
32 info = zip.create_zipinfo(big_file)
33 with open(big_file, 'rb') as input_stream: # always read binary!
34 zip.write_stream(input_stream, info)
36 .. codeauthor:: Intra2net AG <info@intra2net>
42 if sys.version_info.major < 3:
43 raise ImportError('Did not backport zipfile from python 3.5 to py2')
44 if sys.version_info.minor >= 6:
45 # imports for _write_stream_36
48 # imports for create_zipinfo, _write_stream_35 and _get_compressor
49 from stat import S_ISDIR
55 if sys.version_info.minor >= 5:
58 # backport of zipfile from python 3.5 to support stream output
59 from zipfile35 import *
61 from .type_helpers import isstr
64 # copied from zipfile.py
65 ZIP64_LIMIT = (1 << 31) - 1
68 def _get_compressor(compress_type):
69 """Copied fomr zipfile.py in py3.5 (cannot legally import)"""
70 if compress_type == ZIP_DEFLATED:
71 return zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
73 elif compress_type == ZIP_BZIP2:
74 return bz2.BZ2Compressor()
75 elif compress_type == ZIP_LZMA:
76 return LZMACompressor()
81 class BytesTellWrapper:
83 Wrapper around a write-only stream that supports tell but not seek
85 copy of zipfile._Tellable
87 def __init__(self, fp):
91 def write(self, data):
92 n = self.fp.write(data)
106 class ZipStream(ZipFile):
107 """Subclass of ZipFile that supports non-seekable input and output"""
109 def __init__(self, file, *args, **kwargs):
111 Create ZipStream instance which is like a ZipFile plus functions
112 create_zipinfo and write_stream.
114 ZipFile determines whether output stream can seek() and tell().
115 Unfortunately some streams (like sys.stdout.buffer when redirecting
116 output) seem to support these methods but only return 0 from tell.
118 :param bool force_wrap: force wrapping of output stream with
121 if 'force_wrap' in kwargs and kwargs['force_wrap']:
123 raise ValueError('force_wrap only makes sense for streams')
124 del kwargs['force_wrap']
125 super(ZipStream, self).__init__(BytesTellWrapper(file), *args,
128 super(ZipStream, self).__init__(file, *args, **kwargs)
130 def create_zipinfo(self, filename, arcname=None):
132 Create ZipInfo for given file
134 Optionally set arcname as name of file inside archive.
136 Adapted from zipfile.py in (ZipInfo.from_file in py3.6, ZipFile.write
139 if sys.version_info.major >= 3 and sys.version_info.minor >= 6:
140 return ZipInfo.from_file(filename, arcname)
142 st = os.stat(filename)
143 isdir = S_ISDIR(st.st_mode)
144 mtime = time.localtime(st.st_mtime)
145 date_time = mtime[0:6]
146 # Create ZipInfo instance to store file information
149 arcname = os.path.normpath(os.path.splitdrive(arcname)[1])
150 while arcname[0] in (os.sep, os.altsep):
151 arcname = arcname[1:]
154 zinfo = ZipInfo(arcname, date_time)
155 zinfo.external_attr = (st.st_mode & 0xFFFF) << 16 # Unix attributes
157 zinfo.compress_type = ZIP_STORED
159 zinfo.external_attr |= 0x10 # MS-DOS directory flag
161 zinfo.compress_type = self.compression
162 zinfo.file_size = st.st_size
166 def write_stream(self, src, zinfo):
168 Add data from byte stream stream src to archive with info in ZipInfo.
170 Param zinfo must be a ZipInfo, created e.g. with
171 :py:meth:`ZipStream.create_zipinfo`
173 Note: you cannot add directories this way (removed the corresponding
176 This is a shortened version of python's
177 :py:func:`zipfile.ZipFile.write`.
179 if sys.version_info.major >= 3 and sys.version_info.minor >= 6:
180 return self._write_stream_36(src, zinfo)
182 return self._write_stream_35(src, zinfo)
184 def _write_stream_35(self, src, zinfo):
185 """Implementation of _write_stream based on ZipFile.write (py 3.5)"""
188 "Attempt to write to ZIP archive that was already closed")
190 zinfo.flag_bits = 0x00
193 zinfo.header_offset = self.fp.tell() # Start of header bytes
194 if zinfo.compress_type == ZIP_LZMA:
195 # Compressed data includes an end-of-stream (EOS) marker
196 zinfo.flag_bits |= 0x02
198 self._writecheck(zinfo)
199 self._didModify = True
201 cmpr = _get_compressor(zinfo.compress_type)
202 zinfo.flag_bits |= 0x08
204 # Must overwrite CRC and sizes with correct data later
206 zinfo.compress_size = compress_size = 0
207 # Compressed size can be larger than uncompressed size
208 zip64 = self._allowZip64 and \
209 zinfo.file_size * 1.05 > ZIP64_LIMIT
210 self.fp.write(zinfo.FileHeader(zip64))
213 buf = src.read(1024 * 8)
216 file_size = file_size + len(buf)
217 CRC = crc32(buf, CRC) & 0xffffffff
219 buf = cmpr.compress(buf)
220 compress_size = compress_size + len(buf)
224 compress_size = compress_size + len(buf)
226 zinfo.compress_size = compress_size
228 zinfo.compress_size = file_size
230 zinfo.file_size = file_size
232 # Write CRC and file sizes after the file data
233 fmt = '<LQQ' if zip64 else '<LLL'
234 self.fp.write(struct.pack(fmt, zinfo.CRC, zinfo.compress_size,
236 self.start_dir = self.fp.tell()
237 self.filelist.append(zinfo)
238 self.NameToInfo[zinfo.filename] = zinfo
240 def _write_stream_36(self, src, zinfo):
241 """Implementation of _write_stream based on ZipFile.write (py 3.6)"""
244 "Attempt to write to ZIP archive that was already closed")
247 "Can't write to ZIP archive while an open writing handle exists"
251 raise ValueError('streaming a dir entry does not make sense')
252 if zinfo.compress_type is None:
253 zinfo.compress_type = self.compression
255 with self.open(zinfo, 'w') as dest:
256 shutil.copyfileobj(src, dest, 1024*8)