Skip to content

Commit 3436170

Browse files
author
Kazuki Suzuki
authored
Add files via upload
1 parent cf4a975 commit 3436170

File tree

2 files changed

+470
-534
lines changed

2 files changed

+470
-534
lines changed

neofile.py

Lines changed: 127 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,27 @@
55
from __future__ import absolute_import, division, print_function, unicode_literals
66

77
"""
8-
neofile.py — CLI for the PyNeoFile format (.neo).
9-
Uses the pyneofile wrappers (which call into pyneoarc_alt) and prefers pyneofile.ini.
10-
Supports converting from zip/tar (stdlib), and rar/7z when optional libs are installed.
8+
pyneofile_cli.py — CLI for the PyNeoFile format (.neo).
9+
10+
New:
11+
- Input '-' for list/validate/extract/repack/convert reads archive bytes from stdin.
12+
- Output '-' for create/repack/convert writes archive bytes to stdout.
13+
- Extract with '-o -' streams a TAR archive to stdout (use: `... -e -i in.neo -o - > out.tar`).
1114
"""
1215

13-
import os, sys, argparse, tempfile
16+
import os, sys, argparse, tempfile, tarfile, io, base64
1417
import pyneofile as N
1518

1619
__program_name__ = "pyneofile"
17-
__version__ = "0.1.0"
20+
__version__ = "0.2.0"
21+
22+
def _stdout_bin():
23+
return getattr(sys.stdout, "buffer", sys.stdout)
24+
25+
def _stdin_bin():
26+
return getattr(sys.stdin, "buffer", sys.stdin)
1827

1928
def _build_formatspecs_from_args(args):
20-
# Allow explicit override; otherwise let wrappers auto-load pyneofile.ini
2129
if args.format is None or args.format.lower() == "auto":
2230
return None
2331
return {
@@ -30,10 +38,9 @@ def _build_formatspecs_from_args(args):
3038

3139
def _convert_or_fail(infile, outpath, formatspecs, checksum, compression, level):
3240
try:
33-
N.convert_foreign_to_neo(infile, outpath, formatspecs=formatspecs,
34-
checksumtypes=(checksum, checksum, checksum),
35-
compression=compression, compression_level=level)
36-
return True
41+
return N.convert_foreign_to_neo(infile, outpath, formatspecs=formatspecs,
42+
checksumtypes=(checksum, checksum, checksum),
43+
compression=compression, compression_level=level)
3744
except RuntimeError as e:
3845
msg = str(e)
3946
if "rarfile" in msg.lower():
@@ -42,20 +49,45 @@ def _convert_or_fail(infile, outpath, formatspecs, checksum, compression, level)
4249
sys.stderr.write("error: 7z support requires 'py7zr'. Install via: pip install py7zr\n")
4350
else:
4451
sys.stderr.write("convert error: %s\n" % msg)
45-
return False
52+
return None
4653
except Exception as e:
4754
sys.stderr.write("convert error: %s\n" % e)
48-
return False
55+
return None
56+
57+
def _emit_tar_stream_from_array(arr, outfp):
58+
"""Write a tar stream to outfp from the parsed archive array (no re-compress)."""
59+
tf = tarfile.open(fileobj=outfp, mode='w|') # stream mode
60+
try:
61+
for ent in arr['ffilelist']:
62+
name = ent['fname'].lstrip('./')
63+
if ent['ftype'] == 5:
64+
ti = tarfile.TarInfo(name=name.rstrip('/') + '/')
65+
ti.type = tarfile.DIRTYPE
66+
ti.mode = ent.get('fmode', 0o755) & 0o777
67+
ti.mtime = ent.get('fmtime', 0)
68+
ti.size = 0
69+
tf.addfile(ti)
70+
else:
71+
data = ent.get('fcontent') or b''
72+
bio = io.BytesIO(data)
73+
ti = tarfile.TarInfo(name=name)
74+
ti.type = tarfile.REGTYPE
75+
ti.mode = ent.get('fmode', 0o644) & 0o777
76+
ti.mtime = ent.get('fmtime', 0)
77+
ti.size = len(data)
78+
tf.addfile(ti, fileobj=bio)
79+
finally:
80+
tf.close()
4981

5082
def main(argv=None):
5183
p = argparse.ArgumentParser(description="PyNeoFile (.neo) archiver", add_help=True)
5284
p.add_argument("-V","--version", action="version", version=__program_name__ + " " + __version__)
5385

54-
p.add_argument("-i","--input", nargs="+", required=True, help="Input files/dirs or archive file")
55-
p.add_argument("-o","--output", default=None, help="Output file or directory")
86+
p.add_argument("-i","--input", nargs="+", required=True, help="Input files/dirs or archive file ('-' = stdin for archive bytes or newline-separated paths with -c)")
87+
p.add_argument("-o","--output", default=None, help="Output file or directory ('-'=stdout for archive bytes; on -e streams a TAR)")
5688

5789
p.add_argument("-c","--create", action="store_true", help="Create a .neo archive from inputs")
58-
p.add_argument("-e","--extract", action="store_true", help="Extract an archive to --output")
90+
p.add_argument("-e","--extract", action="store_true", help="Extract an archive to --output (or stream TAR to stdout with -o -)")
5991
p.add_argument("-r","--repack", action="store_true", help="Repack an archive (change compression)")
6092
p.add_argument("-l","--list", action="store_true", help="List entries")
6193
p.add_argument("-v","--validate", action="store_true", help="Validate checksums")
@@ -70,6 +102,7 @@ def main(argv=None):
70102
p.add_argument("-C","--checksum", default="crc32", help="Checksum algorithm")
71103
p.add_argument("-s","--skipchecksum", action="store_true", help="Skip checks while reading")
72104
p.add_argument("-d","--verbose", action="store_true", help="Verbose listing")
105+
p.add_argument("-T","--text", action="store_true", help="Treat -i - as newline-separated path list when used with -c/--create")
73106

74107
args = p.parse_args(argv)
75108

@@ -80,52 +113,97 @@ def main(argv=None):
80113
level = None if args.level in (None, "",) else int(args.level)
81114
checksum = args.checksum
82115

116+
# Determine active action
117+
actions = ["create","extract","repack","list","validate"]
118+
active = next((a for a in actions if getattr(args, a)), None)
119+
if not active:
120+
p.error("one of --create/--extract/--repack/--list/--validate is required")
121+
122+
# Helper: read archive bytes from stdin for non-create ops
123+
def _maybe_archive_bytes():
124+
if infile0 == '-':
125+
return _stdin_bin().read()
126+
return None
127+
83128
if args.create:
129+
if infile0 == '-' and not args.text:
130+
# read newline-separated paths from stdin
131+
items = [line.strip() for line in sys.stdin if line.strip() and not line.startswith('#')]
132+
else:
133+
items = inputs
134+
84135
if args.convert:
85-
if not args.output: p.error("--output is required")
86-
ok = _convert_or_fail(infile0, args.output, formatspecs, checksum, compression, level)
87-
return 0 if ok else 1
88-
if not args.output: p.error("--output is required")
89-
N.pack_neo(inputs, args.output, formatspecs=formatspecs,
90-
checksumtypes=(checksum, checksum, checksum),
91-
compression=compression, compression_level=level)
92-
if args.verbose: sys.stderr.write("created: %s\n" % args.output)
136+
if not args.output:
137+
p.error("--output is required (use '-' to stream to stdout)")
138+
data = _convert_or_fail(infile0, (None if args.output == '-' else args.output),
139+
formatspecs, checksum, compression, level)
140+
if data is None:
141+
return 1
142+
if args.output == '-':
143+
_stdout_bin().write(data)
144+
return 0
145+
146+
if not args.output:
147+
p.error("--output is required for --create (use '-' to stream)")
148+
out_bytes = (args.output == '-')
149+
if out_bytes:
150+
data = N.pack_neo(items, None, formatspecs=formatspecs,
151+
checksumtypes=(checksum, checksum, checksum),
152+
compression=compression, compression_level=level)
153+
_stdout_bin().write(data)
154+
else:
155+
N.pack_neo(items, args.output, formatspecs=formatspecs,
156+
checksumtypes=(checksum, checksum, checksum),
157+
compression=compression, compression_level=level)
158+
if args.verbose: sys.stderr.write("created: %s\n" % args.output)
93159
return 0
94160

95161
if args.repack:
162+
src = _maybe_archive_bytes() or infile0
96163
if args.convert:
97-
if not args.output: p.error("--output is required")
98-
ok = _convert_or_fail(infile0, args.output, formatspecs, checksum, compression, level)
99-
return 0 if ok else 1
100-
if not args.output: p.error("--output is required")
101-
N.repack_neo(infile0, args.output, formatspecs=formatspecs,
102-
checksumtypes=(checksum, checksum, checksum),
103-
compression=compression, compression_level=level)
104-
if args.verbose: sys.stderr.write("repacked: %s\n" % args.output)
164+
if not args.output:
165+
p.error("--output is required (use '-' to stream)")
166+
data = _convert_or_fail(src, (None if args.output == '-' else args.output),
167+
formatspecs, checksum, compression, level)
168+
if data is None:
169+
return 1
170+
if args.output == '-':
171+
_stdout_bin().write(data)
172+
return 0
173+
174+
if not args.output:
175+
p.error("--output is required for --repack (use '-' to stream)")
176+
if args.output == '-':
177+
data = N.repack_neo(src, None, formatspecs=formatspecs,
178+
checksumtypes=(checksum, checksum, checksum),
179+
compression=compression, compression_level=level)
180+
_stdout_bin().write(data)
181+
else:
182+
N.repack_neo(src, args.output, formatspecs=formatspecs,
183+
checksumtypes=(checksum, checksum, checksum),
184+
compression=compression, compression_level=level)
185+
if args.verbose: sys.stderr.write("repacked: %s -> %s\n" % (('<stdin>' if infile0 == '-' else infile0), args.output))
105186
return 0
106187

107188
if args.extract:
189+
src = _maybe_archive_bytes() or infile0
190+
if args.output in (None, '.') and infile0 == '-':
191+
# default would attempt to mkdir '.'; fine
192+
pass
193+
if args.output == '-':
194+
# stream TAR to stdout
195+
arr = N.archive_to_array_neo(src, formatspecs=formatspecs, listonly=False,
196+
skipchecksum=args.skipchecksum, uncompress=True)
197+
_emit_tar_stream_from_array(arr, _stdout_bin())
198+
return 0
108199
outdir = args.output or "."
109-
if args.convert:
110-
tmp_arc = os.path.join(tempfile.gettempdir(), "pyneofile_convert.neo")
111-
ok = _convert_or_fail(infile0, tmp_arc, formatspecs, checksum, compression, level)
112-
if not ok: return 1
113-
use = tmp_arc
114-
else:
115-
use = infile0
116-
N.unpack_neo(use, outdir, formatspecs=formatspecs, skipchecksum=args.skipchecksum, uncompress=True)
200+
N.unpack_neo(src, outdir, formatspecs=formatspecs, skipchecksum=args.skipchecksum, uncompress=True)
117201
if args.verbose: sys.stderr.write("extracted → %s\n" % outdir)
118202
return 0
119203

120204
if args.list:
121-
if args.convert:
122-
tmp_arc = os.path.join(tempfile.gettempdir(), "pyneofile_convert.neo")
123-
ok = _convert_or_fail(infile0, tmp_arc, formatspecs, checksum, compression, level)
124-
if not ok: return 1
125-
use = tmp_arc
126-
else:
127-
use = infile0
128-
names = N.archivefilelistfiles_neo(use, formatspecs=formatspecs, advanced=args.verbose, include_dirs=True)
205+
src = _maybe_archive_bytes() or infile0
206+
names = N.archivefilelistfiles_neo(src, formatspecs=formatspecs, advanced=args.verbose, include_dirs=True)
129207
if not args.verbose:
130208
for n in names: sys.stdout.write(n + "\n")
131209
else:
@@ -136,14 +214,8 @@ def main(argv=None):
136214
return 0
137215

138216
if args.validate:
139-
if args.convert:
140-
tmp_arc = os.path.join(tempfile.gettempdir(), "pyneofile_convert.neo")
141-
ok = _convert_or_fail(infile0, tmp_arc, formatspecs, checksum, compression, level)
142-
if not ok: return 1
143-
use = tmp_arc
144-
else:
145-
use = infile0
146-
ok, details = N.archivefilevalidate_neo(use, formatspecs=formatspecs, verbose=args.verbose, return_details=True)
217+
src = _maybe_archive_bytes() or infile0
218+
ok, details = N.archivefilevalidate_neo(src, formatspecs=formatspecs, verbose=args.verbose, return_details=True)
147219
if not args.verbose:
148220
sys.stdout.write("valid: %s\n" % ("yes" if ok else "no"))
149221
else:

0 commit comments

Comments
 (0)