summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/src/meshbay_cli/commands/mkdir.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-cli/src/meshbay_cli/commands/mkdir.py')
-rw-r--r--packages/meshbay-cli/src/meshbay_cli/commands/mkdir.py108
1 files changed, 108 insertions, 0 deletions
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