diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/files-app.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/files-app.js | 89 |
1 files changed, 7 insertions, 82 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/files-app.js b/packages/meshbay-hub/src/meshbay_hub/static/files-app.js index c09ef40..16ab9f6 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/files-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/files-app.js @@ -3,11 +3,11 @@ import { } from './vendor/htm-preact.js'; import { t } from './i18n.js'; import { Icon } from './icon.js'; -import { ZipStream, entriesUnder } from './zipstream.js'; +import { entriesUnder } from './zipstream.js'; import { transfers } from './transfers.js'; import { FILE_ICONS, formatSize, formatDate, canPreview, CHUNK_SIZE, - _openDownloadTarget, _saveBlob, pipelinedDownload, downloadEntry, + pipelinedDownload, downloadEntry, downloadDirectory as sharedDownloadDirectory, } from './file-utils.js'; // ── Files ──────────────────────────────────────────────────────────────────── @@ -91,88 +91,13 @@ function FilesPanel({ } }, [currentPath]); - /** - * 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. - * - * 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 - * offered but says what it costs first. - */ + // The implementation lives in file-utils.js (docs/photos.md §3) so + // photos-app.js's own "zip this album" button can call the same code + // rather than a second one. const downloadDirectory = useCallback(async (dir) => { const transport = transportRef.current; - if (!transport || !transport.connected) return; - - const files = entriesUnder(entries, dir); - if (!files.length) { - setError(t('group.zip_empty')); - return; - } - const totalBytes = files.reduce((n, f) => n + (f.entry.size || 0), 0); - const suggested = (dir.split('/').pop() || 'files') + '.zip'; - - // 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. - const target = await _openDownloadTarget(suggested, totalBytes, { - types: [{ description: 'ZIP archive', - accept: { 'application/zip': ['.zip'] } }], - }, 0); - if (target === false) return; - if (!target && !confirm(t('group.zip_no_stream', { - size: formatSize(totalBytes), name: suggested, - }))) { - return; - } - const gek = gekRef.current; - const zipOpenRef = { url: null }; - - transfers.start({ - kind: 'download', name: (target && target.name) || suggested, - total: totalBytes, transport, - open: target - ? (target.open || null) - : () => { if (zipOpenRef.url) window.open(zipOpenRef.url, '_blank'); }, - run: async ({ signal, onProgress }) => { - const writable = target ? target.writable : null; - const parts = writable ? null : []; - let written = 0; - try { - const zip = new ZipStream(async (bytes) => { - if (writable) await writable.write(bytes); - else parts.push(bytes.slice()); - }); - - for (const { entry, name } of files) { - await zip.begin(name, entry.size, - new Date((entry.added_at || 0) * 1000)); - // A zero-byte file has no chunk to ask for; the header and an empty - // descriptor are the whole entry. - const totalChunks = Math.ceil(entry.size / CHUNK_SIZE); - if (totalChunks > 0) await pipelinedDownload( - transport, gek, entry.id, totalChunks, - (bytes) => { written += bytes; onProgress(written, totalBytes); }, - // pipelinedDownload writes in order, which the archive needs. - { write: (plaintext) => zip.write(plaintext) }, signal); - await zip.end(); - } - await zip.finish(); - if (writable) await writable.close(); - else { - const blob = new Blob(parts, { type: 'application/zip' }); - _saveBlob(blob, suggested); - zipOpenRef.url = URL.createObjectURL(blob); - } - } catch (err) { - if (writable) await writable.abort().catch(() => {}); - throw err; - } - }, - }); + await sharedDownloadDirectory( + transfers, transport, gekRef.current, entries, dir, { setError }); }, [entries]); const deleteDirectory = useCallback(async (dir) => { |