From 382fcc5f675f9942350013780335315757c245c5 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Thu, 15 Oct 2026 01:12:16 +0200 Subject: feat(cli): rm, and Delete in the interactive mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rm deletes files, resolved as get resolves paths and patterns, with the node's rule (design §5.4): the uploading account's devices, or the node's operator account. The whole run is checked against the index first (the entry's uploader_id, the role the join result gives this key): a file outside the rule, or a folder, refuses it before anything is asked or sent. Then one confirmation, or --yes. The node's challenge is checked against what was asked before it is signed. In the interactive mode, Delete on the right pane does the same, asked once per batch. Co-Authored-By: Claude Opus 5.5 --- .../meshbay-cli/src/meshbay_cli/tui/screens.py | 59 ++++++++++++++++++++++ 1 file changed, 59 insertions(+) (limited to 'packages/meshbay-cli/src/meshbay_cli/tui/screens.py') diff --git a/packages/meshbay-cli/src/meshbay_cli/tui/screens.py b/packages/meshbay-cli/src/meshbay_cli/tui/screens.py index e1ab620..0306325 100644 --- a/packages/meshbay-cli/src/meshbay_cli/tui/screens.py +++ b/packages/meshbay-cli/src/meshbay_cli/tui/screens.py @@ -6,6 +6,10 @@ with the rules `get` and `put` follow: an upload is decided whole before it starts (names the node refuses stop it, names already there are asked about once for the batch), nothing on either side is replaced without being asked, and a name from a group is made writable here before it is written. + +Delete removes the selection from a group with the rules `rm` follows: files +only, every one of them this account's to delete (uploaded by it, or this key +the node's operator) or none is, then asked once for the batch. """ import asyncio @@ -24,6 +28,7 @@ from ..client.account import Account from ..client.names import UnsafeName, local_path from ..client.paths import handle from ..client.pool import Pool +from ..client.remove import delete_file, may_delete from ..client.store import FILE_KEY_NOTICE, KEY_FILE from ..client.transfers import PART_SUFFIX, TransferError, create_dir, download, upload from ..client.tree import Tree @@ -95,6 +100,7 @@ class SignInScreen(Screen): class MainScreen(Screen): BINDINGS = [ Binding("f5", "copy", "Copy"), + Binding("delete", "delete", "Delete"), Binding("q", "app.quit", "Quit"), Binding("tab", "app.focus_next", "Switch", show=False), ] @@ -331,6 +337,59 @@ class MainScreen(Screen): except TransferError as e: self.strip.update_job(job, state="failed", note=e.message) + # ── Delete ─────────────────────────────────────────────────────────────── + + def action_delete(self) -> None: + focused = self.app.focused + if focused is None or self.remote not in focused.ancestors_with_self: + self.app.notify("Delete removes files from a group: select them on the " + "right.", timeout=4) + return + self.run_worker(self._delete(), group="delete") + + async def _delete(self) -> None: + remote = self.remote + rows = remote.selection() + if remote.group is None or not rows: + self.app.notify("Open a group and choose the files to delete.", timeout=4) + return + group = remote.group + try: + session = await self.pool.session(group) + except CliError as e: + self.app.notify(e.message, severity="error", markup=False) + return + folders = [r.name for r in rows if r.is_dir] + not_yours = [r.name for r in rows if not r.is_dir + and not may_delete(session, r.data["entry"], self.account.user_id)] + if folders or not_yours: + why = (["Folders are not deleted here: " + ", ".join(map(safe, folders))] + if folders else []) + if not_yours: + why.append("You did not upload these, and this machine's key is not the " + "node's operator: " + ", ".join(map(safe, not_yours))) + await self._ask("Nothing was deleted.\n" + "\n".join(why), [("ok", "OK")]) + return + names = ", ".join(safe(r.name) for r in rows[:5]) + ( + f" and {len(rows) - 5} more" if len(rows) > 5 else "") + answer = await self._ask( + f"Delete {len(rows)} file(s) from {safe(handle(group))} for every member? " + f"{names}", [("delete", "Delete"), ("cancel", "Cancel")]) + if answer != "delete": + return + remote.marks.clear() + deleted = 0 + for row in rows: + try: + await delete_file(session, row.data["entry"]["id"]) + deleted += 1 + except CliError as e: + self.app.notify(f"{safe(row.name)}: {e.message}", severity="error", + markup=False, timeout=8) + if deleted: + self.app.notify(f"Deleted {deleted} file(s).", timeout=4) + self._changed(group) + async def on_unmount(self) -> None: with contextlib.suppress(Exception): await self.pool.close() -- cgit v1.2.3