streaming export with log
All checks were successful
Deploy Nyla Frontend to Production / deploy (push) Successful in 46s
All checks were successful
Deploy Nyla Frontend to Production / deploy (push) Successful in 46s
This commit is contained in:
parent
f27bfd2221
commit
57319507bb
1 changed files with 45 additions and 57 deletions
|
|
@ -857,75 +857,63 @@ const MigrationTab: React.FC = () => {
|
||||||
if (m) filename = m[1];
|
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 reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
let totalBytes = 0;
|
let totalBytes = 0;
|
||||||
let lastLogBytes = 0;
|
let lastLogBytes = 0;
|
||||||
|
let currentDb = '';
|
||||||
// Try File System Access API → streams directly to disk, no memory limit
|
let dbCount = 0;
|
||||||
if ('showSaveFilePicker' in window) {
|
const totalDbs = dbList.length;
|
||||||
try {
|
let tailBuf = '';
|
||||||
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[] = [];
|
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
if (done) break;
|
if (done) break;
|
||||||
chunks.push(value);
|
await writable.write(value);
|
||||||
totalBytes += value.length;
|
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;
|
lastLogBytes = totalBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tailBuf.length > 500) tailBuf = tailBuf.slice(-200);
|
||||||
}
|
}
|
||||||
|
|
||||||
_addExportLog(t('Erstelle Datei ({size})...', { size: _formatBytes(totalBytes) }));
|
await writable.close();
|
||||||
|
const mb = (totalBytes / (1024 * 1024)).toFixed(1);
|
||||||
const blob = new Blob(chunks as BlobPart[], { type: 'application/json' });
|
_addExportLog(t('Export abgeschlossen: {count} Datenbanken, {mb} MB', { count: dbCount, mb }), 'success');
|
||||||
const blobUrl = URL.createObjectURL(blob);
|
toast.showSuccess(t('Export gespeichert'));
|
||||||
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'));
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
_addExportLog(t('Fehler: {error}', { error: String(err) }), 'error');
|
_addExportLog(t('Fehler: {error}', { error: String(err) }), 'error');
|
||||||
toast.showError(t('Export fehlgeschlagen'));
|
toast.showError(t('Export fehlgeschlagen'));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue