Skip to content

Commit 76199ac

Browse files
author
Kazuki Suzuki
authored
Add files via upload
1 parent 61e4ca3 commit 76199ac

File tree

2 files changed

+1801
-0
lines changed

2 files changed

+1801
-0
lines changed

neofile.py

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
2+
#!/usr/bin/env python
3+
# -*- coding: utf-8 -*-
4+
5+
from __future__ import absolute_import, division, print_function, unicode_literals
6+
7+
"""
8+
neofile.py
9+
A lightweight CLI re-creation of archivefile.py that uses the *alt* implementation
10+
(pyneofile) you asked for, including:
11+
- INI-based format defaults (auto-fallback)
12+
- stdlib compression with size-based "auto" (zlib/gzip/bz2, xz on Py3 when available)
13+
- robust checksums (CRC-32 padded), JSON/header/content verification
14+
- Python 2 and 3 compatibility
15+
- Optional conversion from ZIP/TAR (stdlib), and **RAR/7z** when extra libs are installed:
16+
* RAR: pip install rarfile
17+
* 7z: pip install py7zr
18+
19+
Operations:
20+
-c / --create pack files/dirs into an archive
21+
-e / --extract extract an archive
22+
-r / --repack repack (optionally change compression)
23+
-l / --list list entries (fast, header-only)
24+
-v / --validate validate checksums
25+
26+
Convert support:
27+
Use -t/--convert with --create/--repack/--list/--validate to convert a foreign archive
28+
(zip/tar are stdlib; rar/7z need optional libs) into the alt ArchiveFile format first.
29+
"""
30+
31+
import os
32+
import sys
33+
import argparse
34+
import tempfile
35+
36+
# Graceful SIGPIPE on non-Windows
37+
if os.name != 'nt':
38+
try:
39+
import signal
40+
if hasattr(signal, 'SIGPIPE'):
41+
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
42+
except Exception:
43+
pass
44+
45+
# Import alt core
46+
try:
47+
import pyneofile as A
48+
except Exception as e:
49+
sys.stderr.write("Failed to import pyneofile: %s\n" % (e,))
50+
sys.exit(2)
51+
52+
__program_name__ = "neofile"
53+
__version__ = "0.2.0"
54+
55+
def _build_formatspecs_from_args(args):
56+
"""Create a formatspecs dict or return None to use INI auto-fallback."""
57+
if args.format is None or args.format.lower() == "auto":
58+
return None # let alt core load INI or defaults
59+
magic = args.format
60+
ver = args.formatver if args.formatver is not None else "001"
61+
delim = args.delimiter if args.delimiter is not None else "\x00"
62+
return {
63+
"format_name": magic,
64+
"format_magic": magic,
65+
"format_ver": ver, # alt core keeps digits as-is
66+
"format_delimiter": delim,
67+
"new_style": True,
68+
}
69+
70+
def _read_listfile(path):
71+
items = []
72+
with open(path, 'r') as f:
73+
for line in f:
74+
s = line.strip()
75+
if not s or s.startswith('#'):
76+
continue
77+
items.append(s)
78+
return items
79+
80+
def _convert_or_fail(infile, outpath, formatspecs, checksum, compression, level):
81+
"""Call core convert function and show friendly messages for missing deps."""
82+
try:
83+
A.convert_foreign_to_alt(infile, outpath, formatspecs=formatspecs,
84+
checksumtypes=(checksum, checksum, checksum),
85+
compression=compression, compression_level=level)
86+
return True
87+
except RuntimeError as e:
88+
msg = str(e)
89+
if "rarfile" in msg.lower():
90+
sys.stderr.write("error: RAR support requires 'rarfile'. Install via: pip install rarfile\n")
91+
elif "py7zr" in msg.lower():
92+
sys.stderr.write("error: 7z support requires 'py7zr'. Install via: pip install py7zr\n")
93+
else:
94+
sys.stderr.write("convert error: %s\n" % msg)
95+
return False
96+
except ValueError as e:
97+
# Unsupported format (not zip/tar/rar/7z)
98+
sys.stderr.write("convert error: %s\n" % e)
99+
return False
100+
except Exception as e:
101+
sys.stderr.write("unexpected convert error: %s\n" % e)
102+
return False
103+
104+
def main(argv=None):
105+
p = argparse.ArgumentParser(description="Manipulate ArchiveFile (alt) archives.", conflict_handler="resolve", add_help=True)
106+
107+
p.add_argument("-V", "--version", action="version", version=__program_name__ + " " + __version__)
108+
109+
# IO
110+
p.add_argument("-i", "--input", nargs="+", required=True, help="Input file(s) or archive file.")
111+
p.add_argument("-o", "--output", default=None, help="Output file or directory.")
112+
113+
# Ops
114+
p.add_argument("-c", "--create", action="store_true", help="Create an archive from input files/dirs.")
115+
p.add_argument("-e", "--extract", action="store_true", help="Extract an archive to --output (directory).")
116+
p.add_argument("-r", "--repack", action="store_true", help="Repack an existing archive (can change compression).")
117+
p.add_argument("-l", "--list", action="store_true", help="List archive entries.")
118+
p.add_argument("-v", "--validate", action="store_true", help="Validate archive checksums.")
119+
p.add_argument("-t", "--convert", action="store_true", help="Treat input as foreign (zip/tar/rar/7z) and convert to ArchiveFile first.")
120+
121+
# Format & delimiter
122+
p.add_argument("-F", "--format", default="auto", help="Format magic to use (or 'auto' to read INI).")
123+
p.add_argument("-D", "--delimiter", default=None, help="Delimiter to use when --format is not 'auto'.")
124+
p.add_argument("-m", "--formatver", default=None, help="Format version digits (e.g. 001).")
125+
126+
# Compression
127+
p.add_argument("-P", "--compression", default="auto", help="Compression: none|zlib|gzip|bz2|xz|auto")
128+
p.add_argument("-L", "--level", default=None, help="Compression level/preset (int).")
129+
p.add_argument("-W", "--wholefile", action="store_true", help="(Ignored; CLI compatibility).")
130+
131+
# Validation & extraction behavior
132+
p.add_argument("-C", "--checksum", default="crc32", help="Checksum algorithm (header/content/json).")
133+
p.add_argument("-s", "--skipchecksum", action="store_true", help="Skip checksum verification while reading.")
134+
p.add_argument("-p", "--preserve", action="store_false", help="Do not preserve permissions/times (kept for compatibility).")
135+
136+
# Misc
137+
p.add_argument("-d", "--verbose", action="store_true", help="Verbose logging.")
138+
p.add_argument("-T", "--text", action="store_true", help="Treat the first input argument as a text file containing paths (one per line).")
139+
140+
args = p.parse_args(argv)
141+
142+
# Choose primary action
143+
actions = ['create', 'extract', 'repack', 'list', 'validate']
144+
active = next((name for name in actions if getattr(args, name)), None)
145+
if not active:
146+
p.error("one of --create/--extract/--repack/--list/--validate is required")
147+
148+
# formatspecs (None => INI auto-fallback inside the alt core)
149+
formatspecs = _build_formatspecs_from_args(args)
150+
151+
# Inputs
152+
inputs = args.input
153+
infile0 = inputs[0]
154+
155+
# Compression/level
156+
compression = args.compression
157+
level = None if args.level in (None, "",) else int(args.level)
158+
159+
# Checksum triple
160+
checksum = args.checksum
161+
checks = (checksum, checksum, checksum)
162+
163+
if active == 'create':
164+
if args.text:
165+
inputs = _read_listfile(infile0)
166+
if args.convert:
167+
if not args.output:
168+
p.error("--output is required when using --convert")
169+
ok = _convert_or_fail(infile0, args.output, formatspecs, checksum, compression, level)
170+
return 0 if ok else 1
171+
if not args.output:
172+
p.error("--output is required for --create")
173+
A.pack_alt(inputs if not args.text else inputs, args.output, formatspecs=formatspecs,
174+
checksumtypes=checks, compression=compression, compression_level=level)
175+
if args.verbose:
176+
sys.stderr.write("created: %s\n" % args.output)
177+
return 0
178+
179+
if active == 'repack':
180+
if args.convert:
181+
if not args.output:
182+
p.error("--output is required when using --repack --convert")
183+
ok = _convert_or_fail(infile0, args.output, formatspecs, checksum, compression, level)
184+
return 0 if ok else 1
185+
if not args.output:
186+
p.error("--output is required for --repack")
187+
A.repack_alt(infile0, args.output, formatspecs=formatspecs,
188+
checksumtypes=checks, compression=compression, compression_level=level)
189+
if args.verbose:
190+
sys.stderr.write("repacked: %s -> %s\n" % (infile0, args.output))
191+
return 0
192+
193+
if active == 'extract':
194+
outdir = args.output or "."
195+
if args.convert:
196+
tmp_arc = os.path.join(tempfile.gettempdir(), "af_alt_convert.arc")
197+
ok = _convert_or_fail(infile0, tmp_arc, formatspecs, checksum, compression, level)
198+
if not ok:
199+
return 1
200+
infile0_use = tmp_arc
201+
else:
202+
infile0_use = infile0
203+
A.unpack_alt(infile0_use, outdir, formatspecs=formatspecs, skipchecksum=args.skipchecksum, uncompress=True)
204+
if args.verbose:
205+
sys.stderr.write("extracted to: %s\n" % outdir)
206+
return 0
207+
208+
if active == 'list':
209+
if args.convert:
210+
tmp_arc = os.path.join(tempfile.gettempdir(), "af_alt_convert.arc")
211+
ok = _convert_or_fail(infile0, tmp_arc, formatspecs, checksum, compression, level)
212+
if not ok:
213+
return 1
214+
use = tmp_arc
215+
else:
216+
use = infile0
217+
names = A.archivefilelistfiles_alt(use, formatspecs=formatspecs, advanced=args.verbose, include_dirs=True)
218+
if not args.verbose:
219+
for n in names:
220+
sys.stdout.write(n + "\n")
221+
else:
222+
for ent in names:
223+
sys.stdout.write("%s\t%s\t%s\t%s\n" % (
224+
ent['type'], ent['compression'], ent['size'], ent['name']
225+
))
226+
return 0
227+
228+
if active == 'validate':
229+
if args.convert:
230+
tmp_arc = os.path.join(tempfile.gettempdir(), "af_alt_convert.arc")
231+
ok = _convert_or_fail(infile0, tmp_arc, formatspecs, checksum, compression, level)
232+
if not ok:
233+
return 1
234+
use = tmp_arc
235+
else:
236+
use = infile0
237+
ok, details = A.archivefilevalidate_alt(use, formatspecs=formatspecs, verbose=args.verbose, return_details=True)
238+
if not args.verbose:
239+
sys.stdout.write("valid: %s\n" % ("yes" if ok else "no"))
240+
else:
241+
sys.stdout.write("valid: %s (entries: %d)\n" % ("yes" if ok else "no", len(details)))
242+
for d in details:
243+
sys.stdout.write("%4d %s h:%s j:%s c:%s\n" % (
244+
d['index'], d['name'], d['header_ok'], d['json_ok'], d['content_ok']
245+
))
246+
return 0
247+
248+
return 0
249+
250+
if __name__ == "__main__":
251+
sys.exit(main())

0 commit comments

Comments
 (0)