streaming export with log
All checks were successful
Deploy Nyla Frontend to Production / deploy (push) Successful in 46s

This commit is contained in:
ValueOn AG 2026-05-27 23:05:00 +02:00
parent f27bfd2221
commit 57319507bb

View file

@ -857,75 +857,63 @@ const MigrationTab: React.FC = () => {
if (m) filename = m[1];
}
const handle = await (window as any).showSaveFilePicker({
suggestedName: filename,
types: [{ description: 'JSON', accept: { 'application/json': ['.json'] } }],
});
const writable = await handle.createWritable();
const reader = response.body.getReader();
const decoder = new TextDecoder();
let totalBytes = 0;
let lastLogBytes = 0;
// Try File System Access API → streams directly to disk, no memory limit
if ('showSaveFilePicker' in window) {
try {
const handle = await (window as any).showSaveFilePicker({
suggestedName: filename,
types: [{ description: 'JSON', accept: { 'application/json': ['.json'] } }],
});
const writable = await handle.createWritable();
_addExportLog(t('Streame Daten direkt auf Disk...'));
for (;;) {
const { done, value } = await reader.read();
if (done) break;
await writable.write(value);
totalBytes += value.length;
if (totalBytes - lastLogBytes >= 2 * 1024 * 1024) {
_addExportLog(t('{size} geschrieben...', { size: _formatBytes(totalBytes) }));
lastLogBytes = totalBytes;
}
}
await writable.close();
_addExportLog(t('Export abgeschlossen: {size}', { size: _formatBytes(totalBytes) }), 'success');
toast.showSuccess(t('Export gespeichert'));
setExporting(false);
return;
} catch (fsErr: any) {
if (fsErr.name === 'AbortError') {
_addExportLog(t('Export abgebrochen'), 'error');
setExporting(false);
return;
}
}
}
// Fallback: accumulate binary chunks → Blob (no JS string limit)
_addExportLog(t('Lade Daten herunter...'));
const chunks: Uint8Array[] = [];
let currentDb = '';
let dbCount = 0;
const totalDbs = dbList.length;
let tailBuf = '';
for (;;) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
await writable.write(value);
totalBytes += value.length;
if (totalBytes - lastLogBytes >= 5 * 1024 * 1024) {
_addExportLog(t('{size} heruntergeladen...', { size: _formatBytes(totalBytes) }));
const text = decoder.decode(value, { stream: true });
tailBuf += text;
const match = tailBuf.match(/"(poweron_[a-z_]+)":\{"tables"/);
if (match && match[1] !== currentDb) {
currentDb = match[1];
dbCount++;
tailBuf = '';
const mb = (totalBytes / (1024 * 1024)).toFixed(1);
_addExportLog(t('{index}/{total}: {db} — {mb} MB', {
index: dbCount, total: totalDbs, db: currentDb, mb,
}));
lastLogBytes = totalBytes;
} else if (totalBytes - lastLogBytes >= 2 * 1024 * 1024) {
const mb = (totalBytes / (1024 * 1024)).toFixed(1);
setExportLog(prev => {
const updated = [...prev];
updated[updated.length - 1] = {
ts: new Date().toLocaleTimeString(),
message: t('{index}/{total}: {db} — {mb} MB', {
index: dbCount, total: totalDbs, db: currentDb || '...', mb,
}),
status: 'info',
};
return updated;
});
lastLogBytes = totalBytes;
}
if (tailBuf.length > 500) tailBuf = tailBuf.slice(-200);
}
_addExportLog(t('Erstelle Datei ({size})...', { size: _formatBytes(totalBytes) }));
const blob = new Blob(chunks as BlobPart[], { type: 'application/json' });
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
_addExportLog(t('Export abgeschlossen: {size}', { size: _formatBytes(totalBytes) }), 'success');
toast.showSuccess(t('Export heruntergeladen'));
await writable.close();
const mb = (totalBytes / (1024 * 1024)).toFixed(1);
_addExportLog(t('Export abgeschlossen: {count} Datenbanken, {mb} MB', { count: dbCount, mb }), 'success');
toast.showSuccess(t('Export gespeichert'));
} catch (err: any) {
_addExportLog(t('Fehler: {error}', { error: String(err) }), 'error');
toast.showError(t('Export fehlgeschlagen'));