aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/config.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-06 16:05:39 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-06 16:05:39 +0200
commite76e27868b30a2b00b1ba42dd8e7ee6071e0c0d7 (patch)
tree3c23483207d77adf3b67f290b67a57ce185fb1a1 /packages/meshbay-node/src/meshbay_node/config.py
parent0ed078c92cabab1dab0f70f321562032ea549ce6 (diff)
downloadmeshbay-e76e27868b30a2b00b1ba42dd8e7ee6071e0c0d7.tar.gz
feat: groups refactor Phase 1 — root RO/RW model + shared directories UI
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 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/config.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/config.py42
1 files changed, 20 insertions, 22 deletions
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(