diff --git a/backend/app/api/backups.py b/backend/app/api/backups.py index 609304a4..cd07ac1d 100644 --- a/backend/app/api/backups.py +++ b/backend/app/api/backups.py @@ -1,3 +1,4 @@ +import logging import os from flask import Blueprint, request, jsonify from flask_jwt_extended import jwt_required @@ -11,6 +12,10 @@ from app.exceptions import field_required, not_found from app.api._request import json_object backups_bp = Blueprint('backups', __name__) +logger = logging.getLogger(__name__) + +_STORAGE_UNAVAILABLE = ('Backup storage is not accessible: the panel cannot create or read ' + 'its backup directory. Check its ownership and permissions.') # UI-facing metadata for the CORE target types. Extension kinds @@ -62,7 +67,12 @@ def list_target_types(): def list_backups(): """List all backups.""" backup_type = request.args.get('type') - backups = BackupService.list_backups(backup_type) + try: + backups = BackupService.list_backups(backup_type) + except OSError: + # e.g. PermissionError creating /var/backups/serverkit: keep the page usable. + logger.exception('Backup directory is not accessible') + return jsonify({'backups': [], 'warning': _STORAGE_UNAVAILABLE}), 200 return jsonify({'backups': backups}), 200 @@ -70,7 +80,14 @@ def list_backups(): @admin_required def get_stats(): """Get backup statistics.""" - stats = BackupService.get_backup_stats() + try: + stats = BackupService.get_backup_stats() + except OSError: + logger.exception('Backup directory is not accessible') + stats = {'total_backups': 0, 'application_backups': 0, 'database_backups': 0, + 'file_backups': 0, 'total_size': 0, 'total_size_human': '0 B', + 'remote_count': 0, 'remote_size': 0, 'remote_size_human': '0 B', + 'warning': _STORAGE_UNAVAILABLE} return jsonify(stats), 200 diff --git a/backend/tests/test_backups_unwritable_dir.py b/backend/tests/test_backups_unwritable_dir.py new file mode 100644 index 00000000..5d34f9fa --- /dev/null +++ b/backend/tests/test_backups_unwritable_dir.py @@ -0,0 +1,22 @@ +"""GET /backups must not 500 when the backup directory cannot be created.""" +import pytest + +from app.services.backup_service import BackupService + + +@pytest.fixture +def unwritable(monkeypatch): + def deny(): + raise PermissionError(13, 'Permission denied', '/var/backups/serverkit') + monkeypatch.setattr(BackupService, 'ensure_backup_dirs', deny) + + +@pytest.mark.parametrize('path', ['/api/v1/backups', '/api/v1/backups/stats']) +def test_get_degrades_with_a_warning(client, auth_headers, unwritable, path): + resp = client.get(path, headers=auth_headers) + + assert resp.status_code == 200 + body = resp.get_json() + assert 'not accessible' in body['warning'] + assert '/var/backups' not in body['warning'] + assert body.get('backups', []) == [] diff --git a/frontend/src/pages/Backups.jsx b/frontend/src/pages/Backups.jsx index 3a697e13..eeb3be23 100644 --- a/frontend/src/pages/Backups.jsx +++ b/frontend/src/pages/Backups.jsx @@ -216,6 +216,7 @@ const Backups = () => { ]); setBackups(backupsRes.backups || []); + if (backupsRes.warning) setError(backupsRes.warning); setStats(statsRes); setConfig(configRes); if (refreshSchedules) await reloadSchedules();