unzip-http 0.4__tar.gz → 0.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.1
2
+ Name: unzip-http
3
+ Version: 0.6
4
+ Summary: extract files from .zip files over http without downloading entire archive
5
+ Home-page: https://github.com/saulpw/unzip-http
6
+ Author: Saul Pwanson
7
+ Keywords: http zip unzip
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE-mit.txt
13
+
14
+ # unzip-http
15
+
16
+ Extract individual files from .zip files over http without downloading the entire archive.
17
+
18
+ ## Install
19
+
20
+ pip install unzip-http
21
+
22
+ ## Usage
23
+
24
+ unzip_http [-l] [-f] [-o] <url> <filenames..>
25
+
26
+ Extract <filenames> from a remote .zip at `<url>` to stdout.
27
+
28
+ A filename can be a wildcard glob; all matching files are extracted in this case.
29
+
30
+ Specify multiple filenames as distinct arguments (separated with spaces on the command line).
31
+
32
+ Note: HTTP server must send `Accept-Ranges: bytes` and `Content-Length` in headers (most do).
33
+
34
+ Options:
35
+
36
+ - `-l`: List files in remote .zip file (default if no filenames given)
37
+ - `-f`: Recreate folder structure from .zip file when extracting (instead of extracting files to the current directly to the current directory)
38
+ - `-o`: Write files to stdout (if multiple files, concatenate them in zipfile order)
39
+
40
+ # Python module `unzip_http`
41
+
42
+ import unzip_http
43
+
44
+ rzf = unzip_http.RemoteZipFile('https://example.com/foo.zip')
45
+ binfp = rzf.open('bar.bin')
46
+ txtfp = rzf.open_text('baz.txt')
47
+
48
+ # Credits
49
+
50
+ `unzip-http` was written by [Saul Pwanson](https://saul.pw) and made available for use under the MIT License.
@@ -8,15 +8,21 @@ Extract individual files from .zip files over http without downloading the entir
8
8
 
9
9
  ## Usage
10
10
 
11
- unzip-http <url.zip> <filenames..>
11
+ unzip_http [-l] [-f] [-o] <url> <filenames..>
12
12
 
13
13
  Extract <filenames> from a remote .zip at `<url>` to stdout.
14
14
 
15
- If no filenames given, displays .zip contents (filenames and sizes).
15
+ A filename can be a wildcard glob; all matching files are extracted in this case.
16
16
 
17
- Each filename can be a wildcard glob; all matching files are concatenated and sent to stdout in zipfile order.
17
+ Specify multiple filenames as distinct arguments (separated with spaces on the command line).
18
18
 
19
- Note: HTTP server must send `Accept-Ranges: bytes` and `Content-Length` in headers.
19
+ Note: HTTP server must send `Accept-Ranges: bytes` and `Content-Length` in headers (most do).
20
+
21
+ Options:
22
+
23
+ - `-l`: List files in remote .zip file (default if no filenames given)
24
+ - `-f`: Recreate folder structure from .zip file when extracting (instead of extracting files to the current directly to the current directory)
25
+ - `-o`: Write files to stdout (if multiple files, concatenate them in zipfile order)
20
26
 
21
27
  # Python module `unzip_http`
22
28
 
@@ -14,7 +14,7 @@ def requirements():
14
14
 
15
15
  setup(
16
16
  name="unzip-http",
17
- version="0.4",
17
+ version="0.6",
18
18
  description="extract files from .zip files over http without downloading entire archive",
19
19
  long_description=readme(),
20
20
  long_description_content_type="text/markdown",
@@ -27,6 +27,9 @@ setup(
27
27
  url="https://github.com/saulpw/unzip-http",
28
28
  python_requires=">=3.8",
29
29
  py_modules=["unzip_http"],
30
- scripts=["unzip-http"],
30
+ scripts=["unzip_http.py"],
31
+ entry_points={
32
+ "console_scripts": ["unzip_http=unzip_http:main"],
33
+ },
31
34
  install_requires=requirements(),
32
35
  )
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.1
2
+ Name: unzip-http
3
+ Version: 0.6
4
+ Summary: extract files from .zip files over http without downloading entire archive
5
+ Home-page: https://github.com/saulpw/unzip-http
6
+ Author: Saul Pwanson
7
+ Keywords: http zip unzip
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE-mit.txt
13
+
14
+ # unzip-http
15
+
16
+ Extract individual files from .zip files over http without downloading the entire archive.
17
+
18
+ ## Install
19
+
20
+ pip install unzip-http
21
+
22
+ ## Usage
23
+
24
+ unzip_http [-l] [-f] [-o] <url> <filenames..>
25
+
26
+ Extract <filenames> from a remote .zip at `<url>` to stdout.
27
+
28
+ A filename can be a wildcard glob; all matching files are extracted in this case.
29
+
30
+ Specify multiple filenames as distinct arguments (separated with spaces on the command line).
31
+
32
+ Note: HTTP server must send `Accept-Ranges: bytes` and `Content-Length` in headers (most do).
33
+
34
+ Options:
35
+
36
+ - `-l`: List files in remote .zip file (default if no filenames given)
37
+ - `-f`: Recreate folder structure from .zip file when extracting (instead of extracting files to the current directly to the current directory)
38
+ - `-o`: Write files to stdout (if multiple files, concatenate them in zipfile order)
39
+
40
+ # Python module `unzip_http`
41
+
42
+ import unzip_http
43
+
44
+ rzf = unzip_http.RemoteZipFile('https://example.com/foo.zip')
45
+ binfp = rzf.open('bar.bin')
46
+ txtfp = rzf.open_text('baz.txt')
47
+
48
+ # Credits
49
+
50
+ `unzip-http` was written by [Saul Pwanson](https://saul.pw) and made available for use under the MIT License.
@@ -3,10 +3,10 @@ MANIFEST.in
3
3
  README.md
4
4
  requirements.txt
5
5
  setup.py
6
- unzip-http
7
6
  unzip_http.py
8
7
  unzip_http.egg-info/PKG-INFO
9
8
  unzip_http.egg-info/SOURCES.txt
10
9
  unzip_http.egg-info/dependency_links.txt
10
+ unzip_http.egg-info/entry_points.txt
11
11
  unzip_http.egg-info/requires.txt
12
12
  unzip_http.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ unzip_http = unzip_http:main
@@ -1,3 +1,5 @@
1
+ #!/usr/bin/env python3
2
+
1
3
  # Copyright (c) 2022 Saul Pwanson
2
4
  #
3
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -18,22 +20,50 @@
18
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
21
  # SOFTWARE.
20
22
 
23
+ """
24
+ usage: unzip_http [-h] [-l] [-f] [-o] url [files ...]
25
+
26
+ Extract individual files from .zip files over http without downloading the
27
+ entire archive. HTTP server must send `Accept-Ranges: bytes` and
28
+ `Content-Length` in headers.
29
+
30
+ positional arguments:
31
+ url URL of the remote zip file
32
+ files Files to extract. If no filenames given, displays .zip
33
+ contents (filenames and sizes). Each filename can be a
34
+ wildcard glob.
35
+
36
+ options:
37
+ -h, --help show this help message and exit
38
+ -l, --list List files in the remote zip file
39
+ -f, --full-filepaths Recreate folder structure from zip file when extracting
40
+ (instead of extracting the files to the current
41
+ directory)
42
+ -o, --stdout Write files to stdout (if multiple files: concatenate
43
+ them to stdout, in zipfile order)
44
+ """
45
+
46
+ import sys
21
47
  import os
22
48
  import io
49
+ import math
50
+ import time
23
51
  import zlib
24
52
  import struct
25
53
  import fnmatch
54
+ import argparse
26
55
  import pathlib
27
56
  import urllib.parse
28
57
 
29
- import urllib3
30
58
 
31
- __version__ = '0.4'
59
+ __version__ = '0.6'
32
60
 
33
61
 
34
62
  def error(s):
35
63
  raise Exception(s)
36
64
 
65
+ def warning(s):
66
+ print(s, file=sys.stderr)
37
67
 
38
68
  def get_bits(val:int, *args):
39
69
  'Generate bitfields (one for each arg) from LSB to MSB.'
@@ -59,6 +89,9 @@ class RemoteZipInfo:
59
89
  sec, mins, hour, day, mon, year = get_bits(date_time, 5, 6, 5, 5, 4, 7)
60
90
  self.date_time = (year+1980, mon, day, hour, mins, sec)
61
91
 
92
+ def is_dir(self):
93
+ return self.filename.endswith('/')
94
+
62
95
  def parse_extra(self, extra):
63
96
  i = 0
64
97
  while i < len(extra):
@@ -93,6 +126,7 @@ class RemoteZipFile:
93
126
  magic_eocd = b'\x50\x4b\x05\x06'
94
127
 
95
128
  def __init__(self, url):
129
+ import urllib3
96
130
  self.url = url
97
131
  self.http = urllib3.PoolManager()
98
132
  self.zip_size = 0
@@ -120,10 +154,13 @@ class RemoteZipFile:
120
154
  r = resp.headers.get('Accept-Ranges', '')
121
155
  if r != 'bytes':
122
156
  hostname = urllib.parse.urlparse(self.url).netloc
123
- error(f"{hostname} does not support HTTP range requests (Accept-Ranges header ('{r}') is not 'bytes')")
157
+ warning(f"{hostname} Accept-Ranges header ('{r}') is not 'bytes'--trying anyway")
124
158
 
125
159
  self.zip_size = int(resp.headers['Content-Length'])
126
- resp = self.get_range(self.zip_size-65536, 65536)
160
+ resp = self.get_range(
161
+ max(self.zip_size-65536, 0),
162
+ 65536
163
+ )
127
164
 
128
165
  cdir_start = -1
129
166
  i = resp.data.rfind(self.magic_eocd64)
@@ -140,7 +177,15 @@ class RemoteZipFile:
140
177
  if cdir_start < 0 or cdir_start >= self.zip_size:
141
178
  error('cannot find central directory')
142
179
 
143
- filehdr_index = 65536 - (self.zip_size - cdir_start)
180
+ if self.zip_size <= 65536:
181
+ filehdr_index = cdir_start
182
+ else:
183
+ filehdr_index = 65536 - (self.zip_size - cdir_start)
184
+
185
+ if filehdr_index < 0:
186
+ resp = self.get_range(cdir_start, self.zip_size - cdir_start)
187
+ filehdr_index = 0
188
+
144
189
  cdir_end = filehdr_index + cdir_bytes
145
190
  while filehdr_index < cdir_end:
146
191
  sizeof_cdirentry = struct.calcsize(self.fmt_cdirentry)
@@ -166,19 +211,26 @@ class RemoteZipFile:
166
211
  rzi.parse_extra(extra)
167
212
  yield rzi
168
213
 
169
- def extractall(self, path, members=None):
170
- path = path or pathlib.Path('.')
171
- for fn in members or self.namelist():
172
- outpath = path/fn
214
+ def extract(self, member, path=None, pwd=None):
215
+ if pwd:
216
+ raise NotImplementedError('Passwords not supported yet')
217
+
218
+ path = path or pathlib.Path('.')
219
+
220
+ outpath = path/member
173
221
  os.makedirs(outpath.parent, exist_ok=True)
174
- with self.open(fn) as fpin:
175
- with open(path/fn, mode='wb') as fpout:
222
+ with self.open(member) as fpin:
223
+ with open(path/member, mode='wb') as fpout:
176
224
  while True:
177
225
  r = fpin.read(65536)
178
226
  if not r:
179
227
  break
180
228
  fpout.write(r)
181
229
 
230
+ def extractall(self, path=None, members=None, pwd=None):
231
+ for fn in members or self.namelist():
232
+ self.extract(fn, path, pwd=pwd)
233
+
182
234
  def get_range(self, start, n):
183
235
  return self.http.request('GET', self.url, headers={'Range': f'bytes={start}-{start+n-1}'}, preload_content=False)
184
236
 
@@ -204,12 +256,12 @@ class RemoteZipFile:
204
256
  return self.get_range(f.header_offset + sizeof_localhdr + fnlen + extralen, f.compress_size)
205
257
  elif method == 8: # DEFLATE
206
258
  resp = self.get_range(f.header_offset + sizeof_localhdr + fnlen + extralen, f.compress_size)
207
- return RemoteZipStream(resp, f)
259
+ return io.BufferedReader(RemoteZipStream(resp, f))
208
260
  else:
209
261
  error(f'unknown compression method {method}')
210
262
 
211
263
  def open_text(self, fn):
212
- return io.TextIOWrapper(io.BufferedReader(self.open(fn)))
264
+ return io.TextIOWrapper(self.open(fn))
213
265
 
214
266
 
215
267
  class RemoteZipStream(io.RawIOBase):
@@ -239,3 +291,95 @@ class RemoteZipStream(io.RawIOBase):
239
291
  self._buffer = self._buffer[n:]
240
292
 
241
293
  return ret
294
+
295
+
296
+ ### script start
297
+
298
+ class StreamProgress:
299
+ def __init__(self, fp, name='', total=0):
300
+ self.name = name
301
+ self.fp = fp
302
+ self.total = total
303
+ self.start_time = time.time()
304
+ self.last_update = 0
305
+ self.amtread = 0
306
+
307
+ def read(self, n):
308
+ r = self.fp.read(n)
309
+ self.amtread += len(r)
310
+ now = time.time()
311
+ if now - self.last_update > 0.1:
312
+ self.last_update = now
313
+
314
+ elapsed_s = now - self.start_time
315
+ sys.stderr.write(f'\r{elapsed_s:.0f}s {self.amtread/10**6:.02f}/{self.total/10**6:.02f}MB ({self.amtread/10**6/elapsed_s:.02f} MB/s) {self.name}')
316
+
317
+ if not r:
318
+ sys.stderr.write('\n')
319
+
320
+ return r
321
+
322
+
323
+ def list_files(rzf):
324
+ def safelog(x):
325
+ return 1 if x == 0 else math.ceil(math.log10(x))
326
+
327
+ digits_compr = max(safelog(f.compress_size) for f in rzf.infolist())
328
+ digits_plain = max(safelog(f.file_size ) for f in rzf.infolist())
329
+ fmtstr = f'%{digits_compr}d -> %{digits_plain}d\t%s'
330
+ for f in rzf.infolist():
331
+ print(fmtstr % (f.compress_size, f.file_size, f.filename), file=sys.stderr)
332
+
333
+
334
+ def extract_one(outfile, rzf, f, ofname):
335
+ print(f'Extracting {f.filename} to {ofname}...', file=sys.stderr)
336
+
337
+ fp = StreamProgress(rzf.open(f), name=f.filename, total=f.compress_size)
338
+ while r := fp.read(2**18):
339
+ outfile.write(r)
340
+
341
+
342
+ def download_file(f, rzf, args):
343
+ if not any(fnmatch.fnmatch(f.filename, g) for g in args.files):
344
+ return
345
+
346
+ if args.stdout:
347
+ extract_one(sys.stdout.buffer, rzf, f, "stdout")
348
+ else:
349
+ path = pathlib.Path(f.filename)
350
+ if args.full_filepaths:
351
+ path.parent.mkdir(parents=True, exist_ok=True)
352
+ else:
353
+ path = path.name
354
+
355
+ with open(str(path), 'wb') as of:
356
+ extract_one(of, rzf, f, str(path))
357
+
358
+
359
+ def main():
360
+ parser = argparse.ArgumentParser(prog='unzip-http', \
361
+ description="Extract individual files from .zip files over http without downloading the entire archive. HTTP server must send `Accept-Ranges: bytes` and `Content-Length` in headers.")
362
+
363
+ parser.add_argument('-l', '--list', action='store_true', default=False,
364
+ help="List files in the remote zip file")
365
+ parser.add_argument('-f', '--full-filepaths', action='store_true', default=False,
366
+ help="Recreate folder structure from zip file when extracting (instead of extracting the files to the current directory)")
367
+ parser.add_argument('-o', '--stdout', action='store_true', default=False,
368
+ help="Write files to stdout (if multiple files: concatenate them to stdout, in zipfile order)")
369
+
370
+ parser.add_argument("url", nargs=1, help="URL of the remote zip file")
371
+ parser.add_argument("files", nargs='*', help="Files to extract. If no filenames given, displays .zip contents (filenames and sizes). Each filename can be a wildcard glob.")
372
+
373
+ args = parser.parse_args()
374
+
375
+ rzf = RemoteZipFile(args.url[0])
376
+ if args.list or len(args.files) == 0:
377
+ list_files(rzf)
378
+ else:
379
+ for f in rzf.infolist():
380
+ download_file(f, rzf, args)
381
+
382
+
383
+
384
+ if __name__ == '__main__':
385
+ main()
unzip-http-0.4/PKG-INFO DELETED
@@ -1,45 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: unzip-http
3
- Version: 0.4
4
- Summary: extract files from .zip files over http without downloading entire archive
5
- Home-page: https://github.com/saulpw/unzip-http
6
- Author: Saul Pwanson
7
- License: UNKNOWN
8
- Description: # unzip-http
9
-
10
- Extract individual files from .zip files over http without downloading the entire archive.
11
-
12
- ## Install
13
-
14
- pip install unzip-http
15
-
16
- ## Usage
17
-
18
- unzip-http <url.zip> <filenames..>
19
-
20
- Extract <filenames> from a remote .zip at `<url>` to stdout.
21
-
22
- If no filenames given, displays .zip contents (filenames and sizes).
23
-
24
- Each filename can be a wildcard glob; all matching files are concatenated and sent to stdout in zipfile order.
25
-
26
- Note: HTTP server must send `Accept-Ranges: bytes` and `Content-Length` in headers.
27
-
28
- # Python module `unzip_http`
29
-
30
- import unzip_http
31
-
32
- rzf = unzip_http.RemoteZipFile('https://example.com/foo.zip')
33
- binfp = rzf.open('bar.bin')
34
- txtfp = rzf.open_text('baz.txt')
35
-
36
- # Credits
37
-
38
- `unzip-http` was written by [Saul Pwanson](https://saul.pw) and made available for use under the MIT License.
39
-
40
- Keywords: http zip unzip
41
- Platform: UNKNOWN
42
- Classifier: Development Status :: 4 - Beta
43
- Classifier: Programming Language :: Python :: 3
44
- Requires-Python: >=3.8
45
- Description-Content-Type: text/markdown
unzip-http-0.4/unzip-http DELETED
@@ -1,62 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- '''
4
- Usage:
5
- unzip-http <url.zip> <filenames..>
6
-
7
- Extract <filenames> from a remote .zip at <url> to stdout.
8
- If no filenames given, displays .zip contents (filenames and sizes).
9
- Each filename can be a wildcard glob; all matching files are concatenated and sent to stdout in zipfile order.
10
-
11
- HTTP server must send `Accept-Ranges: bytes` and `Content-Length` in headers.
12
- '''
13
-
14
- import sys
15
- import io
16
- import time
17
- import fnmatch
18
-
19
- import unzip_http
20
-
21
-
22
- class StreamProgress:
23
- def __init__(self, fp, name='', total=0):
24
- self.name = name
25
- self.fp = fp
26
- self.total = total
27
- self.start_time = time.time()
28
- self.last_update = 0
29
- self.amtread = 0
30
-
31
- def read(self, n):
32
- r = self.fp.read(n)
33
- self.amtread += len(r)
34
- now = time.time()
35
- if now - self.last_update > 0.1:
36
- self.last_update = now
37
-
38
- elapsed_s = now - self.start_time
39
- sys.stderr.write(f'\r{elapsed_s:.0f}s {self.amtread/10**6:.02f}/{self.total/10**6:.02f}MB ({self.amtread/10**6/elapsed_s:.02f} MB/s) {self.name}')
40
-
41
- if not r:
42
- sys.stderr.write('\n')
43
-
44
- return r
45
-
46
-
47
- def main(url, *globs):
48
- rzf = unzip_http.RemoteZipFile(url)
49
- for f in rzf.infolist():
50
- if not globs:
51
- print(f'{f.compress_size} -> {f.file_size} {f.filename}')
52
- elif any(fnmatch.fnmatch(f.filename, g) for g in globs):
53
- fp = StreamProgress(rzf.open(f), name=f.filename, total=f.compress_size)
54
- while r := fp.read(2**18):
55
- sys.stdout.buffer.write(r)
56
-
57
-
58
- args = sys.argv[1:]
59
- if not args:
60
- print(__doc__, file=sys.stderr)
61
- else:
62
- main(*args)
@@ -1,45 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: unzip-http
3
- Version: 0.4
4
- Summary: extract files from .zip files over http without downloading entire archive
5
- Home-page: https://github.com/saulpw/unzip-http
6
- Author: Saul Pwanson
7
- License: UNKNOWN
8
- Description: # unzip-http
9
-
10
- Extract individual files from .zip files over http without downloading the entire archive.
11
-
12
- ## Install
13
-
14
- pip install unzip-http
15
-
16
- ## Usage
17
-
18
- unzip-http <url.zip> <filenames..>
19
-
20
- Extract <filenames> from a remote .zip at `<url>` to stdout.
21
-
22
- If no filenames given, displays .zip contents (filenames and sizes).
23
-
24
- Each filename can be a wildcard glob; all matching files are concatenated and sent to stdout in zipfile order.
25
-
26
- Note: HTTP server must send `Accept-Ranges: bytes` and `Content-Length` in headers.
27
-
28
- # Python module `unzip_http`
29
-
30
- import unzip_http
31
-
32
- rzf = unzip_http.RemoteZipFile('https://example.com/foo.zip')
33
- binfp = rzf.open('bar.bin')
34
- txtfp = rzf.open_text('baz.txt')
35
-
36
- # Credits
37
-
38
- `unzip-http` was written by [Saul Pwanson](https://saul.pw) and made available for use under the MIT License.
39
-
40
- Keywords: http zip unzip
41
- Platform: UNKNOWN
42
- Classifier: Development Status :: 4 - Beta
43
- Classifier: Programming Language :: Python :: 3
44
- Requires-Python: >=3.8
45
- Description-Content-Type: text/markdown
File without changes
File without changes
File without changes
File without changes