From e76e27868b30a2b00b1ba42dd8e7ee6071e0c0d7 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sun, 6 Sep 2026 16:05:39 +0200 Subject: feat: groups refactor Phase 1 — root RO/RW model + shared directories UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the upload boolean with per-root writable/removable/ejected flags. Backend: new ops (update_root, eject_root, plug_root), MNP 1.1 protocol messages, live RootSet updates so API always reflects current state, CLI root subcommand (add/remove/set/list/eject/plug). Frontend: SharedDirectoriesTable with optimistic toggle switches, eject/plug in Files and Settings, upload gated on root.writable, ejected-root filtering in all media apps, updated Create Group wizard, 10-locale i18n. Co-Authored-By: Claude Opus 4.6 --- packages/meshbay-node/src/meshbay_node/config.py | 42 +++++++++++------------- 1 file changed, 20 insertions(+), 22 deletions(-) (limited to 'packages/meshbay-node/src/meshbay_node/config.py') diff --git a/packages/meshbay-node/src/meshbay_node/config.py b/packages/meshbay-node/src/meshbay_node/config.py index 0e5a7de..4712312 100644 --- a/packages/meshbay-node/src/meshbay_node/config.py +++ b/packages/meshbay-node/src/meshbay_node/config.py @@ -89,25 +89,20 @@ transcode_incompatible_video = true # A root's name is the directory's basename, and it becomes the first segment of # every path members see: /home/user/Media appears to everyone as "Media/". # Two roots cannot share a name (compared without regard to case), and no root -# may sit inside another. Exactly one root receives uploads. +# may sit inside another. A writable root accepts uploads from group members. [[groups]] id = "" # set after joining name = "My Media" quic_port = 19010 [[groups.roots]] - path = "/home/user/Media" - upload = true + path = "/home/user/Media" + writable = true [[groups.roots]] - path = "/run/media/user/USB/Musique" # an external drive is fine: if it is - kind = "audio" # unplugged the root goes unavailable - # and its files stay in the index, - # rather than looking deleted - -# upload_dir: a separate directory for uploads. Files land directly in it, -# not in an "uploads" subdirectory. It appears as its own root in the index. -# upload_dir = "/home/user/Incoming" + path = "/run/media/user/USB/Musique" + kind = "audio" + removable = true # eject before unplugging # The single-directory form still works and means the same thing — one root, # named after the directory, receiving uploads. @@ -187,7 +182,8 @@ class RootSpec: path: str = "" name: str = "" # empty → the directory's basename, derived at load kind: str = "generic" # generic|video|audio|photo — a view hint, unused for now - upload: bool = False # exactly one root per group receives uploads + writable: bool = False # RW roots accept uploads from group members + removable: bool = False # operator can eject this root before unplugging the device direct: bool = False # uploads land at root path, not in a subdirectory @@ -201,7 +197,7 @@ class GroupConfig: # unprefixed shape. roots: list[RootSpec] = field(default_factory=list) shared_dir: str = "" # legacy single-root form, migrated at load - upload_dir: str = "" # separate filesystem path for uploads + upload_dir: str = "" # legacy — migrated to a writable root visibility: str = "private" # public|private — discoverability, not admission # Admission. "invite" (default) means a newcomer needs a one-time pairing code # before the node wraps the group key for them; "open" means the node pins @@ -224,12 +220,12 @@ class GroupConfig: which reads like configuration rather than a bug. """ if not self.roots and self.shared_dir.strip(): - self.roots = [RootSpec(path=self.shared_dir.strip(), upload=True)] + self.roots = [RootSpec(path=self.shared_dir.strip(), writable=True)] if self.upload_dir.strip(): for r in self.roots: - r.upload = False + r.writable = False self.roots.append(RootSpec( - path=self.upload_dir.strip(), upload=True, direct=True)) + path=self.upload_dir.strip(), writable=True, direct=True)) @dataclass @@ -285,15 +281,17 @@ def _read_roots(group: dict) -> list[RootSpec]: than merged: which one receives uploads would be a guess, and a wrong guess is discovered weeks later. """ - specs = [ - RootSpec( + specs = [] + for r in group.get("roots", []) or []: + # Backward compat: old configs have `upload = true` instead of `writable` + writable = bool(r.get("writable", r.get("upload", False))) + specs.append(RootSpec( path=str(r.get("path", "")), name=str(r.get("name", "")), kind=str(r.get("kind", "generic")), - upload=bool(r.get("upload", False)), - ) - for r in group.get("roots", []) or [] - ] + writable=writable, + removable=bool(r.get("removable", False)), + )) legacy = str(group.get("shared_dir", "") or "").strip() if specs and legacy: log.warning( -- cgit v1.2.3