This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/src/js/sw/surfaces/admin/backup.js
Jeffrey Smith 786bc92768
All checks were successful
CI/CD / detect-changes (push) Successful in 3s
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Successful in 2m44s
CI/CD / test-sqlite (push) Successful in 2m54s
CI/CD / build-and-deploy (push) Successful in 1m17s
Feat v0.6.11 css dedup (#46)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-04-01 11:18:28 +00:00

163 lines
6.4 KiB
JavaScript

/**
* 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`<div class="settings-placeholder">Loading\u2026</div>`;
return html`
<div>
<div style="display:flex;gap:8px;margin-bottom:20px;flex-wrap:wrap;">
<button class="sw-btn sw-btn--primary sw-btn--md" onclick=${() => createBackup(false)} disabled=${creating || restoring}>
${creating ? 'Creating\u2026' : 'Download Backup'}
</button>
<button class="btn" onclick=${() => createBackup(true)} disabled=${creating || restoring}>
${creating ? 'Creating\u2026' : 'Save to Server'}
</button>
<button class="sw-btn sw-btn--danger sw-btn--md" onclick=${restoreBackup} disabled=${creating || restoring}>
${restoring ? 'Restoring\u2026' : 'Restore from File'}
</button>
</div>
<h3 style="margin:0 0 12px;">Server Backups</h3>
${backups.length === 0 ? html`
<p style="color:var(--text-secondary);">No server-side backups. Use "Save to Server" to create one.</p>
` : html`
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Size</th>
<th>Created</th>
<th style="width:120px;">Actions</th>
</tr>
</thead>
<tbody>
${backups.map(b => html`
<tr key=${b.name}>
<td><code>${b.name}</code></td>
<td>${formatBytes(b.size)}</td>
<td>${formatDate(b.created_at)}</td>
<td>
<button class="sw-btn sw-btn--secondary sw-btn--sm" onclick=${() => downloadBackup(b.name)}>Download</button>
<button class="sw-btn sw-btn--danger sw-btn--sm" onclick=${() => deleteBackup(b.name)} style="margin-left:4px;">Delete</button>
</td>
</tr>
`)}
</tbody>
</table>
`}
</div>
`;
}