Skip to content

Commit 4723bcf

Browse files
authored
fix: harden backup module config coercion (#226)
Co-authored-by: Dennis Braun <itsDNNS@users.noreply.github.com>
1 parent b34e87e commit 4723bcf

File tree

3 files changed

+73
-3
lines changed

3 files changed

+73
-3
lines changed

app/modules/backup/collector.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,27 @@ class BackupCollector(Collector):
1313
name = "backup"
1414

1515
def __init__(self, config_mgr, poll_interval=86400, **kwargs):
16-
super().__init__(poll_interval)
1716
self._config_mgr = config_mgr
17+
interval_hours = self._get_interval_hours(config_mgr, poll_interval // 3600)
18+
super().__init__(interval_hours * 3600)
19+
20+
@staticmethod
21+
def _get_interval_hours(config_mgr, default_hours=24):
22+
"""Return configured backup interval in hours as int."""
23+
try:
24+
value = int(config_mgr.get("backup_interval_hours", default_hours))
25+
except (TypeError, ValueError):
26+
return default_hours
27+
return value if value > 0 else default_hours
28+
29+
@staticmethod
30+
def _get_retention(config_mgr, default_keep=5):
31+
"""Return backup retention as int."""
32+
try:
33+
value = int(config_mgr.get("backup_retention", default_keep))
34+
except (TypeError, ValueError):
35+
return default_keep
36+
return value if value > 0 else default_keep
1837

1938
def is_enabled(self):
2039
return self._config_mgr.is_backup_configured()
@@ -24,7 +43,7 @@ def collect(self):
2443

2544
data_dir = self._config_mgr.data_dir
2645
backup_path = self._config_mgr.get("backup_path", "/backup")
27-
retention = self._config_mgr.get("backup_retention", 5)
46+
retention = self._get_retention(self._config_mgr)
2847

2948
try:
3049
filename = create_backup_to_file(data_dir, backup_path)

app/modules/backup/routes.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@
2020
bp = Blueprint("backup_bp", __name__)
2121

2222

23+
def _int_config(config_mgr, key, default):
24+
"""Read an integer-like config value safely."""
25+
try:
26+
value = int(config_mgr.get(key, default))
27+
except (TypeError, ValueError):
28+
return default
29+
return value if value > 0 else default
30+
31+
2332
@bp.route("/api/backup", methods=["POST"])
2433
@require_auth
2534
def api_backup_download():
@@ -47,7 +56,7 @@ def api_backup_scheduled():
4756
if not _config_manager:
4857
return jsonify({"error": "Not initialized"}), 500
4958
backup_path = _config_manager.get("backup_path", "/backup")
50-
retention = _config_manager.get("backup_retention", 5)
59+
retention = _int_config(_config_manager, "backup_retention", 5)
5160
try:
5261
from .backup import create_backup_to_file, cleanup_old_backups
5362
filename = create_backup_to_file(_config_manager.data_dir, backup_path)

tests/test_backup.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
import sqlite3
66
import tarfile
7+
from unittest.mock import MagicMock
78

89
import pytest
910
from io import BytesIO
@@ -20,6 +21,7 @@
2021
restore_backup,
2122
validate_backup,
2223
)
24+
from app.modules.backup.collector import BackupCollector
2325

2426

2527
# ── Fixtures ──
@@ -264,6 +266,46 @@ def test_cleanup_noop_when_few(self, backup_dir):
264266
assert deleted == 0
265267

266268

269+
class TestBackupCollectorConfig:
270+
def test_uses_configured_interval_hours_from_string(self):
271+
mgr = MagicMock()
272+
mgr.get.side_effect = lambda key, default=None: {
273+
"backup_interval_hours": "168",
274+
}.get(key, default)
275+
276+
collector = BackupCollector(mgr)
277+
278+
assert collector.poll_interval_seconds == 168 * 3600
279+
280+
def test_collect_casts_retention_from_string(self, monkeypatch):
281+
mgr = MagicMock()
282+
mgr.data_dir = "/data"
283+
mgr.get.side_effect = lambda key, default=None: {
284+
"backup_path": "/backup",
285+
"backup_retention": "5",
286+
}.get(key, default)
287+
288+
calls = {}
289+
290+
def fake_create_backup_to_file(data_dir, backup_path):
291+
calls["create"] = (data_dir, backup_path)
292+
return "docsight_backup_test.tar.gz"
293+
294+
def fake_cleanup_old_backups(backup_path, keep=5):
295+
calls["cleanup"] = (backup_path, keep)
296+
return 0
297+
298+
monkeypatch.setattr("app.modules.backup.backup.create_backup_to_file", fake_create_backup_to_file)
299+
monkeypatch.setattr("app.modules.backup.backup.cleanup_old_backups", fake_cleanup_old_backups)
300+
301+
collector = BackupCollector(mgr)
302+
result = collector.collect()
303+
304+
assert result.success is True
305+
assert calls["create"] == ("/data", "/backup")
306+
assert calls["cleanup"] == ("/backup", 5)
307+
308+
267309
# ── TestBrowseDirectory ──
268310

269311

0 commit comments

Comments
 (0)