make data export a bool config setting

This commit is contained in:
2026-05-13 07:24:16 +02:00
parent 18661e5d2c
commit 5d2dd3aca8
4 changed files with 77 additions and 23 deletions

View File

@@ -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,

View File

@@ -1499,9 +1499,53 @@
exportStatusMsg.textContent = exportStatusText.dataset.generating || 'Generating ZIP file';
exportAnimatedDots.style.display = 'inline';
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({ type: 'uint8array', compression: 'DEFLATE', compressionOptions: { level: 6 } })
zip.generateInternalStream(zipOptions)
.on('data', (chunk) => chunks.push(chunk))
.on('error', reject)
.on('end', () => {
@@ -1513,7 +1557,6 @@
})
.resume();
});
exportAnimatedDots.style.display = 'none';
const link = document.createElement('a');
link.href = URL.createObjectURL(content);
@@ -1523,6 +1566,9 @@
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);

View File

@@ -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

View File

@@ -257,6 +257,7 @@
@endif
</div>
@if(enable_data_export)
<h2>{{ t('settings.export_data_title') || 'Export Data' }}</h2>
<div class="export-settings-wrapper" style="background: rgba(0,0,0,0.1); padding: 20px; border-radius: 4px; border: 1px solid var(--nav-border-color); margin-bottom: 30px;">
<p>{{ 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.' }}</p>
@@ -295,6 +296,7 @@
<i class="fa-solid fa-download" style="margin-right: 5px;"></i> {{ t('settings.start_export') || 'Generate Export (ZIP)' }}
</button>
</div>
@endif
<h2>{{ t('settings.account') }}</h2>
<div class="account-settings-wrapper"