make data export a bool config setting
This commit is contained in:
@@ -59,6 +59,7 @@
|
|||||||
"abyss_enabled": true,
|
"abyss_enabled": true,
|
||||||
"meme_creator": true,
|
"meme_creator": true,
|
||||||
"enable_cleanup": false,
|
"enable_cleanup": false,
|
||||||
|
"enable_data_export": true,
|
||||||
"cleanup_timeframe_days": 30,
|
"cleanup_timeframe_days": 30,
|
||||||
|
|
||||||
"web_url_upload": true,
|
"web_url_upload": true,
|
||||||
|
|||||||
@@ -1499,30 +1499,76 @@
|
|||||||
|
|
||||||
exportStatusMsg.textContent = exportStatusText.dataset.generating || 'Generating ZIP file';
|
exportStatusMsg.textContent = exportStatusText.dataset.generating || 'Generating ZIP file';
|
||||||
exportAnimatedDots.style.display = 'inline';
|
exportAnimatedDots.style.display = 'inline';
|
||||||
const content = await new Promise((resolve, reject) => {
|
|
||||||
const chunks = [];
|
const zipOptions = {
|
||||||
zip.generateInternalStream({ type: 'uint8array', compression: 'DEFLATE', compressionOptions: { level: 6 } })
|
type: 'uint8array',
|
||||||
.on('data', (chunk) => chunks.push(chunk))
|
compression: 'STORE' // Media is already compressed, STORE saves massive CPU/RAM
|
||||||
.on('error', reject)
|
};
|
||||||
.on('end', () => {
|
|
||||||
try {
|
// Try Direct-to-Disk saving if supported (Chrome/Edge/Opera)
|
||||||
resolve(new Blob(chunks, { type: 'application/zip' }));
|
// This bypasses RAM entirely for the final file construction.
|
||||||
} catch (e) {
|
if ('showSaveFilePicker' in window) {
|
||||||
reject(e);
|
try {
|
||||||
}
|
const handle = await window.showSaveFilePicker({
|
||||||
})
|
suggestedName: `f0ckm_export_${new Date().toISOString().split('T')[0]}.zip`,
|
||||||
.resume();
|
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';
|
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!';
|
|
||||||
btnStartExport.disabled = false;
|
btnStartExport.disabled = false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Export failed:', err);
|
console.error('Export failed:', err);
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export default (router, tpl) => {
|
|||||||
email: user?.email || '',
|
email: user?.email || '',
|
||||||
joined: user?.created_at || null,
|
joined: user?.created_at || null,
|
||||||
enable_swf: cfg.enable_swf,
|
enable_swf: cfg.enable_swf,
|
||||||
|
enable_data_export: cfg.websrv.enable_data_export,
|
||||||
session: (req.session && req.session.user) ? { ...req.session } : false,
|
session: (req.session && req.session.user) ? { ...req.session } : false,
|
||||||
page_meta: {
|
page_meta: {
|
||||||
title: 'settings',
|
title: 'settings',
|
||||||
@@ -59,6 +60,10 @@ export default (router, tpl) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
group.get('/export-data', auth, async (req, res) => {
|
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`
|
const uploads = await db`
|
||||||
select id, dest, mime from items
|
select id, dest, mime from items
|
||||||
where username = ${req.session.user} and active = true
|
where username = ${req.session.user} and active = true
|
||||||
|
|||||||
@@ -257,6 +257,7 @@
|
|||||||
@endif
|
@endif
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@if(enable_data_export)
|
||||||
<h2>{{ t('settings.export_data_title') || 'Export Data' }}</h2>
|
<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;">
|
<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>
|
<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)' }}
|
<i class="fa-solid fa-download" style="margin-right: 5px;"></i> {{ t('settings.start_export') || 'Generate Export (ZIP)' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<h2>{{ t('settings.account') }}</h2>
|
<h2>{{ t('settings.account') }}</h2>
|
||||||
<div class="account-settings-wrapper"
|
<div class="account-settings-wrapper"
|
||||||
|
|||||||
Reference in New Issue
Block a user