Skip to content

Commit 25469fe

Browse files
committed
--upload and --download globbing
1 parent 4013031 commit 25469fe

3 files changed

Lines changed: 541 additions & 26 deletions

File tree

meshtastic/__main__.py

Lines changed: 137 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
except ImportError as e:
3838
have_test = False
3939

40+
import meshtastic.file_transfer_cli as file_transfer_cli
4041
import meshtastic.ota
4142
import meshtastic.util
4243
import meshtastic.serial_interface
@@ -454,28 +455,102 @@ def onConnected(interface):
454455
for path, sz in rows:
455456
print(f"{sz}\t{path}")
456457

457-
if args.cp:
458+
if args.upload is not None:
458459
closeNow = True
459-
src, dst = args.cp
460-
import os
461460
node = interface.localNode
462-
# Direction: if src is an existing local file → upload; otherwise → download
463-
if os.path.isfile(src):
464-
print(f"Uploading {src}{dst}")
465-
def _upload_progress(sent, total):
466-
pct = 100 * sent // total
467-
bar = '#' * (pct // 5) + '.' * (20 - pct // 5)
468-
print(f"\r [{bar}] {pct}%", end="", flush=True)
469-
ok = node.uploadFile(src, dst, on_progress=_upload_progress)
470-
print(f"\r {'OK' if ok else 'FAILED'}: {src}{dst} ")
461+
local_tokens = list(args.upload[:-1])
462+
remote_base = args.upload[-1]
463+
try:
464+
upload_pairs = file_transfer_cli.plan_upload(local_tokens, remote_base)
465+
except file_transfer_cli.FileTransferCliError as e:
466+
meshtastic.util.our_exit(str(e), 1)
467+
468+
def _upload_progress(sent, total):
469+
pct = 100 * sent // total if total else 0
470+
bar = '#' * (pct // 5) + '.' * (20 - pct // 5)
471+
print(f"\r [{bar}] {pct}%", end="", flush=True)
472+
473+
for i, (lp, devp) in enumerate(upload_pairs, start=1):
474+
print(f"Uploading ({i}/{len(upload_pairs)}) {lp}{devp}")
475+
ok = node.uploadFile(lp, devp, on_progress=_upload_progress)
476+
print(f"\r {'OK' if ok else 'FAILED'}: {lp}{devp} ")
471477
if not ok:
472478
meshtastic.util.our_exit("Upload failed", 1)
473-
else:
474-
print(f"Downloading {src}{dst}")
475-
def _download_progress(received, _total):
476-
print(f"\r {received} bytes received...", end="", flush=True)
477-
ok = node.downloadFile(src, dst, on_progress=_download_progress)
478-
print(f"\r {'OK' if ok else 'FAILED'}: {src}{dst} ")
479+
480+
if args.download is not None:
481+
closeNow = True
482+
node = interface.localNode
483+
rpath, lpath = args.download
484+
lpath_abs = os.path.abspath(os.path.expanduser(lpath))
485+
if os.path.isdir(lpath_abs):
486+
meshtastic.util.our_exit(
487+
"ERROR: --download LOCAL must be a file path, not a directory (use --download-tree or --download-glob).",
488+
1,
489+
)
490+
parent = os.path.dirname(lpath_abs)
491+
if parent:
492+
os.makedirs(parent, exist_ok=True)
493+
print(f"Downloading {rpath}{lpath}")
494+
def _download_progress(received, _total):
495+
print(f"\r {received} bytes received...", end="", flush=True)
496+
ok = node.downloadFile(rpath, lpath_abs, on_progress=_download_progress)
497+
print(f"\r {'OK' if ok else 'FAILED'}: {rpath}{lpath_abs} ")
498+
if not ok:
499+
meshtastic.util.our_exit("Download failed", 1)
500+
501+
if args.download_tree is not None:
502+
closeNow = True
503+
node = interface.localNode
504+
rdir, ldir = args.download_tree
505+
depth = 255
506+
rows = node.listDir(rdir.rstrip("/") or "/", depth=depth)
507+
if rows is None:
508+
meshtastic.util.our_exit("listDir failed", 1)
509+
try:
510+
tree_pairs = file_transfer_cli.plan_download_tree(rdir, ldir, rows)
511+
except file_transfer_cli.FileTransferCliError as e:
512+
meshtastic.util.our_exit(str(e), 1)
513+
if not tree_pairs:
514+
meshtastic.util.our_exit("No files matched for --download-tree", 1)
515+
516+
def _download_progress(received, _total):
517+
print(f"\r {received} bytes received...", end="", flush=True)
518+
519+
for i, (devp, locp) in enumerate(tree_pairs, start=1):
520+
os.makedirs(os.path.dirname(locp) or ".", exist_ok=True)
521+
print(f"Downloading ({i}/{len(tree_pairs)}) {devp}{locp}")
522+
ok = node.downloadFile(devp, locp, on_progress=_download_progress)
523+
print(f"\r {'OK' if ok else 'FAILED'}: {devp}{locp} ")
524+
if not ok:
525+
meshtastic.util.our_exit("Download failed", 1)
526+
527+
if args.download_glob is not None:
528+
closeNow = True
529+
node = interface.localNode
530+
pattern, ldir = args.download_glob
531+
try:
532+
base, _rel = file_transfer_cli.split_remote_glob_pattern(pattern)
533+
except file_transfer_cli.FileTransferCliError as e:
534+
meshtastic.util.our_exit(str(e), 1)
535+
depth = 255
536+
rows = node.listDir(base, depth=depth)
537+
if rows is None:
538+
meshtastic.util.our_exit("listDir failed", 1)
539+
try:
540+
glob_pairs = file_transfer_cli.plan_download_glob(pattern, ldir, rows)
541+
except file_transfer_cli.FileTransferCliError as e:
542+
meshtastic.util.our_exit(str(e), 1)
543+
if not glob_pairs:
544+
meshtastic.util.our_exit("No files matched for --download-glob", 1)
545+
546+
def _download_progress(received, _total):
547+
print(f"\r {received} bytes received...", end="", flush=True)
548+
549+
for i, (devp, locp) in enumerate(glob_pairs, start=1):
550+
os.makedirs(os.path.dirname(locp) or ".", exist_ok=True)
551+
print(f"Downloading ({i}/{len(glob_pairs)}) {devp}{locp}")
552+
ok = node.downloadFile(devp, locp, on_progress=_download_progress)
553+
print(f"\r {'OK' if ok else 'FAILED'}: {devp}{locp} ")
479554
if not ok:
480555
meshtastic.util.our_exit("Download failed", 1)
481556

@@ -1912,17 +1987,53 @@ def addLocalActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPars
19121987
default=None
19131988
)
19141989

1915-
group.add_argument(
1916-
"--cp",
1990+
xfer = group.add_mutually_exclusive_group()
1991+
xfer.add_argument(
1992+
"--upload",
1993+
help=(
1994+
"Upload local files to the device via XModem (requires matching firmware). "
1995+
"Usage: --upload LOCAL [LOCAL ...] REMOTE. "
1996+
"Last argument is the device path: for a single plain file LOCAL, REMOTE is the exact destination file path; "
1997+
"otherwise REMOTE is a directory prefix and relative paths are preserved. "
1998+
"LOCAL may be files, directories (recursive), or globs (quote patterns with **). "
1999+
"Each device path must be <= 128 UTF-8 bytes."
2000+
),
2001+
nargs="+",
2002+
metavar="SPEC",
2003+
default=None,
2004+
)
2005+
xfer.add_argument(
2006+
"--download",
2007+
help=(
2008+
"Download one file from the device. "
2009+
"Usage: --download REMOTE_FILE LOCAL_FILE. "
2010+
"For a directory tree use --download-tree; for remote globs use --download-glob."
2011+
),
2012+
nargs=2,
2013+
metavar=("REMOTE", "LOCAL"),
2014+
default=None,
2015+
)
2016+
xfer.add_argument(
2017+
"--download-tree",
19172018
help=(
1918-
"Copy a file to or from the device via XModem. "
1919-
"Usage: --cp <src> <dst>. "
1920-
"If <src> is an existing local file it is uploaded to <dst> on the device. "
1921-
"Otherwise <src> is treated as a device path and downloaded to local <dst>. "
1922-
"Use /__ext__/ or /__int__/ prefixes to target external or internal flash."
2019+
"Download a full remote directory tree via MFLIST + XModem. "
2020+
"Usage: --download-tree REMOTE_DIR LOCAL_DIR. "
2021+
"Only rows with size > 0 are treated as files."
19232022
),
19242023
nargs=2,
1925-
metavar=("SRC", "DST"),
2024+
metavar=("REMOTE_DIR", "LOCAL_DIR"),
2025+
default=None,
2026+
)
2027+
xfer.add_argument(
2028+
"--download-glob",
2029+
help=(
2030+
"Download remote files matching a glob (MFLIST at the literal base + filter). "
2031+
"Usage: --download-glob 'REMOTE_PATTERN' LOCAL_DIR. "
2032+
"Pattern must include * ? or [; ** matches across / (relative to the literal base)."
2033+
),
2034+
nargs=2,
2035+
metavar=("REMOTE_PATTERN", "LOCAL_DIR"),
2036+
default=None,
19262037
)
19272038

19282039
group.add_argument(

0 commit comments

Comments
 (0)