Fix backup restore freezing the panel with loading feedback.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2793,27 +2793,30 @@ async def api_protocol_backup_create(request: Request, server_id: int, req: Prot
|
|||||||
return JSONResponse({'error': 'Forbidden'}, status_code=403)
|
return JSONResponse({'error': 'Forbidden'}, status_code=403)
|
||||||
if not is_valid_protocol(req.protocol):
|
if not is_valid_protocol(req.protocol):
|
||||||
return JSONResponse({'error': 'Unknown protocol'}, status_code=400)
|
return JSONResponse({'error': 'Unknown protocol'}, status_code=400)
|
||||||
ssh = None
|
|
||||||
try:
|
try:
|
||||||
data = load_data()
|
data = await load_data_async()
|
||||||
if server_id >= len(data['servers']):
|
if server_id >= len(data['servers']):
|
||||||
return JSONResponse({'error': 'Server not found'}, status_code=404)
|
return JSONResponse({'error': 'Server not found'}, status_code=404)
|
||||||
container = protocol_container_name(req.protocol)
|
container = protocol_container_name(req.protocol)
|
||||||
if not container:
|
if not container:
|
||||||
return JSONResponse({'error': 'Unknown protocol'}, status_code=400)
|
return JSONResponse({'error': 'Unknown protocol'}, status_code=400)
|
||||||
server = data['servers'][server_id]
|
server = data['servers'][server_id]
|
||||||
ssh = get_ssh(server)
|
|
||||||
ssh.connect()
|
def _do_create():
|
||||||
result = BackupManager(ssh).create_backup(req.protocol, container)
|
ssh = get_ssh(server)
|
||||||
|
ssh.connect()
|
||||||
|
try:
|
||||||
|
return BackupManager(ssh).create_backup(req.protocol, container)
|
||||||
|
finally:
|
||||||
|
ssh.disconnect()
|
||||||
|
|
||||||
|
result = await asyncio.to_thread(_do_create)
|
||||||
if result.get('status') == 'error':
|
if result.get('status') == 'error':
|
||||||
return JSONResponse({'error': result.get('message', 'Failed to create backup')}, status_code=500)
|
return JSONResponse({'error': result.get('message', 'Failed to create backup')}, status_code=500)
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Error creating protocol backup")
|
logger.exception("Error creating protocol backup")
|
||||||
return JSONResponse({'error': str(e)}, status_code=500)
|
return JSONResponse({'error': str(e)}, status_code=500)
|
||||||
finally:
|
|
||||||
if ssh:
|
|
||||||
ssh.disconnect()
|
|
||||||
|
|
||||||
|
|
||||||
@app.post('/api/servers/{server_id}/backups/download', tags=["Protocols"])
|
@app.post('/api/servers/{server_id}/backups/download', tags=["Protocols"])
|
||||||
@@ -2898,27 +2901,30 @@ async def api_protocol_backup_restore(request: Request, server_id: int, req: Bac
|
|||||||
return JSONResponse({'error': 'Forbidden'}, status_code=403)
|
return JSONResponse({'error': 'Forbidden'}, status_code=403)
|
||||||
if not is_valid_protocol(req.protocol):
|
if not is_valid_protocol(req.protocol):
|
||||||
return JSONResponse({'error': 'Unknown protocol'}, status_code=400)
|
return JSONResponse({'error': 'Unknown protocol'}, status_code=400)
|
||||||
ssh = None
|
|
||||||
try:
|
try:
|
||||||
data = load_data()
|
data = await load_data_async()
|
||||||
if server_id >= len(data['servers']):
|
if server_id >= len(data['servers']):
|
||||||
return JSONResponse({'error': 'Server not found'}, status_code=404)
|
return JSONResponse({'error': 'Server not found'}, status_code=404)
|
||||||
container = protocol_container_name(req.protocol)
|
container = protocol_container_name(req.protocol)
|
||||||
if not container:
|
if not container:
|
||||||
return JSONResponse({'error': 'Unknown protocol'}, status_code=400)
|
return JSONResponse({'error': 'Unknown protocol'}, status_code=400)
|
||||||
server = data['servers'][server_id]
|
server = data['servers'][server_id]
|
||||||
ssh = get_ssh(server)
|
|
||||||
ssh.connect()
|
def _do_restore():
|
||||||
result = BackupManager(ssh).restore_backup(req.protocol, container, req.filename)
|
ssh = get_ssh(server)
|
||||||
|
ssh.connect()
|
||||||
|
try:
|
||||||
|
return BackupManager(ssh).restore_backup(req.protocol, container, req.filename)
|
||||||
|
finally:
|
||||||
|
ssh.disconnect()
|
||||||
|
|
||||||
|
result = await asyncio.to_thread(_do_restore)
|
||||||
if result.get('status') == 'error':
|
if result.get('status') == 'error':
|
||||||
return JSONResponse({'error': result.get('message', 'Failed to restore backup')}, status_code=500)
|
return JSONResponse({'error': result.get('message', 'Failed to restore backup')}, status_code=500)
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Error restoring protocol backup")
|
logger.exception("Error restoring protocol backup")
|
||||||
return JSONResponse({'error': str(e)}, status_code=500)
|
return JSONResponse({'error': str(e)}, status_code=500)
|
||||||
finally:
|
|
||||||
if ssh:
|
|
||||||
ssh.disconnect()
|
|
||||||
|
|
||||||
|
|
||||||
@app.post('/api/servers/{server_id}/backups/export-clients', tags=["Protocols"])
|
@app.post('/api/servers/{server_id}/backups/export-clients', tags=["Protocols"])
|
||||||
|
|||||||
@@ -229,15 +229,15 @@ for p in {host_paths}; do restore_host_path "$p"; done
|
|||||||
for p in {container_paths}; do restore_container_path "$p"; done
|
for p in {container_paths}; do restore_container_path "$p"; done
|
||||||
if [ -n "$container" ] && docker inspect "$container" >/dev/null 2>&1; then
|
if [ -n "$container" ] && docker inspect "$container" >/dev/null 2>&1; then
|
||||||
docker restart "$container" >/dev/null 2>&1 || true
|
docker restart "$container" >/dev/null 2>&1 || true
|
||||||
# Prefer bringing the WireGuard/AWG interface back up after file restore
|
# Entrypoint usually brings the interface up; give start.sh a short bounded window
|
||||||
if docker exec "$container" test -x /opt/amnezia/start.sh >/dev/null 2>&1; then
|
if docker exec "$container" test -x /opt/amnezia/start.sh >/dev/null 2>&1; then
|
||||||
docker exec "$container" /opt/amnezia/start.sh >/dev/null 2>&1 || true
|
timeout 25 docker exec "$container" /opt/amnezia/start.sh >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
printf 'restored:%s\\n' "$protocol"
|
printf 'restored:%s\\n' "$protocol"
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
out, err, code = self.ssh.run_sudo_command(script, timeout=180)
|
out, err, code = self.ssh.run_sudo_command(script, timeout=120)
|
||||||
if code != 0:
|
if code != 0:
|
||||||
return {'status': 'error', 'message': err or out or 'Failed to restore backup'}
|
return {'status': 'error', 'message': err or out or 'Failed to restore backup'}
|
||||||
return {'status': 'success', 'protocol': protocol, 'filename': safe_name}
|
return {'status': 'success', 'protocol': protocol, 'filename': safe_name}
|
||||||
|
|||||||
@@ -761,6 +761,10 @@
|
|||||||
<button class="btn btn-secondary btn-sm" onclick="loadProtocolBackups()">{{ _('refresh') }}</button>
|
<button class="btn btn-secondary btn-sm" onclick="loadProtocolBackups()">{{ _('refresh') }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-hint hidden" id="exportClientsHint" style="margin-bottom:var(--space-md);">{{ _('export_client_configs_hint') }}</div>
|
<div class="form-hint hidden" id="exportClientsHint" style="margin-bottom:var(--space-md);">{{ _('export_client_configs_hint') }}</div>
|
||||||
|
<div id="backupBusy" class="hidden" style="display:none; align-items:center; gap:var(--space-sm); margin-bottom:var(--space-md); padding:var(--space-md); border-radius:12px; background:var(--accent-glow); border:1px solid color-mix(in srgb, var(--accent) 35%, transparent);">
|
||||||
|
<div class="spinner" style="width:18px;height:18px;border-width:2px;"></div>
|
||||||
|
<span id="backupBusyText">{{ _('restoring_protocol_backup') }}</span>
|
||||||
|
</div>
|
||||||
<div id="backupLoading" class="form-hint hidden">{{ _('loading') }}</div>
|
<div id="backupLoading" class="form-hint hidden">{{ _('loading') }}</div>
|
||||||
<div id="backupEmpty" class="empty-state hidden" style="padding:var(--space-xl);">
|
<div id="backupEmpty" class="empty-state hidden" style="padding:var(--space-xl);">
|
||||||
<div class="empty-icon">{{ icon('package') }}</div>
|
<div class="empty-icon">{{ icon('package') }}</div>
|
||||||
@@ -2129,11 +2133,38 @@
|
|||||||
|
|
||||||
async function restoreProtocolBackup(proto, filename) {
|
async function restoreProtocolBackup(proto, filename) {
|
||||||
if (!confirm(_('restore_protocol_backup_confirm'))) return;
|
if (!confirm(_('restore_protocol_backup_confirm'))) return;
|
||||||
|
|
||||||
|
const busy = document.getElementById('backupBusy');
|
||||||
|
const busyText = document.getElementById('backupBusyText');
|
||||||
|
const createBtn = document.getElementById('createBackupBtn');
|
||||||
|
const exportBtn = document.getElementById('exportClientsBtn');
|
||||||
|
const list = document.getElementById('backupList');
|
||||||
|
const restoreButtons = list ? Array.from(list.querySelectorAll('button')) : [];
|
||||||
|
|
||||||
|
if (busyText) busyText.textContent = _('restoring_protocol_backup');
|
||||||
|
if (busy) {
|
||||||
|
busy.classList.remove('hidden');
|
||||||
|
busy.style.display = 'flex';
|
||||||
|
}
|
||||||
|
if (createBtn) createBtn.disabled = true;
|
||||||
|
if (exportBtn) exportBtn.disabled = true;
|
||||||
|
restoreButtons.forEach(b => { b.disabled = true; });
|
||||||
|
showToast(_('restoring_protocol_backup'), 'info');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiCall(`/api/servers/${SERVER_ID}/backups/restore`, 'POST', { protocol: proto, filename });
|
await apiCall(`/api/servers/${SERVER_ID}/backups/restore`, 'POST', { protocol: proto, filename });
|
||||||
showToast(_('restore_protocol_backup_done'), 'success');
|
showToast(_('restore_protocol_backup_done'), 'success');
|
||||||
|
await loadProtocolBackups();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(_('error') + ': ' + err.message, 'error');
|
showToast(_('error') + ': ' + err.message, 'error');
|
||||||
|
await loadProtocolBackups();
|
||||||
|
} finally {
|
||||||
|
if (busy) {
|
||||||
|
busy.classList.add('hidden');
|
||||||
|
busy.style.display = 'none';
|
||||||
|
}
|
||||||
|
if (createBtn) createBtn.disabled = false;
|
||||||
|
if (exportBtn) exportBtn.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -517,6 +517,7 @@
|
|||||||
"export_client_configs_done": "Client configs ZIP downloaded",
|
"export_client_configs_done": "Client configs ZIP downloaded",
|
||||||
"restore_protocol_backup": "Restore",
|
"restore_protocol_backup": "Restore",
|
||||||
"restore_protocol_backup_confirm": "Restore this backup on the server? Current protocol files will be overwritten and the container will restart.",
|
"restore_protocol_backup_confirm": "Restore this backup on the server? Current protocol files will be overwritten and the container will restart.",
|
||||||
|
"restoring_protocol_backup": "Restoring… please wait",
|
||||||
"restore_protocol_backup_done": "Backup restored",
|
"restore_protocol_backup_done": "Backup restored",
|
||||||
"refresh": "Refresh"
|
"refresh": "Refresh"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -517,6 +517,7 @@
|
|||||||
"export_client_configs_done": "ZIP с конфигами скачан",
|
"export_client_configs_done": "ZIP с конфигами скачан",
|
||||||
"restore_protocol_backup": "Восстановить",
|
"restore_protocol_backup": "Восстановить",
|
||||||
"restore_protocol_backup_confirm": "Восстановить этот бекап на сервер? Текущие файлы протокола будут перезаписаны, контейнер перезапустится.",
|
"restore_protocol_backup_confirm": "Восстановить этот бекап на сервер? Текущие файлы протокола будут перезаписаны, контейнер перезапустится.",
|
||||||
|
"restoring_protocol_backup": "Восстановление… подождите",
|
||||||
"restore_protocol_backup_done": "Бекап восстановлен",
|
"restore_protocol_backup_done": "Бекап восстановлен",
|
||||||
"refresh": "Обновить"
|
"refresh": "Обновить"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user