diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-08 03:04:49 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-08 03:04:49 +0200 |
| commit | 11c039ed3f0dc2ecc5eb30512b3dafa647fc4520 (patch) | |
| tree | 72efdf58bdbc1cf3582d4f570f0cd421e4cf2a1f /packages/meshbay-hub/src/meshbay_hub/static/file-utils.js | |
| parent | 2ac1f1f704d44e2b20f9598044f7e266ffae4d36 (diff) | |
| download | meshbay-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/src/meshbay_hub/static/file-utils.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/file-utils.js | 33 |
1 files changed, 28 insertions, 5 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js index 09ee6c9..241d761 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js @@ -15,6 +15,14 @@ function formatSize(bytes) { return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; } +// An arbitrary ceiling on one directory zip. Not a technical limit — the +// writer streams and holds one chunk plus a record per file, so it would +// happily produce a hundred gigabytes — but a deliberate one: past this +// size the honest answer is a subfolder at a time, or the files +// individually. Counted in the same 1024-based units formatSize prints, so +// the number in the refusal is the number in this constant. +const ZIP_MAX_BYTES = 512 * 1024 * 1024; + function formatDate(ts) { return new Date(ts * 1000).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', @@ -240,10 +248,10 @@ async function downloadEntry(transfers, transport, gek, entry) { /** * Download a directory as a zip, written straight to disk. * - * An archive of a group directory is routinely tens of gigabytes, so it is - * never held anywhere: each file is fetched chunk by chunk, decrypted, and - * handed to the zip writer, which hands it to the file the browser opened. - * Peak memory is one chunk plus one small record per file. + * Nothing is held anywhere: each file is fetched chunk by chunk, decrypted, + * and handed to the zip writer, which hands it to the file the browser + * opened. Peak memory is one chunk plus one small record per file — which is + * why ZIP_MAX_BYTES below is a policy, not a constraint this code has. * * Without the File System Access API there is nowhere to stream to, and the * only alternative is to build the whole thing in memory — so that path is @@ -266,6 +274,21 @@ async function downloadDirectory(transfers, transport, gek, entries, dir, { setE const totalBytes = files.reduce((n, f) => n + (f.entry.size || 0), 0); const suggested = (dir.split('/').pop() || 'files') + '.zip'; + // Checked here rather than by disabling the button: Files zips a whole + // multi-directory selection in one click (`for (const d of selectedDirs)`), + // so the answer is per directory and has to be given where each one is + // actually started — an oversized folder is refused and its siblings still + // download. Before _openDownloadTarget, so nothing opens a save dialog for + // an archive that is not going to be written. + if (totalBytes > ZIP_MAX_BYTES) { + setError(t('group.zip_too_large', { + name: dir.split('/').pop() || dir, + size: formatSize(totalBytes), + limit: formatSize(ZIP_MAX_BYTES), + })); + return; + } + // totalBytes decides how this is delivered, but it is not the archive's // size — headers and the central directory come on top — so it is not // announced as a Content-Length that the download would then miss. @@ -327,7 +350,7 @@ async function downloadDirectory(transfers, transport, gek, entries, dir, { setE export { FILE_ICONS, - formatSize, formatDate, PREVIEWABLE_TEXT, canPreview, CHUNK_SIZE, + formatSize, formatDate, PREVIEWABLE_TEXT, canPreview, CHUNK_SIZE, ZIP_MAX_BYTES, _openDownloadTarget, _saveBlob, pipelinedDownload, downloadEntry, downloadDirectory, }; |