summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/src/meshbay_cli/commands/rm.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-cli/src/meshbay_cli/commands/rm.py')
-rw-r--r--packages/meshbay-cli/src/meshbay_cli/commands/rm.py111
1 files changed, 111 insertions, 0 deletions
diff --git a/packages/meshbay-cli/src/meshbay_cli/commands/rm.py b/packages/meshbay-cli/src/meshbay_cli/commands/rm.py
new file mode 100644
index 0000000..6f5e869
--- /dev/null
+++ b/packages/meshbay-cli/src/meshbay_cli/commands/rm.py
@@ -0,0 +1,111 @@
+"""
+`rm`: delete files from groups. Asked first; `--yes` answers for a script.
+
+Paths and patterns are resolved against each group's index, as `get` resolves
+them, and the whole run is checked before anything is asked or sent: a file
+this account may not delete — not uploaded by it, and this machine's key not
+the node's operator — refuses the run (exit 8), as a folder does. The node
+checks again on every file; one it refuses is reported and the others go on
+(exit 4).
+"""
+
+import json
+
+from ..client.groups import my_groups
+from ..client.paths import handle, parse, resolve_group
+from ..client.pool import Pool
+from ..client.remove import delete_file, may_delete
+from ..client.tree import Tree
+from ..errors import NODE, NOT_FOUND, PARTIAL, REFUSED, USAGE, CliError
+from ..output import confirm, err, safe, sentence
+from .context import Context
+
+HELP = "delete files you uploaded (or any, from the node's operator key)"
+
+# How many names the confirmation shows before it counts the rest.
+SHOWN = 10
+
+
+def add_parser(sub, parent) -> None:
+ p = sub.add_parser("rm", parents=[parent], help=HELP, description=sentence(HELP)
+ + ". Wildcards (* ? [...]) are matched against the group, so "
+ "quote them. Being the group's owner is not enough: the node "
+ "lets its operator's key, and the uploader's devices, delete.")
+ p.add_argument("paths", nargs="+", metavar="PATH", help="group[@owner]/root/.../file")
+ p.add_argument("-y", "--yes", action="store_true", help="do not ask for confirmation")
+
+
+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)
+ trees: dict[str, Tree] = {}
+ targets = [] # (session, entry, shown)
+ missing, folders, not_yours = [], [], []
+ for text in a.paths:
+ remote = parse(text)
+ if remote is None or not remote.parts:
+ raise CliError(USAGE, "Name the files to delete, inside a group.")
+ group = resolve_group(groups, remote.group)
+ session = await pool.session(group)
+ if group["id"] not in trees:
+ trees[group["id"]] = Tree.from_index(await session.index())
+ tree = trees[group["id"]]
+ found = tree.expand(remote.parts)
+ if not found and tree.file(remote.inner):
+ found = [remote.inner] # a literal name with brackets in it
+ if not found:
+ missing.append(text)
+ for path in found:
+ shown = f"{handle(group)}/{path}"
+ entry = tree.file(path)
+ if entry is None:
+ folders.append(shown)
+ elif not may_delete(session, entry, account.user_id):
+ not_yours.append(shown)
+ elif all(e["id"] != entry["id"] for _, e, _ in targets):
+ targets.append((session, entry, shown))
+ if missing:
+ raise CliError(NOT_FOUND, "Nothing in your groups matches: "
+ + ", ".join(safe(m) for m in missing))
+ if folders:
+ raise CliError(USAGE, "rm deletes files; these are folders: "
+ + ", ".join(safe(f) for f in folders))
+ if not_yours:
+ raise CliError(REFUSED, "Not deleted — you did not upload these, and this machine's "
+ "key is not the node's operator:\n "
+ + "\n ".join(safe(n) for n in not_yours))
+
+ names = [shown for _, _, shown in targets]
+ listed = "\n ".join(safe(n) for n in names[:SHOWN])
+ more = f"\n ... and {len(names) - SHOWN} more" if len(names) > SHOWN else ""
+ confirm(f"Delete {len(names)} file{'s' if len(names) != 1 else ''} for every member "
+ f"of the group?\n {listed}{more}\n", a.yes)
+
+ deleted, failed = [], []
+ for session, entry, shown in targets:
+ try:
+ await delete_file(session, entry["id"])
+ deleted.append(shown)
+ except CliError as e:
+ if e.code not in (NODE, REFUSED):
+ raise
+ failed.append(f"{shown}: {e.message}")
+ if a.json:
+ print(json.dumps({"deleted": deleted, "failed": failed}, ensure_ascii=False))
+ elif not a.quiet:
+ n = len(deleted)
+ err(f"Deleted {n} file{'s' if n != 1 else ''}.")
+ for f in failed:
+ err(f"meshbay-cli: {safe(f)}")
+ return PARTIAL if failed else 0