From c2c49182e6d4f1f1ef3ed3ba5e8699912f9a27bd Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 19 Sep 2026 01:02:05 +0200 Subject: feat(node): per-file upload ceiling is an operator setting, default 8 GB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Was a 4 GB constant in webrtc_server.py, the same on a small board and on a machine holding a library. Now max_upload_gb in node.toml, on the Node page and via `meshbay-node transfers max-size`, read from the transport context per chunk so a change reaches an upload already running. MESHBAY_DESIGN.md §6.4; §15.3 records a defect found beside it. Co-Authored-By: Claude Opus 5 --- docs/MESHBAY_DESIGN.md | 15 +- docs/MESHBAY_NODE_PROTOCOL.md | 6 +- man/meshbay-node.1 | 14 ++ .../src/meshbay_hub/static/locales/de.js | 2 + .../src/meshbay_hub/static/locales/en.js | 2 + .../src/meshbay_hub/static/locales/es.js | 2 + .../src/meshbay_hub/static/locales/fr.js | 2 + .../src/meshbay_hub/static/locales/it.js | 2 + .../src/meshbay_hub/static/locales/ja.js | 2 + .../src/meshbay_hub/static/locales/nl.js | 2 + .../src/meshbay_hub/static/locales/pl.js | 2 + .../src/meshbay_hub/static/locales/pt-BR.js | 2 + .../src/meshbay_hub/static/locales/zh-CN.js | 2 + .../src/meshbay_hub/static/node-page.js | 8 ++ packages/meshbay-node/src/meshbay_node/config.py | 37 +++++ packages/meshbay-node/src/meshbay_node/daemon.py | 47 ++++++- packages/meshbay-node/src/meshbay_node/ops.py | 22 +++ packages/meshbay-node/src/meshbay_node/roster.py | 4 + .../src/meshbay_node/transport/webrtc_server.py | 41 +++++- .../meshbay-node/tests/test_chat_is_bounded.py | 2 +- packages/meshbay-node/tests/test_cli_dispatch.py | 5 + .../meshbay-node/tests/test_transfer_settings.py | 6 +- .../meshbay-node/tests/test_upload_size_cap.py | 152 +++++++++++++++++++++ 23 files changed, 359 insertions(+), 20 deletions(-) create mode 100644 packages/meshbay-node/tests/test_upload_size_cap.py diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index 03ae77e..dcfc77b 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -1326,9 +1326,15 @@ Five protections, and they are the substance: and `stat()` is itself case-insensitive on NTFS and exFAT, so this already holds there; - **strict chunk ordering**; -- a **size cap** — 4 GB per file. There is deliberately no aggregate quota yet, and - that gap is named in §15.3 rather than left to be discovered: a member can still - fill the operator's disk one capped file at a time; +- a **size cap** — 8 GB per file by default, and **the operator's to set** + (`max_upload_gb` in node.toml, on the Node page, or `meshbay-node transfers + max-size`), because the disk that fills is theirs and a Pi and a film server + are not the same machine. It is read from the transport context on every + chunk, so a change applies to an upload already running. A file past the + ceiling is refused at the chunk that crosses it and its `.part` is deleted. + There is deliberately no aggregate quota yet, and that gap is named in §15.3 + rather than left to be discovered: a member can still fill the operator's + disk one capped file at a time; - the target root must be **writable and available**, enforced by the node. **There is no quarantine subdirectory.** A folder appearing beside the operator's @@ -3062,7 +3068,8 @@ process runs it — `systemctl --user` on Linux, Task Scheduler on Windows. | **C4** for browser-using accounts | Open until the signed bundle opt-out ships (O3) | | **T3** for browser users | **Accepted permanently.** Removed for native clients, and that removal's value depends on reproducible builds | | **Hub identity pinning** (O13) | Nothing pins the hub's key. Bounded, because a substituted hub can neither read content nor ship code to a native client | -| **Aggregate upload quota** | Per-file caps exist; a per-user or per-group total does not | +| **Aggregate upload quota** | Per-file caps exist and the operator sets theirs (§6.4); a per-user or per-group total does not | +| **`max_concurrent_downloads` / `max_concurrent_uploads` are lost at startup** | The daemon builds the `defaults` dict it hands `roster.node_settings` without those two keys, so a node that has never had them set from the panel resolves both to `None`, assigns that back onto the config, and builds the transport with it — the pools then come up at `transfers.py`'s defaults and **what node.toml says about them is read and ignored**. The Node page and the CLI still work, because writing the setting is what creates the roster row the startup path then finds. Found while adding the per-file ceiling beside them, which is in that dict | | **A signed upload transcript** | Ownership is recorded by the node and verifiable by nobody else (§5.4). Making it provable is a transcript the uploader signs, stored with the entry — designed in outline, not built | | Forward secrecy in group chat | **Given up deliberately and on the record** (§4.5). If it becomes a requirement it belongs in 1:1 DM | | Metadata at the hub | Membership, and who posted in which group and when. A known leak, not a solved problem (§7.1) | diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md index 51feacd..b2508c1 100644 --- a/docs/MESHBAY_NODE_PROTOCOL.md +++ b/docs/MESHBAY_NODE_PROTOCOL.md @@ -106,7 +106,7 @@ Identical on every transport: | File chunk (plaintext) | 1 MiB | `CHUNK_SIZE` | | Video segment (plaintext, before encryption) | 256 KiB | `STREAM_SEGMENT_SIZE` | | Upload chunk sent by the browser | 48 KiB | fits the aiortc SCTP limit after msgpack overhead | -| Upload total per file | 4 GiB | `MAX_UPLOAD_BYTES` | +| Upload total per file | 8 GiB, operator-settable | `MAX_UPLOAD_BYTES`, `max_upload_gb` | | Files one session may read at once **without a transfer lease** | 12 | `MAX_LEASELESS_IN_FLIGHT` (§11.2) | The two-tier frame limit is not tidiness. A flat 64 MiB budget applied before @@ -1483,7 +1483,7 @@ ct = AES-256-GCM(chunk_key).encrypt(nonce, plaintext) no AAD | | root writable and available | | destination resolves in-group | | chunk_index == next expected - | | running total <= 4 GiB + | | running total <= max_upload_gb | | append to .part |<- file_upload_ack {v, upload_id, chunk_index, | | nonce, ct} ------------------------------------| @@ -2227,7 +2227,7 @@ LP(x) = uint32be(len(x)) || x every field, no exceptions | `PRE_HANDSHAKE_MAX_MSG` / `MAX_MSG` | 64 KiB / 64 MiB | `webrtc_server.py` | | `CHUNK_SIZE` | 1 MiB | ” | | `DOWNLOAD_BUFFER_HIGH` | 2 MiB | ” | -| `MAX_UPLOAD_BYTES` | 4 GiB | ” | +| `MAX_UPLOAD_BYTES` | 8 GiB, default only — `max_upload_gb` overrides it per node | ” | | Download pipeline / chunk retry (client) | 8 in flight; 6 attempts, 1.5 s apart | `file-utils.js` | | Upload chunk / window / send-buffer high water (client) | 48 KiB / 32 / 1 MiB | `transport.js` | | `UPLOAD_ID_LEN` | 16 bytes, hex on the wire | `protocol.py` | diff --git a/man/meshbay-node.1 b/man/meshbay-node.1 index 7c7cf96..24ba284 100644 --- a/man/meshbay-node.1 +++ b/man/meshbay-node.1 @@ -372,6 +372,20 @@ Must be a positive integer; zero or negative values are ignored with a warning. . .TP +.B max_upload_gb +The largest single file a member may upload to this node, in GB. It is this +machine's disk that fills, so the ceiling belongs to the operator: lower it on +a small disk, raise it for a library of films. Fractions are accepted +(\fB0.5\fR is 512\ MB). A file past the ceiling is refused at the chunk that +crosses it and its partial file is removed. There is no aggregate quota behind +it. Default: +.BR 8 . +Also settable from the Node page and with +.BR "meshbay\-node transfers max\-size" , +either of which applies it at once and writes it back here. Zero and negative +values are ignored with a warning. +. +.TP .B transcode_incompatible_video When .BR true " (default)," diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index 9d9319d..2d925e1 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -886,6 +886,8 @@ export default { 'node.setting_max_downloads': 'Max. gleichzeitige Downloads', 'node.setting_max_uploads': 'Max. gleichzeitige Uploads', 'node.setting_transcode': 'Transcode incompatible video', + 'node.setting_max_upload': 'Größter einzelner Upload', + 'node.setting_unit_gb': 'GB', 'node.setting_unit_hours': 'hours', 'node.setting_unit_minutes': 'min', 'node.setting_on': 'On', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index a928a52..690a70b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -1038,6 +1038,8 @@ export default { 'node.setting_max_downloads': 'Max concurrent downloads', 'node.setting_max_uploads': 'Max concurrent uploads', 'node.setting_transcode': 'Transcode incompatible video', + 'node.setting_max_upload': 'Largest single upload', + 'node.setting_unit_gb': 'GB', 'node.setting_unit_hours': 'hours', 'node.setting_unit_minutes': 'min', 'node.setting_on': 'On', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index 2e6ea26..c48f942 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -881,6 +881,8 @@ export default { 'node.setting_max_downloads': 'Descargas simultáneas máximas', 'node.setting_max_uploads': 'Subidas simultáneas máximas', 'node.setting_transcode': 'Transcode incompatible video', + 'node.setting_max_upload': 'Subida individual máxima', + 'node.setting_unit_gb': 'GB', 'node.setting_unit_hours': 'hours', 'node.setting_unit_minutes': 'min', 'node.setting_on': 'On', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index 5e6f258..6d148d6 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -884,6 +884,8 @@ export default { 'node.setting_max_downloads': 'Téléchargements simultanés max', 'node.setting_max_uploads': 'Téléversements simultanés max', 'node.setting_transcode': 'Transcoder les vidéos incompatibles', + 'node.setting_max_upload': 'Taille maximale d\'un envoi', + 'node.setting_unit_gb': 'GB', 'node.setting_unit_hours': 'heures', 'node.setting_unit_minutes': 'min', 'node.setting_on': 'Activé', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index ba594d2..c619d61 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -883,6 +883,8 @@ export default { 'node.setting_max_downloads': 'Download simultanei massimi', 'node.setting_max_uploads': 'Caricamenti simultanei massimi', 'node.setting_transcode': 'Transcode incompatible video', + 'node.setting_max_upload': 'Caricamento singolo massimo', + 'node.setting_unit_gb': 'GB', 'node.setting_unit_hours': 'hours', 'node.setting_unit_minutes': 'min', 'node.setting_on': 'On', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index 9bbafb2..eac90dc 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -871,6 +871,8 @@ export default { 'node.setting_max_downloads': '同時ダウンロードの上限', 'node.setting_max_uploads': '同時アップロードの上限', 'node.setting_transcode': 'Transcode incompatible video', + 'node.setting_max_upload': '1ファイルあたりの上限', + 'node.setting_unit_gb': 'GB', 'node.setting_unit_hours': 'hours', 'node.setting_unit_minutes': 'min', 'node.setting_on': 'On', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index 8f28d0c..65bf4f5 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -885,6 +885,8 @@ export default { 'node.setting_max_downloads': 'Max. gelijktijdige downloads', 'node.setting_max_uploads': 'Max. gelijktijdige uploads', 'node.setting_transcode': 'Transcode incompatible video', + 'node.setting_max_upload': 'Grootste enkele upload', + 'node.setting_unit_gb': 'GB', 'node.setting_unit_hours': 'hours', 'node.setting_unit_minutes': 'min', 'node.setting_on': 'On', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index fadf622..264d442 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -903,6 +903,8 @@ export default { 'node.setting_max_downloads': 'Maks. równoczesnych pobierań', 'node.setting_max_uploads': 'Maks. równoczesnych wysyłek', 'node.setting_transcode': 'Transcode incompatible video', + 'node.setting_max_upload': 'Największa pojedyncza wysyłka', + 'node.setting_unit_gb': 'GB', 'node.setting_unit_hours': 'hours', 'node.setting_unit_minutes': 'min', 'node.setting_on': 'On', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index 39da2b2..0fbfb1e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -882,6 +882,8 @@ export default { 'node.setting_max_downloads': 'Máximo de downloads simultâneos', 'node.setting_max_uploads': 'Máximo de envios simultâneos', 'node.setting_transcode': 'Transcode incompatible video', + 'node.setting_max_upload': 'Maior envio individual', + 'node.setting_unit_gb': 'GB', 'node.setting_unit_hours': 'hours', 'node.setting_unit_minutes': 'min', 'node.setting_on': 'On', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index 8639592..ab75b11 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -858,6 +858,8 @@ export default { 'node.setting_max_downloads': '最大同时下载数', 'node.setting_max_uploads': '最大同时上传数', 'node.setting_transcode': 'Transcode incompatible video', + 'node.setting_max_upload': '单个上传的最大大小', + 'node.setting_unit_gb': 'GB', 'node.setting_unit_hours': 'hours', 'node.setting_unit_minutes': 'min', 'node.setting_on': 'On', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js index 0f75edc..2edcbab 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js @@ -1067,6 +1067,14 @@ export function NodePage({ groups, token, username }) { onInput=${e => setEditSettings(s => ({...s, max_concurrent_uploads: parseInt(e.target.value) || 1}))} /> +