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