/** * Admin > System > Backup * * Create, download, and restore .swb backup archives. */ const { html } = window; const { useState, useEffect, useCallback } = hooks; export default function BackupSection() { const [backups, setBackups] = useState([]); const [loading, setLoading] = useState(true); const [creating, setCreating] = useState(false); const [restoring, setRestoring] = useState(false); const load = useCallback(async () => { try { const resp = await sw.api.admin.backup.list(); setBackups(Array.isArray(resp) ? resp : resp.data || []); } catch (e) { sw.toast(e.message, 'error'); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, []); async function createBackup(store) { setCreating(true); try { if (store) { await sw.api.admin.backup.create({ store: 'true' }); sw.toast('Backup created', 'success'); load(); } else { // Stream download const url = '/api/v1/admin/backup'; const token = sw.auth.token(); const resp = await fetch(url, { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, }); if (!resp.ok) throw new Error('Backup failed'); const blob = await resp.blob(); const disposition = resp.headers.get('Content-Disposition') || ''; const match = disposition.match(/filename="?([^"]+)"?/); const filename = match ? match[1] : 'backup.swb'; const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; a.click(); URL.revokeObjectURL(a.href); sw.toast('Backup downloaded', 'success'); } } catch (e) { sw.toast(e.message, 'error'); } finally { setCreating(false); } } async function downloadBackup(name) { const token = sw.auth.token(); const resp = await fetch(sw.api.admin.backup.download(name), { headers: { 'Authorization': 'Bearer ' + token }, }); if (!resp.ok) { sw.toast('Download failed', 'error'); return; } const blob = await resp.blob(); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name; a.click(); URL.revokeObjectURL(a.href); } async function deleteBackup(name) { const ok = await sw.confirm(`Delete backup "${name}"?`); if (!ok) return; try { await sw.api.admin.backup.del(name); sw.toast('Backup deleted', 'success'); load(); } catch (e) { sw.toast(e.message, 'error'); } } async function restoreBackup() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.swb'; input.onchange = async () => { const file = input.files[0]; if (!file) return; const ok = await sw.confirm( `Restore from "${file.name}"? This will replace ALL data in the database. This action cannot be undone.` ); if (!ok) return; setRestoring(true); try { const resp = await sw.api.admin.backup.restore(file); const d = resp.data || resp; sw.toast(`Restored ${d.rows_restored || 0} rows`, 'success'); load(); } catch (e) { sw.toast('Restore failed: ' + e.message, 'error'); } finally { setRestoring(false); } }; input.click(); } function formatBytes(b) { if (!b) return '0 B'; if (b < 1024) return b + ' B'; if (b < 1048576) return (b / 1024).toFixed(1) + ' KB'; if (b < 1073741824) return (b / 1048576).toFixed(1) + ' MB'; return (b / 1073741824).toFixed(2) + ' GB'; } function formatDate(d) { if (!d) return '\u2014'; return new Date(d).toLocaleString(); } if (loading) return html`
Loading\u2026
`; return html`

Server Backups

${backups.length === 0 ? html`

No server-side backups. Use "Save to Server" to create one.

` : html` ${backups.map(b => html` `)}
Name Size Created Actions
${b.name} ${formatBytes(b.size)} ${formatDate(b.created_at)}
`}
`; }