aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/ops.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-19 14:01:38 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-19 14:01:38 +0200
commitd2495a2c4b89fbbfc18cefec83ae96cabdd745e2 (patch)
tree463e4d11b1c742475aac07155818c9c64a1e43e5 /packages/meshbay-node/src/meshbay_node/ops.py
parentf8223293a211a87c92b1fed80f5ca53660f6b26c (diff)
parent933daccbcfde7705413d3a10db87d910c650ed42 (diff)
downloadmeshbay-d2495a2c4b89fbbfc18cefec83ae96cabdd745e2.tar.gz
Merge origin/main: the operator's upload ceiling beside the disk-thread work
One conflict, in §15.3's open list, and it was two changes agreeing rather than disagreeing: this side removed the rows for the third-party search bound and the node-announcement bound because both are now built (AV27, AV28), while the other side kept them and added a new one. Resolved by keeping what is genuinely still open — per-device revocation having no CLI — and leaving the two closed. `webrtc_server.py` merged without conflict but the two sides met inside one function: `_upload_chunk` gained the operator's `max_upload_gb` ceiling from there and the per-group lock and `off_disk` calls from here. Read back rather than trusted: the operator's ceiling now sits inside the critical section that keeps chunk ordering, and the unlink beside it goes to the disk thread with everything else. 2893 passed. The twelve `test_sticky_header.py[firefox]` setup errors are the open Firefox on this machine, as before.
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/ops.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/ops.py85
1 files changed, 50 insertions, 35 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/ops.py b/packages/meshbay-node/src/meshbay_node/ops.py
index 98f67c5..ca1b87d 100644
--- a/packages/meshbay-node/src/meshbay_node/ops.py
+++ b/packages/meshbay-node/src/meshbay_node/ops.py
@@ -40,6 +40,7 @@ from meshbay_common.crypto import (
from meshbay_node.config import DEFAULT_CONFIG_PATH
from meshbay_common.join import ROLE_MEMBER, ROLE_OPERATOR
from meshbay_node.roots import RootError, RootSet, off_disk
+from meshbay_node.roster import Roster
log = logging.getLogger(__name__)
@@ -623,17 +624,11 @@ async def list_groups(state: dict) -> dict:
members = await roster.list_members()
has_operator = any(m["role"] == "operator" and m["status"] == "active"
for m in members)
- from meshbay_node.config import DEFAULT_STUN_SERVERS
- nd = config.node if config else None
- defaults = {
- "invite_ttl_hours": nd.invite_ttl_hours if nd else 168,
- "pair_ttl_hours": nd.pair_ttl_hours if nd else 24,
- "device_request_ttl_minutes": nd.device_request_ttl_minutes if nd else 60,
- "max_concurrent_streams": nd.max_concurrent_streams if nd else 8,
- "transcode_incompatible_video": nd.transcode_incompatible_video if nd else True,
- "stun_servers": nd.stun_servers if nd and nd.stun_servers else list(DEFAULT_STUN_SERVERS),
- "ice_interfaces": nd.ice_interfaces if nd else [],
- }
+ from meshbay_node.config import node_settings_defaults
+ # No config (a test, an unconfigured node) falls back to NodeConfig()'s own
+ # values rather than to numbers repeated here, which is the copy this used
+ # to be: it was missing three settings and reported them as null.
+ defaults = node_settings_defaults(config.node if config else None)
if roster:
settings = await roster.node_settings(defaults)
else:
@@ -1288,23 +1283,33 @@ async def clear_denylist(state: dict, *, subject: str = "") -> dict:
# ── Node settings ────────────────────────────────────────────────────────────
+# What `set_node_settings` accepts, and how each value is validated. A module
+# constant so a test can hold its key set against `Roster.node_setting_keys()`:
+# this is the third list of the same settings, and the first two had already
+# drifted apart once — the reader's defaults covered fewer settings than the
+# resolver answered for, which is how node.toml's transfer pools came to be
+# parsed and then ignored. The kinds here are the *writer's* validation and
+# deliberately not the resolver's coercions.
+NODE_SETTING_WRITERS: dict[str, tuple[str, str]] = {
+ "invite_ttl_hours": ("int", Roster.SETTING_INVITE_TTL),
+ "pair_ttl_hours": ("int", Roster.SETTING_PAIR_TTL),
+ "device_request_ttl_minutes": ("int", Roster.SETTING_DEVICE_TTL),
+ "max_concurrent_streams": ("int", Roster.SETTING_MAX_STREAMS),
+ "max_concurrent_downloads": ("int", Roster.SETTING_MAX_DOWNLOADS),
+ "max_concurrent_uploads": ("int", Roster.SETTING_MAX_UPLOADS),
+ "max_upload_gb": ("size", Roster.SETTING_MAX_UPLOAD_GB),
+ "transcode_incompatible_video": ("bool", Roster.SETTING_TRANSCODE),
+ "stun_servers": ("stun_list", Roster.SETTING_STUN_SERVERS),
+ "ice_interfaces": ("list", Roster.SETTING_ICE_INTERFACES),
+}
+
+
async def get_node_settings(state: dict) -> dict:
"""Return current effective node settings."""
- from meshbay_node.config import DEFAULT_STUN_SERVERS
+ from meshbay_node.config import node_settings_defaults
roster = _roster(state)
config = _config(state)
- nd = config.node
- defaults = {
- "invite_ttl_hours": nd.invite_ttl_hours,
- "pair_ttl_hours": nd.pair_ttl_hours,
- "device_request_ttl_minutes": nd.device_request_ttl_minutes,
- "max_concurrent_streams": nd.max_concurrent_streams,
- "max_concurrent_downloads": nd.max_concurrent_downloads,
- "max_concurrent_uploads": nd.max_concurrent_uploads,
- "transcode_incompatible_video": nd.transcode_incompatible_video,
- "stun_servers": nd.stun_servers if nd.stun_servers else list(DEFAULT_STUN_SERVERS),
- "ice_interfaces": nd.ice_interfaces,
- }
+ defaults = node_settings_defaults(config.node)
if roster:
return await roster.node_settings(defaults)
return defaults
@@ -1316,17 +1321,7 @@ async def set_node_settings(state: dict, settings: dict) -> dict:
nd = config.node
conf_path = Path(state.get("config_path") or DEFAULT_CONFIG_PATH)
- allowed_keys = {
- "invite_ttl_hours": ("int", roster.SETTING_INVITE_TTL),
- "pair_ttl_hours": ("int", roster.SETTING_PAIR_TTL),
- "device_request_ttl_minutes": ("int", roster.SETTING_DEVICE_TTL),
- "max_concurrent_streams": ("int", roster.SETTING_MAX_STREAMS),
- "max_concurrent_downloads": ("int", roster.SETTING_MAX_DOWNLOADS),
- "max_concurrent_uploads": ("int", roster.SETTING_MAX_UPLOADS),
- "transcode_incompatible_video": ("bool", roster.SETTING_TRANSCODE),
- "stun_servers": ("stun_list", roster.SETTING_STUN_SERVERS),
- "ice_interfaces": ("list", roster.SETTING_ICE_INTERFACES),
- }
+ allowed_keys = NODE_SETTING_WRITERS
set_by = state.get("node_user_id", "")
updated = {}
@@ -1344,6 +1339,22 @@ async def set_node_settings(state: dict, settings: dict) -> dict:
setattr(nd, key, v)
await roster.set_node_setting(setting_key, str(v), set_by)
updated[key] = v
+ elif kind == "size":
+ # A quantity, not a count: half a gigabyte is a legitimate ceiling
+ # on a small disk, so this one is not run through the `int` branch
+ # above, whose floor of 1 would round it to "refuse everything".
+ # bool before float, as config.py does it: `true` is not 1 GB.
+ if isinstance(value, bool):
+ raise OpError(f"{key} must be a number")
+ try:
+ fv = float(value)
+ except (TypeError, ValueError):
+ raise OpError(f"{key} must be a number")
+ if fv <= 0:
+ raise OpError(f"{key} must be greater than zero")
+ setattr(nd, key, fv)
+ await roster.set_node_setting(setting_key, repr(fv), set_by)
+ updated[key] = fv
elif kind == "bool":
v = bool(value)
setattr(nd, key, v)
@@ -1383,6 +1394,10 @@ async def set_node_settings(state: dict, settings: dict) -> dict:
"max_concurrent_downloads"),
max_concurrent_uploads=updated.get(
"max_concurrent_uploads"))
+ if "max_upload_gb" in updated:
+ webrtc = state.get("webrtc")
+ if webrtc is not None:
+ webrtc.set_capacity(max_upload_gb=updated["max_upload_gb"])
if "stun_servers" in updated:
webrtc = state.get("webrtc")
if webrtc and hasattr(webrtc, '_stun'):