From 988a0eecd522431980174910db28e45a63ff2710 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Thu, 15 Oct 2026 12:32:24 +0200 Subject: feat(cli): account, rm -r and mkdir, Tab completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit account shows the account and does what the sign-up form and the Profile page do: register (the passphrase floor and the hub's proof of work checked here, the recovery key printed once), verify, change the passphrase, reset a forgotten one, change the e-mail address, sign out everywhere, delete. A new passphrase carries the copy of the account's keys each node keeps for browsers first — opened under the old M, the same keys sealed under the new one — and only then tells the hub; a refusal sets the copies back. A reset restores them from the recovery copy. The connections that do it never make keys nor ask for a device approval. This machine keeps the M it changed away from, so a node out of reach is carried at its next connection; a copy under an M it does not know is no longer rewritten, which locked the browsers out after a passphrase changed elsewhere. rm -r removes a folder and all it holds, from the node's operator key: the files, then the folders deepest first, one confirmation that counts them. mkdir makes folders, --parents the missing ones (not -p, the passphrase). completion bash|powershell prints the script that makes Tab complete actions, verbs, options and the paths of the groups this machine is signed in to, cached two minutes; the deb and rpm packages install the bash one. Co-Authored-By: Claude Opus 5.5 --- .../meshbay-cli/src/meshbay_cli/commands/mkdir.py | 108 +++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 packages/meshbay-cli/src/meshbay_cli/commands/mkdir.py (limited to 'packages/meshbay-cli/src/meshbay_cli/commands/mkdir.py') diff --git a/packages/meshbay-cli/src/meshbay_cli/commands/mkdir.py b/packages/meshbay-cli/src/meshbay_cli/commands/mkdir.py new file mode 100644 index 0000000..de8cdcb --- /dev/null +++ b/packages/meshbay-cli/src/meshbay_cli/commands/mkdir.py @@ -0,0 +1,108 @@ +""" +`mkdir`: make folders in a group, as the Files tab's "New folder" does. + +Any member may, where the root accepts uploads: organising where files go is +not privileged on the node (removing a folder is — see `rm -r`). The parent +must exist, unless --parents makes the missing ones too; with it a folder +already there is not an error. Not -p, which is the passphrase everywhere in +this program. Names follow the node's rule for uploads, checked here +first so nothing is sent that it would refuse. +""" + +import json + +from ..client.groups import my_groups +from ..client.names import same_name, upload_name_problem +from ..client.paths import handle, parse, resolve_group +from ..client.pool import Pool +from ..client.transfers import TransferError, create_dir +from ..client.tree import Tree +from ..errors import NOT_FOUND, PARTIAL, REFUSED, USAGE, CliError +from ..output import err, safe, sentence +from .context import Context + +HELP = "make folders in a group" + + +def add_parser(sub, parent) -> None: + p = sub.add_parser("mkdir", parents=[parent], help=HELP, description=sentence(HELP) + + ", where its root accepts uploads.") + p.add_argument("paths", nargs="+", metavar="PATH", + help="group[@owner]/root/.../new-folder") + p.add_argument("--parents", action="store_true", + help="make missing parent folders too; a folder already there is " + "not an error (not -p: that is --password)") + + +async def run(ctx: Context) -> int: + await ctx.hub.check_version() + account = ctx.account() + pool = Pool(account) + try: + return await _run(ctx, account, pool) + finally: + await pool.close() + await account.close() + + +async def _run(ctx: Context, account, pool: Pool) -> int: + a = ctx.args + groups = await my_groups(account) + plans = [] # (session, group, [(parent, name), ...]) + for text in a.paths: + remote = parse(text) + if remote is None or len(remote.parts) < 2: + raise CliError(USAGE, "Name the folder to make inside a root: " + "group[@owner]/root/folder") + group = resolve_group(groups, remote.group) + session = await pool.session(group) + tree = Tree.from_index(await session.index()) + shown = f"{handle(group)}/{remote.inner}" + root = tree.roots.get(remote.parts[0]) + if root is None: + raise CliError(NOT_FOUND, f"No root {safe(remote.parts[0])} in " + f"{safe(handle(group))}.") + if not root.get("available", True): + raise CliError(REFUSED, f"{safe(shown)}: its drive is disconnected from the node.") + if not root.get("writable"): + raise CliError(REFUSED, f"{safe(shown)}: the root is read-only — its operator " + "does not accept uploads there.") + steps = [] + for depth in range(2, len(remote.parts) + 1): + parent, name = "/".join(remote.parts[:depth - 1]), remote.parts[depth - 1] + if (why := upload_name_problem(name, is_dir=True)): + raise CliError(REFUSED, f"{safe(name)}: {why}") + path = f"{parent}/{name}" + if tree.is_dir(path): + continue + # Folders are told apart without case, as the node does. + clash = next((i for i in tree.children(parent) if same_name(i.name, name)), + None) if tree.is_dir(parent) else None + if clash is not None: + raise CliError(REFUSED, f"{safe(handle(group))}/{safe(clash.path)} is already " + "there" + ("" if clash.is_dir else " (a file)") + ".") + if depth < len(remote.parts) and not a.parents: + raise CliError(NOT_FOUND, f"No folder {safe(handle(group))}/{safe(path)} " + "(--parents makes it too).") + steps.append((parent, name)) + if not steps and not a.parents: + raise CliError(REFUSED, f"{safe(shown)} is already there.") + plans.append((session, group, steps)) + + made, failed = [], [] + for session, group, steps in plans: + for parent, name in steps: + try: + path = await create_dir(session, parent, name) + except TransferError as e: + failed.append(f"{handle(group)}/{parent}/{name}: {e.message}") + break # the folders below it cannot follow + made.append(f"{handle(group)}/{path}") + if a.json: + print(json.dumps({"made": made, "failed": failed}, ensure_ascii=False)) + elif not a.quiet: + for m in made: + err(f"Made {safe(m)}/") + for f in failed: + err(f"meshbay-cli: {safe(f)}") + return PARTIAL if failed else 0 -- cgit v1.2.3