From 60a5e0a18a8bc391ecd73ad3e06a23e973bd1503 Mon Sep 17 00:00:00 2001 From: Juan Denis Date: Tue, 3 Nov 2026 13:10:27 -0500 Subject: [PATCH] fix(backups): answer 503 backup_dir_unavailable when the backup dir is not writable --- backend/app/api/backups.py | 9 ++++++++- backend/tests/test_backups_unwritable_dir.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/backend/app/api/backups.py b/backend/app/api/backups.py index cd07ac1d..9578f935 100644 --- a/backend/app/api/backups.py +++ b/backend/app/api/backups.py @@ -8,7 +8,7 @@ from app.services.backup_service import BackupService from app.services.backup_policy_service import BackupPolicyService, BackupPolicyError from app.services.storage_provider_service import StorageProviderService from app import db, paths -from app.exceptions import field_required, not_found +from app.exceptions import DependencyUnavailableError, field_required, not_found from app.api._request import json_object backups_bp = Blueprint('backups', __name__) @@ -18,6 +18,13 @@ _STORAGE_UNAVAILABLE = ('Backup storage is not accessible: the panel cannot crea 'its backup directory. Check its ownership and permissions.') +@backups_bp.errorhandler(PermissionError) +def _backup_dir_unavailable(exc): + logger.error('Backup directory is not writable: %s', exc) + err = DependencyUnavailableError('Backup directory is not writable', code='backup_dir_unavailable') + return jsonify(err.to_dict()), err.status_code + + # UI-facing metadata for the CORE target types. Extension kinds # carry their own label/scopes in backup_kind_registry. 'server' is listed # but flagged unimplemented so the UI never shows a dead whole-server button. diff --git a/backend/tests/test_backups_unwritable_dir.py b/backend/tests/test_backups_unwritable_dir.py index 5d34f9fa..6a7e7d20 100644 --- a/backend/tests/test_backups_unwritable_dir.py +++ b/backend/tests/test_backups_unwritable_dir.py @@ -20,3 +20,14 @@ def test_get_degrades_with_a_warning(client, auth_headers, unwritable, path): assert 'not accessible' in body['warning'] assert '/var/backups' not in body['warning'] assert body.get('backups', []) == [] + + +def test_create_returns_503_with_code_when_dir_unwritable(client, auth_headers, unwritable): + resp = client.post('/api/v1/backups/database', headers=auth_headers, + json={'db_type': 'mysql', 'db_name': 'x'}) + + assert resp.status_code == 503 + body = resp.get_json() + assert body['code'] == 'backup_dir_unavailable' + assert body['error'] == 'Backup directory is not writable' + assert '/var/backups' not in resp.get_data(as_text=True)