feat(backup): download the full backup as ZIP

GET /export?format=zip streams the JSON backup compressed (plain JSON stays
the API default). The MindSift import recognises the backup ZIP next to the
plain JSON and the Markdown archive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-29 06:43:50 +01:00
1 parent 1601a6a2d5
commit 1df32ece92
7 files changed
+122 -25

No files matched your search

@@ -375,12 +375,16 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
window.setTimeout(() => URL.revokeObjectURL(url), 10000);
};
const [exportingBackup, setExportingBackup] = useState(false);
const handleExportData = async () => {
const data = await api.exportAll();
const blob = new Blob([JSON.stringify(data, null, 2)], {
type: 'application/json;charset=utf-8',
});
saveBlob(blob, `mindsift-backup-${new Date().toISOString().slice(0, 10)}.json`);
setExportingBackup(true);
try {
saveBlob(await api.exportBackupZip(), `mindsift-backup-${new Date().toISOString().slice(0, 10)}.zip`);
} catch (err: any) {
setVaultError(err.message || 'Backup fehlgeschlagen');
} finally {
setExportingBackup(false);
}
};
const [exportingVault, setExportingVault] = useState(false);
@@ -1238,18 +1242,19 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
{/* Full Data Backup */}
<div className="pt-4 border-t border-border flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<h3 className="font-bold text-main">Vollständiger Daten-Export (JSON Backup)</h3>
<h3 className="font-bold text-main">Vollständiges Backup (.zip)</h3>
<p className="text-muted text-[11px]">
Lädt alle deine Gedanken, Aufgaben, Markdown-Dokumente und Listen als portable
JSON-Datei herunter.
Alle deine Gedanken, Aufgaben, Notizen und Listen als JSON, gepackt in eine ZIP-Datei. Über
„MindSift-Daten importieren“ lässt es sich vollständig zurückspielen.
</p>
</div>
<button
onClick={handleExportData}
className="btn-action h-9 px-4 text-xs flex items-center gap-2 flex-shrink-0"
disabled={exportingBackup}
className="btn-action h-11 sm:h-9 px-4 text-xs flex items-center justify-center gap-2 flex-shrink-0 disabled:opacity-50"
>
<Download className="w-3.5 h-3.5 text-accent" />
<span>Backup herunterladen</span>
<span>{exportingBackup ? 'Wird erstellt…' : 'Backup herunterladen'}</span>
</button>
</div>
+14 -7
View File
@@ -135,6 +135,14 @@ export const setCurrentSharingMode = (mode: SharingMode) => {
};
export const isSharingDisabled = () => currentSharingMode === 'private';
// Authenticated binary download (archives); no JSON parsing and no request timeout
async function downloadBlob(path: string): Promise<Blob> {
const token = getToken();
const res = await fetch(path, { headers: token ? { Authorization: `Bearer ${token}` } : {}, cache: 'no-store' });
if (!res.ok) throw new Error(`Download fehlgeschlagen (HTTP ${res.status})`);
return res.blob();
}
export const api = {
// --- AUTH, FIRST-BOOT SETUP & SETTINGS ---
async getAuthStatus(): Promise<AuthStatus> {
@@ -1093,15 +1101,14 @@ export const api = {
return data as ImportSummary;
},
// Full backup (all data as JSON) packed into a ZIP; restorable via importData('mindsift', …)
async exportBackupZip(): Promise<Blob> {
return downloadBlob('/api/v1/export?format=zip');
},
// Obsidian-compatible ZIP of the workspace; binary and possibly large, so no JSON helper and no timeout
async exportMarkdownVault(): Promise<Blob> {
const token = getToken();
const res = await fetch('/api/v1/export/markdown', {
headers: token ? { Authorization: `Bearer ${token}` } : {},
cache: 'no-store',
});
if (!res.ok) throw new Error(`Export fehlgeschlagen (HTTP ${res.status})`);
return res.blob();
return downloadBlob('/api/v1/export/markdown');
},
};