diff --git a/config_example.json b/config_example.json index 5dd623b..15370e0 100644 --- a/config_example.json +++ b/config_example.json @@ -59,6 +59,7 @@ "abyss_enabled": true, "meme_creator": true, "enable_cleanup": false, + "enable_data_export": true, "cleanup_timeframe_days": 30, "web_url_upload": true, diff --git a/public/s/js/settings.js b/public/s/js/settings.js index 2371cd5..932c4da 100644 --- a/public/s/js/settings.js +++ b/public/s/js/settings.js @@ -1499,30 +1499,76 @@ exportStatusMsg.textContent = exportStatusText.dataset.generating || 'Generating ZIP file'; exportAnimatedDots.style.display = 'inline'; - const content = await new Promise((resolve, reject) => { - const chunks = []; - zip.generateInternalStream({ type: 'uint8array', compression: 'DEFLATE', compressionOptions: { level: 6 } }) - .on('data', (chunk) => chunks.push(chunk)) - .on('error', reject) - .on('end', () => { - try { - resolve(new Blob(chunks, { type: 'application/zip' })); - } catch (e) { - reject(e); - } - }) - .resume(); - }); - exportAnimatedDots.style.display = 'none'; - - const link = document.createElement('a'); - link.href = URL.createObjectURL(content); - link.download = `f0ckm_export_${new Date().toISOString().split('T')[0]}.zip`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - exportStatusMsg.textContent = exportStatusText.dataset.complete || 'Export complete!'; + const zipOptions = { + type: 'uint8array', + compression: 'STORE' // Media is already compressed, STORE saves massive CPU/RAM + }; + + // Try Direct-to-Disk saving if supported (Chrome/Edge/Opera) + // This bypasses RAM entirely for the final file construction. + if ('showSaveFilePicker' in window) { + try { + const handle = await window.showSaveFilePicker({ + suggestedName: `f0ckm_export_${new Date().toISOString().split('T')[0]}.zip`, + types: [{ + description: 'ZIP Archive', + accept: { 'application/zip': ['.zip'] }, + }], + }); + + const writable = await handle.createWritable(); + await new Promise((resolve, reject) => { + zip.generateInternalStream(zipOptions) + .on('data', async (chunk) => { + try { + await writable.write(chunk); + } catch (e) { + reject(e); + } + }) + .on('error', reject) + .on('end', resolve) + .resume(); + }); + await writable.close(); + + exportStatusMsg.textContent = exportStatusText.dataset.complete || 'Export complete!'; + } catch (e) { + if (e.name === 'AbortError') { + exportStatusMsg.textContent = 'Export cancelled.'; + } else { + throw e; + } + } + } else { + // Fallback: Original Blob method (uses RAM) + const content = await new Promise((resolve, reject) => { + const chunks = []; + zip.generateInternalStream(zipOptions) + .on('data', (chunk) => chunks.push(chunk)) + .on('error', reject) + .on('end', () => { + try { + resolve(new Blob(chunks, { type: 'application/zip' })); + } catch (e) { + reject(e); + } + }) + .resume(); + }); + + const link = document.createElement('a'); + link.href = URL.createObjectURL(content); + link.download = `f0ckm_export_${new Date().toISOString().split('T')[0]}.zip`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + exportStatusMsg.textContent = exportStatusText.dataset.complete || 'Export complete!'; + } + + exportAnimatedDots.style.display = 'none'; btnStartExport.disabled = false; } catch (err) { console.error('Export failed:', err); diff --git a/src/inc/routes/settings.mjs b/src/inc/routes/settings.mjs index 2e5a7ec..cd07f41 100644 --- a/src/inc/routes/settings.mjs +++ b/src/inc/routes/settings.mjs @@ -49,6 +49,7 @@ export default (router, tpl) => { email: user?.email || '', joined: user?.created_at || null, enable_swf: cfg.enable_swf, + enable_data_export: cfg.websrv.enable_data_export, session: (req.session && req.session.user) ? { ...req.session } : false, page_meta: { title: 'settings', @@ -59,6 +60,10 @@ export default (router, tpl) => { }); }); group.get('/export-data', auth, async (req, res) => { + if (!cfg.websrv.enable_data_export) { + res.status(403).reply({ body: 'Export disabled' }); + return; + } const uploads = await db` select id, dest, mime from items where username = ${req.session.user} and active = true diff --git a/views/settings.html b/views/settings.html index be8621b..f05c2a1 100644 --- a/views/settings.html +++ b/views/settings.html @@ -257,6 +257,7 @@ @endif + @if(enable_data_export)

{{ t('settings.export_data_title') || 'Export Data' }}

{{ t('settings.export_data_desc') || 'Download a copy of your data. This process happens entirely in your browser to protect your privacy and save server resources.' }}

@@ -295,6 +296,7 @@ {{ t('settings.start_export') || 'Generate Export (ZIP)' }}
+ @endif

{{ t('settings.account') }}