Coverage for gws-app/gws/lib/zipx/__init__.py: 95%
86 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 12:46 +0200
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 12:46 +0200
1"""Zipfile wrappers."""
3import io
4import os
5import shutil
6import zipfile
8import gws
11class Error(gws.Error):
12 pass
15def zip_to_path(path: str, sources: list[str | dict], base_dir: str = '', flat: bool = False) -> int:
16 """Create a zip archive in a file.
18 Args:
19 path: Path to the archive.
20 sources: A list of paths or dicts to zip. If a dict is given,
21 its keys are file names in the archive and its values are the file contents.
22 base_dir: If given, this path is stripped from the beginning of the file paths in the archive.
23 flat: If ``True`` only base names are being kept in archive.
25 Returns:
26 The amount of files in the archive.
27 """
29 return _zip(path, sources, base_dir, flat)
32def zip_to_bytes(sources: list[str | dict], base_dir: str = '', flat: bool = False) -> bytes:
33 """Create a zip archive in memory.
35 Args:
36 sources: A list of paths or dicts to zip. If a dict is given,
37 its keys are file names in the archive and its values are the file contents.
38 base_dir: If given, this path is stripped from the beginning of the file paths in the archive.
39 flat: If ``True`` only base names are being kept in archive.
41 Returns:
42 The zipped content as bytes.
43 """
45 with io.BytesIO() as fp:
46 cnt = _zip(fp, sources, base_dir, flat)
47 return fp.getvalue() if cnt else b''
50def unzip_path(path: str, target_dir: str, flat: bool = False) -> int:
51 """Unpack a zip archive into a directory.
53 Args:
54 path: Path to the zip archive.
55 target_dir: Path to the target directory.
56 flat: If ``True`` omit path and consider only base name of files in the zip archive,
57 else complete paths are considered of files in the zip archive. Default is ``False``.
59 Returns:
60 The number of unzipped files.
61 """
63 return _unzip(path, target_dir, None, flat)
66def unzip_bytes(source: bytes, target_dir: str, flat: bool = False) -> int:
67 """Unpack a zip archive in memory into a directory.
69 Args:
70 source: Path to the zip archive.
71 target_dir: Path to the target directory.
72 flat: If ``True`` omit path and consider only base name of files in the zip archive,
73 else complete paths are considered of files in the zip archive. Default is ``False``.
75 Returns:
76 The number of unzipped files.
77 """
79 with io.BytesIO(source) as fp:
80 return _unzip(fp, target_dir, None, flat)
83def unzip_path_to_dict(path: str, flat: bool = False) -> dict[str, bytes]:
84 """Unpack a zip archive into a dict.
86 Args:
87 path: Path to the zip archive.
88 flat: If ``True`` then the result contains the base names of the unzipped files,
89 else it contains the whole path. Default is ``False``.
91 Returns:
92 A dictionary whose keys are the file paths or base names and values are the file contents.
93 """
95 dct = {}
96 _unzip(path, None, dct, flat)
97 return dct
100def unzip_bytes_to_dict(source: bytes, flat: bool = False) -> dict[str, bytes]:
101 """Unpack a zip archive in memory into a dict.
103 Args:
104 source: Path to zip archive.
105 flat: If ``True`` then the result contains the base names of the unzipped files,
106 else it contains the whole path. Default is ``False``.
108 Returns:
109 A dictionary whose keys are the file paths or base names and values are the file contents.
110 """
112 with io.BytesIO(source) as fp:
113 dct = {}
114 _unzip(fp, None, dct, flat)
115 return dct
118##
121def _zip(target, sources, base_dir, flat):
122 def norm_path(p):
123 p = os.path.normpath(p)
124 if flat:
125 return os.path.basename(p)
126 if base_dir:
127 if p.startswith(base_dir):
128 return p[len(base_dir):]
129 return p
131 def scan_dir(d):
132 for de in os.scandir(d):
133 if de.is_file():
134 yield de.path
135 elif de.is_dir():
136 yield from scan_dir(de.path)
138 args = []
140 for src in sources:
141 if isinstance(src, dict):
142 for name, data in src.items():
143 args.append((norm_path(name), None, data))
144 elif os.path.isdir(src):
145 for p in scan_dir(src):
146 args.append((norm_path(p), p, None))
147 elif os.path.isfile(src):
148 args.append((norm_path(src), src, None))
149 else:
150 raise Error(f'zip: invalid argument: {src!r}')
152 if not args:
153 return 0
155 with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
156 for arcname, path, data in args:
157 if path:
158 zf.write(path, arcname)
159 else:
160 zf.writestr(arcname, data)
162 return len(args)
165def _unzip(source, target_dir, target_dict, flat):
166 cnt = 0
168 with zipfile.ZipFile(source, 'r') as zf:
169 for zi in zf.infolist():
170 if zi.is_dir():
171 continue
173 path = zi.filename.replace('\\', '/')
174 base = os.path.basename(path)
176 if path.startswith(('/', '.')) or '..' in path or not base:
177 gws.log.warning(f'unzip: invalid file name: {path!r}')
178 continue
180 cnt += 1
182 if target_dir:
183 if flat:
184 dst = os.path.join(target_dir, base)
185 else:
186 dst = os.path.join(target_dir, *path.split('/'))
187 os.makedirs(os.path.dirname(dst), exist_ok=True)
189 with zf.open(zi) as src, open(dst, 'wb') as fp:
190 shutil.copyfileobj(src, fp)
191 elif target_dict is not None:
192 key = base if flat else path
193 with zf.open(zi) as src:
194 target_dict[key] = src.read()
195 else:
196 raise Error('invalid target for unzip')
198 return cnt