diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/config.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/config.py | 83 |
1 files changed, 79 insertions, 4 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/config.py b/packages/meshbay-node/src/meshbay_node/config.py index 3de2473..42ebcfd 100644 --- a/packages/meshbay-node/src/meshbay_node/config.py +++ b/packages/meshbay-node/src/meshbay_node/config.py @@ -47,13 +47,29 @@ max_concurrent_streams = 8 # Browser and native clients reach this node over WebRTC DataChannel via hub # signaling — no inbound port to open. QUIC is the optional direct path. -# Multiple groups — each with its own directory +# Multiple groups — each with one or more named directories ("roots"). +# +# 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. [[groups]] id = "" # set after joining name = "My Media" -shared_dir = "/home/user/Media" quic_port = 19010 + [[groups.roots]] + path = "/home/user/Media" + upload = 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 + +# The single-directory form still works and means the same thing — one root, +# named after the directory, receiving uploads. [[groups]] id = "" name = "Public Archive" @@ -97,10 +113,24 @@ class NodeConfig: @dataclass +class RootSpec: + """One named directory inside a group. See `meshbay_node/roots.py`.""" + 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 + + +@dataclass class GroupConfig: id: str = "" name: str = "" - shared_dir: str = "" + # A group's content is several named roots. `shared_dir` is the single-root + # form and is still read: it becomes one root named after its basename, which + # is why every path gained a segment. See roots.py for why there is no + # unprefixed shape. + roots: list[RootSpec] = field(default_factory=list) + shared_dir: str = "" # legacy single-root form, migrated at load 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 @@ -112,6 +142,19 @@ class GroupConfig: join_policy: str = "invite" # invite|open quic_port: int = 19010 # QUIC MNP port + def __post_init__(self) -> None: + """ + The single-directory form becomes one root, whoever built this. + + On the dataclass rather than in the TOML reader, because a GroupConfig is + also built by the CLI, by `group attach` and by tests. Putting the + migration in the parser alone left every one of those paths with a group + that had no directory at all — and it presented as "skipping group", + 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)] + @dataclass class KeystoreConfig: @@ -157,6 +200,35 @@ def _positive(value: object, default: int, name: str) -> int: return n +def _read_roots(group: dict) -> list[RootSpec]: + """ + A group's roots, from `[[groups.roots]]` or from the legacy `shared_dir`. + + Both forms are accepted and `shared_dir` is not deprecated for a single + directory — it is the same thing said shorter. Naming both is refused rather + than merged: which one receives uploads would be a guess, and a wrong guess + is discovered weeks later. + """ + specs = [ + 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 [] + ] + legacy = str(group.get("shared_dir", "") or "").strip() + if specs and legacy: + log.warning( + "group %r declares both shared_dir and [[groups.roots]] — using " + "roots and ignoring shared_dir = %s", + group.get("name", ""), legacy) + # A bare shared_dir needs no handling here: GroupConfig.__post_init__ turns + # it into one root for every construction path, not just this one. + return specs + + def load_config(path: Path = DEFAULT_CONFIG_PATH) -> Config: """ Load config from TOML file. Supports both single [group] and @@ -190,7 +262,10 @@ def load_config(path: Path = DEFAULT_CONFIG_PATH) -> Config: cfg.groups.append(GroupConfig( id=g.get("id", ""), name=g.get("name", ""), - shared_dir=g.get("shared_dir", ""), + roots=_read_roots(g), + # Ignored when roots are given explicitly (warned about in + # _read_roots); otherwise __post_init__ migrates it. + shared_dir="" if _read_roots(g) else g.get("shared_dir", ""), visibility=g.get("visibility", "private"), join_policy=g.get("join_policy", "invite"), quic_port=g.get("quic_port", cfg.node.quic_port), |