aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-15 12:19:22 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-15 12:19:22 +0200
commit8cd7e467ebec987f66c4fe93a8d87dfbc57304d2 (patch)
tree0ebd406d893b49ebdf9290ea1a4ac3474d01b7bd /docs
parent0503682c0e2add135b88c2a1fadfe07455680a71 (diff)
downloadmeshbay-8cd7e467ebec987f66c4fe93a8d87dfbc57304d2.tar.gz
feat(files): download a folder as a zip, and remove an empty one
Two things a Files panel needs and did not have. **Removing a directory** is privileged, where creating one is not: it acts on a name other members are using, on the operator's disk. It is refused unless the directory is empty, and that rule is the safety property — whatever the browser sends, this cannot destroy content. The check runs twice, once before the challenge and once after the signature comes back, because a file can land during the round trip. A file also accepts its uploader's key; a directory has no uploader, so only the operator's key will do. **Downloading a folder** produces a zip built in the browser, written straight to disk as the chunks arrive. An archive of a group folder is routinely tens of gigabytes, so nothing is held: peak memory is one chunk plus a small record per file. The node is not involved at all — it serves the same encrypted chunks as any other download, holds no temporary files, and cannot be asked to compress anything. zipstream.js is store-only. Group content is video and images, already compressed, so deflate would spend CPU on every byte to save nothing, in the thread that is also decrypting. Sizes and CRCs go in a data descriptor after each file because a stream cannot seek back to patch a header, and zip64 kicks in per entry past 4 GiB and for the archive itself. Because none of that can be checked from the Python side of the house, test_zipstream.py runs the real module under Node and reads what it produces with zipfile — CRCs, UTF-8 names, zip64 records and all. The archives also pass `unzip -t`. Firefox and Safari have no File System Access API, so there is nowhere to stream to: the fallback builds the archive in memory and says so, with the size, before starting rather than after failing. One mistake worth recording: the first version of deleteDirectory passed the node's own answer as the value to check the challenge against, which turns the comparison into a tautology. It checks the path we asked for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'docs')
-rw-r--r--docs/USERGUIDE.md29
-rw-r--r--docs/meshbay-draft-v5.md19
2 files changed, 48 insertions, 0 deletions
diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md
index e2193f0..7f69712 100644
--- a/docs/USERGUIDE.md
+++ b/docs/USERGUIDE.md
@@ -529,6 +529,35 @@ look at what was sent, move it, or empty it without hunting through the tree.
- Uploads are attributed to the identity the node pinned for that member, and that is what
decides who may delete the file later — not anything the hub says.
+### Downloading a folder as a zip
+
+Any member can take a whole folder: **⋮ → Download as zip** on the folder's row.
+The archive is built in the browser as the files arrive and written straight to
+disk, so a 40 GB folder costs 40 GB of disk and a few megabytes of memory.
+
+- Nothing is compressed. Group content is video, images and archives — already
+ compressed — so deflating would spend CPU on every byte to save nothing, in the
+ same thread that is decrypting.
+- The archive opens as the folder you asked for: a zip of `Holidays/2026` unpacks
+ as `2026/…`, not as a chain of empty parents.
+- Files over 4 GiB, and archives over 4 GiB, use zip64. Anything current reads
+ them; a tool from before 2003 may not.
+- **Firefox and Safari cannot write a download straight to disk** (no File System
+ Access API). There, the archive has to be assembled in memory first, and the
+ browser says so, with the size, before starting. Use Chrome or Edge for a large
+ one.
+
+### Deleting a directory
+
+**⋮ → Delete folder**, for the node operator, from a paired browser (§3). The
+directory must be **empty** — nothing here is recursive, and a folder with
+anything in it is refused before a signature is even asked for. Delete the files
+first, where you can see what you are losing.
+
+Like every privileged action on a node, it is signed with the key the node pinned
+for that browser and refused otherwise: hub membership, or an admin role on the
+hub, grants nothing here.
+
### Creating a directory
Any active member can create a directory from the Files panel (**New folder**). It is
diff --git a/docs/meshbay-draft-v5.md b/docs/meshbay-draft-v5.md
index a8b7e4b..b03328b 100644
--- a/docs/meshbay-draft-v5.md
+++ b/docs/meshbay-draft-v5.md
@@ -250,6 +250,25 @@ version accepted a client-chosen path and overwrote whatever was there, which de
deletion authorization as well: overwriting a file made the attacker its recorded
uploader, and therefore the party allowed to delete it (C5a).
+### 5.2b Removing a directory
+
+Creating a directory is not privileged (§5.2); removing one is, and it is refused
+unless the directory is empty. The emptiness rule is the safety property: whatever
+the caller intended, and whatever the browser sent, this operation cannot destroy
+content. It is checked twice — once before the challenge is issued, and again
+after the signature comes back, because a file can land during the round trip to
+the operator's browser.
+
+Authorized by `_verify_admin_sig` alone. A file also accepts its uploader's key,
+since the node recorded who put it there; a directory has no uploader, so there is
+no second key to accept.
+
+Archives are not the node's business: a member downloading a folder as a zip
+fetches the same encrypted chunks as any other download and assembles the archive
+in the browser. The node serves no bundles, holds no temporary files, and cannot
+be asked to compress anything — one fewer place where a request turns into work
+on someone else's disk.
+
### 5.3 Operator interface
Two personas, and they need different tools: