From 8353b50924cce9bd1d97fae82f9533b60eba46cc Mon Sep 17 00:00:00 2001 From: scott Date: Wed, 4 Mar 2026 11:22:16 -0500 Subject: [PATCH] Add per-share selection in interactive wizard After parsing the archive, present a numbered list of SMB and NFS shares and let the user pick which ones to migrate. Entering nothing (or 'all') keeps everything; 'n' skips the type entirely; space- separated numbers select specific shares. Because archive_data is filtered before the dry run, only selected shares are processed in both the dry and live runs, and the dataset existence check covers exactly the chosen share paths. Co-Authored-By: Claude Sonnet 4.6 --- truenas_migrate.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/truenas_migrate.py b/truenas_migrate.py index d975102..dd08ef3 100644 --- a/truenas_migrate.py +++ b/truenas_migrate.py @@ -1185,6 +1185,57 @@ def _confirm(label: str) -> bool: return False +def _select_shares(shares: list[dict], share_type: str) -> list[dict]: + """ + Display a numbered list of *shares* and return only those the user selects. + Enter (or 'all') returns all shares unchanged. 'n' / 'none' returns []. + """ + if not shares: + return shares + + print(f"\n {_bold(f'{share_type} shares in archive ({len(shares)}):')} \n") + for i, share in enumerate(shares, 1): + if share_type == "SMB": + name = share.get("name", "") + path = share.get("path", "") + print(f" {_cyan(str(i) + '.')} {name:<22} {_dim(path)}") + else: # NFS + pl = share.get("paths") or [] + path = share.get("path") or (pl[0] if pl else "") + extra = f" {_dim('+ ' + str(len(pl) - 1) + ' more')}" if len(pl) > 1 else "" + print(f" {_cyan(str(i) + '.')} {path}{extra}") + + print() + raw = _prompt( + f" Select {share_type} shares to migrate " + "(e.g. '1 3', Enter = all, 'n' = none)", + default="all", + ) + + low = raw.strip().lower() + if low in ("", "all"): + print(f" {_green('✓')} All {len(shares)} {share_type} share(s) selected.") + return shares + if low in ("n", "none", "0"): + print(f" {_yellow('–')} No {share_type} shares selected.") + return [] + + seen: set[int] = set() + selected: list[dict] = [] + for tok in raw.split(): + if tok.isdigit(): + idx = int(tok) - 1 + if 0 <= idx < len(shares) and idx not in seen: + seen.add(idx) + selected.append(shares[idx]) + + if selected: + print(f" {_green('✓')} {len(selected)} of {len(shares)} {share_type} share(s) selected.") + else: + print(f" {_yellow('–')} No valid selections; skipping {share_type} shares.") + return selected + + def interactive_mode() -> None: """Interactive wizard: pick archive → configure → dry run → confirm → apply.""" print( @@ -1258,6 +1309,13 @@ def interactive_mode() -> None: print() archive_data = parse_archive(str(chosen)) + # 5b ── Select individual shares ─────────────────────────────────────────── + if "smb" in migrate and archive_data["smb_shares"]: + archive_data["smb_shares"] = _select_shares(archive_data["smb_shares"], "SMB") + if "nfs" in migrate and archive_data["nfs_shares"]: + archive_data["nfs_shares"] = _select_shares(archive_data["nfs_shares"], "NFS") + print() + base_ns = dict( debug_tar=str(chosen), dest=host,