""" `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