Commit | Line | Data |
---|---|---|
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 | ||
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. | |
27 | ||
a294b6a7 CH |
28 | Use 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 |
39 | import shutil |
40 | from zipfile import * | |
e33dd338 | 41 | |
1242b1cf | 42 | from .type_helpers import isstr |
c2bdf47b | 43 | |
a294b6a7 | 44 | |
667e02f4 CH |
45 | class 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 |
70 | class 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) |