mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-30 12:33:01 +00:00
fix(backup): address PR review findings
- backup_manager: read plugin state from "states" key (not "plugins") to match the actual plugin_state.json format written by state_manager - backup_manager: stream ZIP directly to a temp file instead of building it in an io.BytesIO buffer to avoid OOM on Raspberry Pi - backup_manager: tighten plugin-uploads path validation in validate_backup and restore_backup to require "/uploads/" in the path, rejecting any non-uploads files smuggled under assets/plugins/ - api_v3: enforce 200 MB upload limit by streaming in chunks rather than relying on validate_file_upload (which only checks the filename) - api_v3: replace bool() with _coerce_to_bool() for RestoreOptions fields so string "false" is not treated as truthy - api_v3: capture and log _save_config_atomic return value instead of discarding it; log rather than silence font-cache and config-reload errors - backup_restore.html: track inspectedFile so runRestore always applies to the file the user inspected, not a subsequently selected file; clear on input change or clearRestore() - backup_restore.html: throw on non-success restore payload so errors are surfaced via the error notification path instead of yellow "warnings" - test: update fixture to use correct "states" key structure; import SCHEMA_VERSION constant instead of hardcoding 1; rename unused err -> _err Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -117,6 +117,8 @@
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
let inspectedFile = null;
|
||||
|
||||
function notify(message, kind) {
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification(message, kind || 'info');
|
||||
@@ -245,12 +247,14 @@
|
||||
notify('Choose a backup file first', 'error');
|
||||
return;
|
||||
}
|
||||
const file = input.files[0];
|
||||
const fd = new FormData();
|
||||
fd.append('backup_file', input.files[0]);
|
||||
fd.append('backup_file', file);
|
||||
try {
|
||||
const res = await fetch('/api/v3/backup/validate', { method: 'POST', body: fd });
|
||||
const payload = await res.json();
|
||||
if (payload.status !== 'success') throw new Error(payload.message || 'Validation failed');
|
||||
inspectedFile = file;
|
||||
renderRestorePreview(payload.data);
|
||||
} catch (err) {
|
||||
notify('Invalid backup: ' + err.message, 'error');
|
||||
@@ -273,15 +277,15 @@
|
||||
}
|
||||
|
||||
function clearRestore() {
|
||||
inspectedFile = null;
|
||||
document.getElementById('restore-preview').classList.add('hidden');
|
||||
document.getElementById('restore-result').classList.add('hidden');
|
||||
document.getElementById('restore-file-input').value = '';
|
||||
}
|
||||
|
||||
async function runRestore() {
|
||||
const input = document.getElementById('restore-file-input');
|
||||
if (!input.files || !input.files[0]) {
|
||||
notify('Choose a backup file first', 'error');
|
||||
if (!inspectedFile) {
|
||||
notify('Inspect the file before restoring', 'error');
|
||||
return;
|
||||
}
|
||||
if (!confirm('Restore from this backup? Current configuration will be overwritten.')) return;
|
||||
@@ -295,7 +299,7 @@
|
||||
reinstall_plugins: document.getElementById('opt-reinstall').checked,
|
||||
};
|
||||
const fd = new FormData();
|
||||
fd.append('backup_file', input.files[0]);
|
||||
fd.append('backup_file', inspectedFile);
|
||||
fd.append('options', JSON.stringify(options));
|
||||
|
||||
const btn = document.getElementById('run-restore-btn');
|
||||
@@ -304,13 +308,16 @@
|
||||
try {
|
||||
const res = await fetch('/api/v3/backup/restore', { method: 'POST', body: fd });
|
||||
const payload = await res.json();
|
||||
if (payload.status !== 'success') {
|
||||
const msgs = (payload.data?.errors || []).join('; ');
|
||||
throw new Error(payload.message || msgs || 'Restore had errors');
|
||||
}
|
||||
const data = payload.data || {};
|
||||
const result = document.getElementById('restore-result');
|
||||
const ok = payload.status === 'success';
|
||||
result.className = (ok ? 'bg-green-50 border-green-200 text-green-800' : 'bg-yellow-50 border-yellow-200 text-yellow-800') + ' border rounded-md p-4';
|
||||
result.className = 'bg-green-50 border-green-200 text-green-800 border rounded-md p-4';
|
||||
result.classList.remove('hidden');
|
||||
result.innerHTML = `
|
||||
<h3 class="font-medium mb-2">${ok ? 'Restore complete' : 'Restore finished with warnings'}</h3>
|
||||
<h3 class="font-medium mb-2">Restore complete</h3>
|
||||
<div><strong>Restored:</strong> ${(data.restored || []).map(escapeHtml).join(', ') || 'none'}</div>
|
||||
<div><strong>Skipped:</strong> ${(data.skipped || []).map(escapeHtml).join(', ') || 'none'}</div>
|
||||
<div><strong>Plugins installed:</strong> ${(data.plugins_installed || []).map(escapeHtml).join(', ') || 'none'}</div>
|
||||
@@ -318,7 +325,7 @@
|
||||
<div><strong>Errors:</strong> ${(data.errors || []).map(escapeHtml).join('; ') || 'none'}</div>
|
||||
<p class="mt-2">Restart the display service to apply all changes.</p>
|
||||
`;
|
||||
notify(ok ? 'Restore complete' : 'Restore finished with warnings', ok ? 'success' : 'info');
|
||||
notify('Restore complete', 'success');
|
||||
} catch (err) {
|
||||
notify('Restore failed: ' + err.message, 'error');
|
||||
} finally {
|
||||
@@ -334,6 +341,13 @@
|
||||
window.clearRestore = clearRestore;
|
||||
window.runRestore = runRestore;
|
||||
|
||||
// Clear inspection state whenever the user picks a new file.
|
||||
document.getElementById('restore-file-input').addEventListener('change', function () {
|
||||
inspectedFile = null;
|
||||
document.getElementById('restore-preview').classList.add('hidden');
|
||||
document.getElementById('restore-result').classList.add('hidden');
|
||||
});
|
||||
|
||||
// Initial load.
|
||||
loadPreview();
|
||||
loadBackupList();
|
||||
|
||||
Reference in New Issue
Block a user