summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-08 03:04:49 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-08 03:04:49 +0200
commit11c039ed3f0dc2ecc5eb30512b3dafa647fc4520 (patch)
tree72efdf58bdbc1cf3582d4f570f0cd421e4cf2a1f /packages/meshbay-hub/tests
parent2ac1f1f704d44e2b20f9598044f7e266ffae4d36 (diff)
downloadmeshbay-11c039ed3f0dc2ecc5eb30512b3dafa647fc4520.tar.gz
feat(hub): cap a directory zip at 512 MB
An arbitrary ceiling, not a technical one: the zip writer streams and holds one chunk plus a record per file, so it would happily produce a hundred gigabytes. Past half a gigabyte the honest answer is a subfolder at a time, or the files individually. Enforced in file-utils.js's downloadDirectory, which is the one implementation behind every zip button — Files' single folder, Files' multi-folder selection, and the Photos album button (docs/photos.md §3). - Per directory, not per selection: Files zips a whole multi-directory selection in one click, so an oversized folder is refused and its siblings still download. - Before _openDownloadTarget, so no save dialog opens for an archive that is never going to be written. - The bound is strict, so a folder of exactly 512 MB still goes through. - Counted in the 1024-based units formatSize already prints, so the number in the refusal is the number in the constant. group.zip_too_large in all ten catalogues. test_zip_size_limit.py runs the module under Node and pins the refusal, the inclusive bound, and that nothing is asked or started when a folder is over. The user guide's "a 40 GB folder costs 40 GB of disk" is no longer true and now documents the cap instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8EDjk6pkYZrCbo63m2x87
Diffstat (limited to 'packages/meshbay-hub/tests')
-rw-r--r--packages/meshbay-hub/tests/test_zip_size_limit.py107
1 files changed, 107 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_zip_size_limit.py b/packages/meshbay-hub/tests/test_zip_size_limit.py
new file mode 100644
index 0000000..203c10c
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_zip_size_limit.py
@@ -0,0 +1,107 @@
+"""
+The arbitrary ceiling on a directory zip.
+
+`downloadDirectory` is the one implementation behind every "download this
+folder as a zip" button — Files' single folder, Files' multi-folder selection,
+and the Photos album button (docs/photos.md §3) — so the limit is checked
+once, there, and holds for all of them.
+
+Two things are worth pinning. That an oversized folder is refused *before*
+`_openDownloadTarget`, because a save dialog for an archive that will never be
+written is worse than no dialog at all. And that a folder at exactly the limit
+still goes through, since an off-by-one here silently costs a whole megabyte
+of allowance and nobody would ever notice.
+"""
+
+import json
+import shutil
+import subprocess
+from pathlib import Path
+
+import pytest
+
+STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
+FILE_UTILS = STATIC / "file-utils.js"
+
+pytestmark = pytest.mark.skipif(
+ shutil.which("node") is None or not FILE_UTILS.exists(),
+ reason="node or the SPA sources are not available")
+
+MIB = 1024 * 1024
+
+
+def _run(total_bytes, tmp_path):
+ """
+ Call downloadDirectory over one folder holding `total_bytes`, and report
+ what it did: the errors it set, how many times it put a question to the
+ person, and how many transfers it started.
+ """
+ sandbox = tmp_path / "static"
+ sandbox.mkdir()
+ for src in STATIC.glob("*.js"):
+ (sandbox / src.name).write_text(src.read_text(encoding="utf-8"),
+ encoding="utf-8")
+ (tmp_path / "package.json").write_text('{"type":"module"}')
+
+ script = tmp_path / "case.mjs"
+ script.write_text(f"""
+const store = new Map();
+globalThis.localStorage = {{
+ getItem: k => (store.has(k) ? store.get(k) : null),
+ setItem: (k, v) => store.set(k, String(v)),
+ removeItem: k => store.delete(k),
+}};
+// Node 22 defines `navigator` itself, so it is left alone; `window` is what
+// platform.js reaches for to decide it is not running in the desktop app.
+globalThis.window = globalThis;
+const out = {{ errors: [], started: 0, asked: 0 }};
+// Reached only once the size check has passed: with no File System Access API
+// under Node, downloadDirectory falls through to its build-in-memory path and
+// asks first. Answering yes is what lets the at-the-limit case get as far as
+// starting a transfer, and `asked` is how the refusal proves it never did.
+globalThis.confirm = () => {{ out.asked += 1; return true; }};
+
+const M = await import('{(sandbox / "file-utils.js").as_posix()}');
+
+const transfers = {{ start: () => {{ out.started += 1; }} }};
+const transport = {{ connected: true }};
+// One file, in the folder itself — entriesUnder keys on `path`.
+const entries = [{{ id: 'f1', name: 'big.bin', path: 'album',
+ size: {total_bytes}, added_at: 0 }}];
+
+await M.downloadDirectory(transfers, transport, null, entries, 'album', {{
+ setError: (m) => out.errors.push(m),
+}});
+
+out.limit = M.ZIP_MAX_BYTES;
+console.log(JSON.stringify(out));
+""", encoding="utf-8")
+
+ proc = subprocess.run(["node", str(script)], capture_output=True, text=True)
+ assert proc.returncode == 0, proc.stderr
+ return json.loads(proc.stdout)
+
+
+def test_the_limit_is_512_mib(tmp_path):
+ """The number the refusal quotes is the number the module enforces."""
+ assert _run(0, tmp_path)["limit"] == 512 * MIB
+
+
+def test_an_oversized_folder_is_refused_before_anything_opens(tmp_path):
+ """
+ One byte over. Nothing is started, and the person is told why — a silent
+ return would read as a dead button.
+ """
+ result = _run(512 * MIB + 1, tmp_path)
+ assert result["started"] == 0, "no transfer may begin"
+ assert result["asked"] == 0, "nor may a save dialog have been put up first"
+ assert result["errors"] == ["group.zip_too_large"], (
+ "the refusal must name its own key; t() falls back to the key with no "
+ "catalogue loaded, and test_locales.py holds the ten translations of it")
+
+
+def test_a_folder_exactly_at_the_limit_still_downloads(tmp_path):
+ """The bound is inclusive: `> ZIP_MAX_BYTES`, not `>=`."""
+ result = _run(512 * MIB, tmp_path)
+ assert result["errors"] == []
+ assert result["started"] == 1