Clean up, remove compat with py < 3.6
[pyi2ncommon] / src / zip_stream.py
CommitLineData
f365f614
CH
1# The software in this package is distributed under the GNU General
2# Public License version 2 (with a special exception described below).
3#
4# A copy of GNU General Public License (GPL) is included in this distribution,
5# in the file COPYING.GPL.
6#
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.
12#
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.
15#
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.
18#
19# Copyright (c) 2016-2018 Intra2net AG <info@intra2net.com>
20
c2bdf47b
CH
21""" Streamable version of zipfile
22
23Python's :py:class:`zipfile.ZipFile` can only write to seekable streams
24since version 3.5 and only implements adding files as wholes. This module
25implements class :py:class:`ZipStream` which is a subclass of ZipFile that can
26read from non-seekable input streams and write to non-seekable output streams.
27
a294b6a7
CH
28Use as follows::
29
30 from pyi2ncommon.zip_stream import ZipStream
31 with ZipStream(output_stream, 'w') as zip:
32 info = zip.create_zipinfo(big_file)
e33dd338 33 with open(big_file, 'rb') as input_stream: # always read binary!
a294b6a7 34 zip.write_stream(input_stream, info)
e33dd338
CH
35
36.. codeauthor:: Intra2net AG <info@intra2net>
c2bdf47b
CH
37"""
38
7628bc48
CH
39import shutil
40from zipfile import *
e33dd338 41
1242b1cf 42from .type_helpers import isstr
c2bdf47b 43
a294b6a7 44
667e02f4
CH
45class BytesTellWrapper:
46 """
47 Wrapper around a write-only stream that supports tell but not seek
48
49 copy of zipfile._Tellable
50 """
51 def __init__(self, fp):
52 self.fp = fp
53 self.offset = 0
54
55 def write(self, data):
56 n = self.fp.write(data)
57 self.offset += n
58 return n
59
60 def tell(self):
61 return self.offset
62
63 def flush(self):
64 self.fp.flush()
65
66 def close(self):
67 self.fp.close()
68
69
c2bdf47b
CH
70class ZipStream(ZipFile):
71 """Subclass of ZipFile that supports non-seekable input and output"""
72
667e02f4
CH
73 def __init__(self, file, *args, **kwargs):
74 """
75 Create ZipStream instance which is like a ZipFile plus functions
76 create_zipinfo and write_stream.
77
78 ZipFile determines whether output stream can seek() and tell().
79 Unfortunately some streams (like sys.stdout.buffer when redirecting
80 output) seem to support these methods but only return 0 from tell.
81
82 :param bool force_wrap: force wrapping of output stream with
83 BytesTellWrapper.
84 """
85 if 'force_wrap' in kwargs and kwargs['force_wrap']:
86 if isstr(file):
87 raise ValueError('force_wrap only makes sense for streams')
88 del kwargs['force_wrap']
89 super(ZipStream, self).__init__(BytesTellWrapper(file), *args,
90 **kwargs)
91 else:
92 super(ZipStream, self).__init__(file, *args, **kwargs)
93
c2bdf47b
CH
94 def create_zipinfo(self, filename, arcname=None):
95 """
96 Create ZipInfo for given file
a294b6a7 97
c2bdf47b 98 Optionally set arcname as name of file inside archive.
c2bdf47b 99 """
7628bc48 100 return ZipInfo.from_file(filename, arcname)
c2bdf47b
CH
101
102 def write_stream(self, src, zinfo):
103 """
104 Add data from byte stream stream src to archive with info in ZipInfo.
a294b6a7 105
c2bdf47b
CH
106 Param zinfo must be a ZipInfo, created e.g. with
107 :py:meth:`ZipStream.create_zipinfo`
108
109 Note: you cannot add directories this way (removed the corresponding
110 code).
111
112 This is a shortened version of python's
113 :py:func:`zipfile.ZipFile.write`.
114 """
c2bdf47b
CH
115 if not self.fp:
116 raise ValueError(
117 "Attempt to write to ZIP archive that was already closed")
118 if self._writing:
119 raise ValueError(
120 "Can't write to ZIP archive while an open writing handle exists"
121 )
122
123 if zinfo.is_dir():
124 raise ValueError('streaming a dir entry does not make sense')
125 if zinfo.compress_type is None:
126 zinfo.compress_type = self.compression
127
128 with self.open(zinfo, 'w') as dest:
129 shutil.copyfileobj(src, dest, 1024*8)