// download.js — "Download (zip)" for the currently-viewed directory. // // Server mode: just point an at "?zip=1" — // zddc-server streams an ACL-filtered .zip of the subtree, so nothing // is held in the browser. // // FS-API (offline) mode: there's no server, so we walk the picked // folder ourselves, bundle every file with JSZip, and download the // blob. A two-pass walk (metadata first, then bytes) lets us warn // before loading a very large tree into memory. (function () { 'use strict'; var state = window.app.state; // Soft thresholds for the offline bundle: above either, confirm() // before loading everything into memory. var WARN_FILE_COUNT = 2000; var WARN_TOTAL_BYTES = 500 * 1024 * 1024; function events() { return window.app.modules.events; } function isHiddenName(name) { return name.length === 0 || name[0] === '.' || name[0] === '_'; } function fmtMB(bytes) { return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } // Trigger a browser download of a Blob (revokes the object URL after). function downloadBlob(filename, blob) { var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 0); } // Trigger a download from a same-origin server URL via Content-Disposition. function downloadUrl(filename, url) { var a = document.createElement('a'); a.href = url; a.download = filename; // hint; the server's Content-Disposition wins document.body.appendChild(a); a.click(); setTimeout(function () { a.remove(); }, 0); } // Recursively collect every (non-hidden) file under dirHandle into // `out` as { relPath, handle, size }, accumulating into `tally`. // relPrefix is the slash-terminated path within the picked root // ("" at the root). async function collectFiles(dirHandle, relPrefix, out, tally) { for await (var pair of dirHandle.entries()) { var name = pair[0]; var handle = pair[1]; if (isHiddenName(name)) continue; if (handle.kind === 'directory') { await collectFiles(handle, relPrefix + name + '/', out, tally); } else { var size = 0; try { var f = await handle.getFile(); size = f.size || 0; } catch (_e) { /* permission lost — count it as 0 */ } out.push({ relPath: relPrefix + name, handle: handle, size: size }); tally.count++; tally.bytes += size; } } } async function downloadFsSubtree(rootHandle) { var ev = events(); ev.statusInfo('Scanning ' + rootHandle.name + '…'); var files = []; var tally = { count: 0, bytes: 0 }; await collectFiles(rootHandle, '', files, tally); if (files.length === 0) { ev.statusInfo(rootHandle.name + ' is empty — nothing to download.'); return; } if (tally.count > WARN_FILE_COUNT || tally.bytes > WARN_TOTAL_BYTES) { var ok = window.confirm( 'This folder has ' + tally.count + ' files (~' + fmtMB(tally.bytes) + ').\n\n' + 'Building the zip loads them all into memory — it may be slow or crash the tab.\n\n' + 'Continue?'); if (!ok) { ev.statusClear(); return; } } var zip = new window.JSZip(); for (var i = 0; i < files.length; i++) { ev.statusInfo('Zipping ' + rootHandle.name + '… (' + (i + 1) + '/' + files.length + ')'); var f = await files[i].handle.getFile(); var buf = await f.arrayBuffer(); zip.file(rootHandle.name + '/' + files[i].relPath, buf); } ev.statusInfo('Generating ' + rootHandle.name + '.zip…'); var blob = await zip.generateAsync({ type: 'blob' }); downloadBlob(rootHandle.name + '.zip', blob); ev.statusInfo('Downloaded ' + rootHandle.name + '.zip (' + files.length + ' files)'); } function downloadServerSubtree() { var dir = (state.currentPath || '/').replace(/\/$/, ''); var name = (dir.split('/').filter(Boolean).pop()) || 'download'; events().statusInfo('Preparing ' + name + '.zip…'); downloadUrl(name + '.zip', dir + '/?zip=1'); // The browser owns the download from here; clear the hint shortly. setTimeout(function () { events().statusClear(); }, 2500); } var busy = false; async function downloadCurrentSubtree() { if (busy) return; var btn = document.getElementById('downloadZipBtn'); busy = true; if (btn) btn.disabled = true; try { if (state.source === 'server') { downloadServerSubtree(); } else if (state.source === 'fs' && state.rootHandle) { await downloadFsSubtree(state.rootHandle); } else { events().statusError('Nothing to download — open a directory first.'); } } catch (e) { events().statusError('Download failed: ' + (e && e.message ? e.message : e)); } finally { busy = false; if (btn) btn.disabled = false; } } window.app.modules.download = { downloadCurrentSubtree: downloadCurrentSubtree }; })();