"""Node-wide settings: the denylist, STUN servers, transfer caps.""" import sys from meshbay_node.cli.api import _daemon_api, _resolve_group from meshbay_node.config import DEFAULT_CONFIG_PATH, load_config def denylist(args) -> None: cfg = load_config(args.config or DEFAULT_CONFIG_PATH) sub = args.subcommand or "show" if sub == "show": out = _daemon_api(cfg, "/api/denylist") total = out.get("count", 0) if not total: print("denylist empty — nothing is being refused") return for kind in ("users", "groups", "jtis"): for entry in out.get(kind, []): print(f" {kind[:-1]:<6} {entry}") print(f"\n{total} entr(y/ies). These survive a restart (finding H4).") return if sub == "clear": if not args.yes: what = args.target or "EVERY entry" print(f"Clearing the denylist re-admits {what}.") print("A revocation the hub sent will not come back on its own.") if input("clear now? [y/N] ").strip().lower() not in ("y", "yes"): print("cancelled") return out = _daemon_api(cfg, f"/api/denylist/clear?subject={args.target or ''}", method="POST") print(f"removed {out['removed']} entr(y/ies) ({out['subject']})") return print("usage: meshbay-node denylist show|clear [identifier] [--yes]") sys.exit(1) def stun(args) -> None: cfg = load_config(args.config or DEFAULT_CONFIG_PATH) sub = args.subcommand or "list" if sub == "list": out = _daemon_api(cfg, "/api/node-settings") servers = out.get("stun_servers", []) if not servers: print("stun servers (none configured)") return for i, s in enumerate(servers, 1): print(f" {i}. {s}") return if sub == "add": url = args.target if not url: print("usage: meshbay-node stun add ") sys.exit(1) if not url.startswith("stun:"): print(f"error: STUN URL must start with stun: — got {url!r}") sys.exit(1) out = _daemon_api(cfg, "/api/node-settings") servers = out.get("stun_servers", []) if url in servers: print(f"already present: {url}") return servers.append(url) _daemon_api(cfg, "/api/node-settings", method="PUT", body={"stun_servers": servers}) print(f"added {url} ({len(servers)} servers total)") return if sub == "remove": url = args.target if not url: print("usage: meshbay-node stun remove ") sys.exit(1) out = _daemon_api(cfg, "/api/node-settings") servers = out.get("stun_servers", []) if url not in servers: print(f"not found: {url}") sys.exit(1) servers.remove(url) _daemon_api(cfg, "/api/node-settings", method="PUT", body={"stun_servers": servers}) print(f"removed {url} ({len(servers)} servers remaining)") return if sub == "reset": from meshbay_node.config import DEFAULT_STUN_SERVERS _daemon_api(cfg, "/api/node-settings", method="PUT", body={"stun_servers": list(DEFAULT_STUN_SERVERS)}) print("STUN servers reset to defaults:") for s in DEFAULT_STUN_SERVERS: print(f" {s}") return print("usage: meshbay-node stun list|add|remove|reset [url]") sys.exit(1) def transfers(args) -> None: cfg = load_config(args.config or DEFAULT_CONFIG_PATH) sub = args.subcommand or "show" if sub == "show": out = _daemon_api(cfg, "/api/transfers") for kind, pool in out.get("pools", {}).items(): print(f" {kind:<9} {pool['in_use']}/{pool['cap']} in use, " f"{pool['queued']} queued (node-wide)") # From the daemon, not from node.toml: a change made on the Node # page is live before it is written back, and the number to print # is the one being enforced. The local file is the fallback rather # than a literal, so there is no second copy of the default here. settings = _daemon_api(cfg, "/api/node-settings") gb = settings.get("max_upload_gb") or cfg.node.max_upload_gb print(f"\n largest single upload: {gb:g} GB per file " f"(meshbay-node transfers max-size )") # Per group, because that is the cap that decides how many one # person runs at once — and it is not the node-wide number. An # operator raising `transfers set 8 8` and still seeing two at a # time is looking at this line, which used to print the node's # default and say nothing about where it came from. groups = out.get("groups") or [] if groups: print("\n per member, per group " "(meshbay-node transfers per-member
    --group X):") for g in groups: how = "set" if g["set"] else "default" print(f" {g['name']:<20} {g['download']} download(s), " f"{g['upload']} upload(s) [{how}]") leases = out.get("leases", []) if not leases: print("\n nothing transferring") return print(f"\n {'transfer':<14}{'kind':<10}{'state':<9}" f"{'user':<12}{'bytes':>12}") for x in leases: where = f" (#{x['ahead'] + 1} in queue)" if x["state"] == "queued" else "" print(f" {x['tr']:<14}{x['kind']:<10}{x['state']:<9}" f"{x['user_id'][:10]:<12}{x['bytes']:>12}{where}") return if sub == "set": # `transfers set 4 2` — downloads, then uploads. Node-wide; the # per-member cap is a group's setting and is signed, so it is not # settable from here (see `ops.set_transfer_limits`). values = [v for v in (args.target, args.value) if v] if len(values) != 2: print("usage: meshbay-node transfers set ") sys.exit(1) try: downloads, uploads = int(values[0]), int(values[1]) except ValueError: print("error: both values must be whole numbers") sys.exit(1) if downloads < 1 or uploads < 1: print("error: a cap below 1 is not 'unlimited'; it would stop " "every transfer. Revoke the member instead.") sys.exit(1) out = _daemon_api(cfg, "/api/node-settings", method="PUT", body={"max_concurrent_downloads": downloads, "max_concurrent_uploads": uploads}) print(f"downloads: {downloads}, uploads: {uploads} " f"(applied now, and kept in node.toml)") return if sub == "max-size": # The largest single file a member may upload here. Not a # concurrency cap like `set` — it is the one limit that bounds what # a member writes to the operator's disk, which is why it lives # beside them rather than under a verb of its own. if not args.target: print("usage: meshbay-node transfers max-size ") sys.exit(1) try: gb = float(args.target) except ValueError: print("error: the size must be a number of GB (e.g. 8, or 0.5)") sys.exit(1) if gb <= 0: print("error: a ceiling of zero is not 'unlimited'; it would " "refuse every upload. Make the root read-only instead.") sys.exit(1) _daemon_api(cfg, "/api/node-settings", method="PUT", body={"max_upload_gb": gb}) print(f"largest single upload: {gb:g} GB per file " f"(applied now, and kept in node.toml)") return if sub == "per-member": # How many transfers ONE member may run at once in this group. Not # the same knob as `set`, which is the machine's total — and the # reason "I set 8 8 and still only get two" is the commonest # confusion here: per-member is checked first, by design. values = [v for v in (args.target, args.value) if v] if len(values) != 2: print("usage: meshbay-node transfers per-member " " [--group NAME]") sys.exit(1) try: downloads, uploads = int(values[0]), int(values[1]) except ValueError: print("error: both values must be whole numbers") sys.exit(1) if downloads < 1 or uploads < 1: print("error: a cap below 1 is not 'unlimited'; it would stop " "every transfer for that member. Revoke them instead.") sys.exit(1) group_id = _resolve_group(cfg, args.group) out = _daemon_api(cfg, f"/api/groups/{group_id}/transfer-limits", method="PUT", body={"downloads": downloads, "uploads": uploads}) got = out.get("limits", {}) started = out.get("started") or [] print(f"each member of this group may now run " f"{got.get('download')} download(s) and " f"{got.get('upload')} upload(s) at once") if started: print(f"{len(started)} waiting transfer(s) started at once") return print("usage: meshbay-node transfers show|set |" "max-size |per-member [--group NAME]") sys.exit(1)