|
1 | 1 | #!/usr/bin/env python |
2 | 2 | # -*- coding: utf-8 -*- |
3 | 3 | from __future__ import absolute_import, division, print_function, unicode_literals |
4 | | -import sys, os, io, argparse, json |
5 | 4 |
|
6 | | -__program_name__ = "PyNeoFile" |
| 5 | +"""PyNeoFile CLI (core-only) |
| 6 | +- Uses only the `pyneofile` core module. |
| 7 | +- Supports --no-json fast path for scans. |
| 8 | +- Adds verbose (-d) printing during CREATE (-c), mirroring original output. |
| 9 | +""" |
7 | 10 |
|
8 | | -try: |
9 | | - import pyneofile as P |
10 | | -except Exception as e: |
11 | | - raise SystemExit("Failed to import core module 'pyneofile': %s" % (e,)) |
| 11 | +import sys, os, io, argparse |
| 12 | +import pyneofile as P |
12 | 13 |
|
13 | 14 | def _read_input_bytes(path): |
14 | 15 | if path in (None, '-', b'-'): |
15 | | - data = sys.stdin.buffer.read() |
16 | | - return data |
17 | | - with io.open(path, 'rb') as fp: |
18 | | - return fp.read() |
| 16 | + return getattr(sys.stdin, 'buffer', sys.stdin).read() |
| 17 | + with io.open(path, 'rb') as fp: return fp.read() |
19 | 18 |
|
20 | 19 | def _write_output_bytes(path, data): |
| 20 | + if isinstance(data, str): data = data.encode('utf-8') |
21 | 21 | if path in (None, '-', b'-'): |
22 | | - sys.stdout.buffer.write(data) |
23 | | - return |
24 | | - d = os.path.dirname(path) |
25 | | - if d and not os.path.isdir(d): |
26 | | - os.makedirs(d) |
27 | | - with io.open(path, 'wb') as fp: |
28 | | - fp.write(data) |
| 22 | + getattr(sys.stdout, 'buffer', sys.stdout).write(data); return |
| 23 | + d = os.path.dirname(path); (os.makedirs(d) if d and not os.path.isdir(d) else None) |
| 24 | + with io.open(path, 'wb') as fp: fp.write(data) |
29 | 25 |
|
30 | 26 | def main(argv=None): |
31 | | - p = argparse.ArgumentParser(prog=__program_name__, description="PyNeoFile CLI (core-only)") |
| 27 | + p = argparse.ArgumentParser(prog="neofile", description="PyNeoFile CLI (core-only)") |
32 | 28 | g = p.add_mutually_exclusive_group(required=True) |
33 | | - g.add_argument('-l', '--list', action='store_true', help='List archive entries') |
34 | | - g.add_argument('-e', '--extract', action='store_true', help='Extract files') |
35 | | - g.add_argument('-c', '--create', action='store_true', help='Create archive from path or stdin') |
36 | | - g.add_argument('-r', '--repack', action='store_true', help='Repack archive, optionally changing compression') |
37 | | - g.add_argument('--validate', action='store_true', help='Validate checksums/structure') |
38 | | - g.add_argument('-t', '--convert', action='store_true', help='Convert foreign (zip/tar) -> neo') |
| 29 | + g.add_argument('-l', '--list', action='store_true') |
| 30 | + g.add_argument('-e', '--extract', action='store_true') |
| 31 | + g.add_argument('-c', '--create', action='store_true') |
| 32 | + g.add_argument('-r', '--repack', action='store_true') |
| 33 | + g.add_argument('--validate', action='store_true') |
| 34 | + g.add_argument('-t', '--convert', action='store_true') |
| 35 | + p.add_argument('-i','--input'); p.add_argument('-o','--output') |
| 36 | + p.add_argument('-P','--compression', default='auto') |
| 37 | + p.add_argument('-L','--level', default=None, type=int) |
| 38 | + p.add_argument('--skipchecksum', action='store_true') |
| 39 | + p.add_argument('-d','--verbose', action='store_true') |
| 40 | + p.add_argument('--no-json', action='store_true') |
| 41 | + a = p.parse_args(argv) |
39 | 42 |
|
40 | | - p.add_argument('-i', '--input', required=False, help='Input path (use - for stdin)') |
41 | | - p.add_argument('-o', '--output', required=False, help='Output path (use - for stdout)') |
42 | | - p.add_argument('-P', '--compression', default='auto', help='Compression: auto|none|zlib|gzip|bz2|lzma') |
43 | | - p.add_argument('-L', '--level', default=None, type=int, help='Compression level') |
44 | | - p.add_argument('--skipchecksum', action='store_true', help='Skip content checksum verification') |
45 | | - p.add_argument('-d', '--verbose', action='store_true', help='Verbose listing') |
46 | | - p.add_argument('--no-json', action='store_true', help='Skip reading per-file JSON blocks (faster)') |
47 | | - |
48 | | - args = p.parse_args(argv) |
49 | | - |
50 | | - if args.list: |
51 | | - src = args.input |
| 43 | + if a.list: |
| 44 | + src = a.input |
52 | 45 | if src in (None, '-', b'-'): |
53 | 46 | data = _read_input_bytes(src) |
54 | | - entries = P.archivefilelistfiles_neo(data, advanced=args.verbose, include_dirs=True, skipjson=True if args.no_json else True) |
| 47 | + entries = P.archivefilelistfiles_neo(data, advanced=a.verbose, include_dirs=True, skipjson=True if a.no_json else True) |
55 | 48 | else: |
56 | | - entries = P.archivefilelistfiles_neo(src, advanced=args.verbose, include_dirs=True, skipjson=args.no_json) |
57 | | - if args.verbose: |
| 49 | + entries = P.archivefilelistfiles_neo(src, advanced=a.verbose, include_dirs=True, skipjson=a.no_json) |
| 50 | + if a.verbose: |
58 | 51 | for e in entries: |
59 | | - if isinstance(e, dict): |
60 | | - print("{type} {compression} {size} {name}".format(**e)) |
61 | | - else: |
62 | | - print(e) |
| 52 | + if isinstance(e, dict): print("{type}\t{compression}\t{size}\t{name}".format(**e)) |
| 53 | + else: print(e) |
63 | 54 | else: |
64 | | - for e in entries: |
65 | | - print(e['name'] if isinstance(e, dict) else e) |
| 55 | + for e in entries: print(e['name'] if isinstance(e, dict) else e) |
66 | 56 | return 0 |
67 | 57 |
|
68 | | - if args.validate: |
69 | | - src = args.input |
| 58 | + if a.validate: |
| 59 | + src = a.input |
70 | 60 | if src in (None, '-', b'-'): |
71 | 61 | data = _read_input_bytes(src) |
72 | | - ok, details = P.archivefilevalidate_neo(data, verbose=args.verbose, return_details=True, skipjson=args.no_json) |
| 62 | + ok, details = P.archivefilevalidate_neo(data, verbose=a.verbose, return_details=True, skipjson=a.no_json) |
73 | 63 | else: |
74 | | - ok, details = P.archivefilevalidate_neo(src, verbose=args.verbose, return_details=True, skipjson=args.no_json) |
| 64 | + ok, details = P.archivefilevalidate_neo(src, verbose=a.verbose, return_details=True, skipjson=a.no_json) |
75 | 65 | print("OK" if ok else "BAD") |
76 | | - if args.verbose: |
77 | | - for d in details: |
78 | | - print("{index} {name} {header_ok} {json_ok} {content_ok}".format(**d)) |
| 66 | + if a.verbose: |
| 67 | + for d in details: print("{index}\t{name}\t{header_ok}\t{json_ok}\t{content_ok}".format(**d)) |
79 | 68 | return 0 if ok else 2 |
80 | 69 |
|
81 | | - if args.extract: |
82 | | - src = args.input |
83 | | - outdir = args.output or '.' |
| 70 | + if a.extract: |
| 71 | + src = a.input; outdir = a.output or '.' |
84 | 72 | if src in (None, '-', b'-'): |
85 | | - data = _read_input_bytes(src) |
86 | | - ok = P.unpack_neo(data, outdir, skipchecksum=args.skipchecksum, uncompress=True) |
| 73 | + data = _read_input_bytes(src); ok = P.unpack_neo(data, outdir, skipchecksum=a.skipchecksum, uncompress=True) |
87 | 74 | else: |
88 | | - ok = P.unpack_neo(src, outdir, skipchecksum=args.skipchecksum, uncompress=True) |
| 75 | + ok = P.unpack_neo(src, outdir, skipchecksum=a.skipchecksum, uncompress=True) |
89 | 76 | return 0 if ok else 1 |
90 | 77 |
|
91 | | - if args.create: |
92 | | - dst = args.output or '-' |
93 | | - if args.input in (None, '-', b'-'): |
94 | | - data = _read_input_bytes(args.input) |
95 | | - payload = {"stdin.bin": data} |
| 78 | + if a.create: |
| 79 | + dst = a.output or '-'; src_path = a.input |
| 80 | + if src_path in (None, '-', b'-'): |
| 81 | + data = _read_input_bytes(src_path); payload = {"stdin.bin": data} |
96 | 82 | blob = P.pack_neo(payload, outfile=None, checksumtypes=('crc32','crc32','crc32'), |
97 | | - encoding='UTF-8', compression=args.compression, compression_level=args.level) |
98 | | - _write_output_bytes(dst, blob) |
99 | | - else: |
100 | | - res = P.pack_neo(args.input, outfile=dst, checksumtypes=('crc32','crc32','crc32'), |
101 | | - encoding='UTF-8', compression=args.compression, compression_level=args.level) |
102 | | - if isinstance(res, (bytes, bytearray)): |
103 | | - _write_output_bytes(dst, res) |
| 83 | + encoding='UTF-8', compression=a.compression, compression_level=a.level) |
| 84 | + _write_output_bytes(dst, blob); return 0 |
| 85 | + if a.verbose: |
| 86 | + norm = os.path.normpath(src_path) |
| 87 | + if os.path.isfile(norm): |
| 88 | + base = os.path.basename(norm).replace('\\','/'); print('./' + base) |
| 89 | + else: |
| 90 | + base = os.path.basename(norm).replace('\\','/') |
| 91 | + for root, dirs, files in os.walk(norm, topdown=True): |
| 92 | + rel = base if root == norm else base + '/' + os.path.relpath(root, norm).replace('\\','/') |
| 93 | + print('./' + rel) |
| 94 | + for fname in sorted(files): print('./' + rel + '/' + fname) |
| 95 | + res = P.pack_neo(src_path, outfile=dst, checksumtypes=('crc32','crc32','crc32'), |
| 96 | + encoding='UTF-8', compression=a.compression, compression_level=a.level) |
| 97 | + if isinstance(res, (bytes, bytearray)): _write_output_bytes(dst, res) |
104 | 98 | return 0 |
105 | 99 |
|
106 | | - if args.repack: |
107 | | - src = args.input |
108 | | - dst = args.output or '-' |
109 | | - res = P.repack_neo(src if src not in (None, '-', b'-') else _read_input_bytes(src), |
110 | | - outfile=dst, checksumtypes=('crc32','crc32','crc32'), |
111 | | - compression=args.compression, compression_level=args.level) |
112 | | - if isinstance(res, (bytes, bytearray)): |
113 | | - _write_output_bytes(dst, res) |
| 100 | + if a.repack: |
| 101 | + src = a.input; dst = a.output or '-' |
| 102 | + data_or_path = src if src not in (None, '-', b'-') else _read_input_bytes(src) |
| 103 | + res = P.repack_neo(data_or_path, outfile=dst, checksumtypes=('crc32','crc32','crc32'), |
| 104 | + compression=a.compression, compression_level=a.level) |
| 105 | + if isinstance(res, (bytes, bytearray)): _write_output_bytes(dst, res) |
114 | 106 | return 0 |
115 | 107 |
|
116 | | - if args.convert: |
117 | | - src = args.input |
118 | | - dst = args.output or '-' |
119 | | - if src in (None, '-', b'-'): |
120 | | - raise SystemExit("convert requires a path input (zip/tar). Use -i <file>") |
| 108 | + if a.convert: |
| 109 | + src = a.input; dst = a.output or '-' |
| 110 | + if src in (None, '-', b'-'): raise SystemExit("convert requires a path input (zip/tar)") |
121 | 111 | res = P.convert_foreign_to_neo(src, outfile=dst, checksumtypes=('crc32','crc32','crc32'), |
122 | | - compression=args.compression, compression_level=args.level) |
123 | | - if isinstance(res, (bytes, bytearray)): |
124 | | - _write_output_bytes(dst, res) |
| 112 | + compression=a.compression, compression_level=a.level) |
| 113 | + if isinstance(res, (bytes, bytearray)): _write_output_bytes(dst, res) |
125 | 114 | return 0 |
126 | 115 |
|
127 | 116 | if __name__ == "__main__": |
|
0 commit comments