diff --git a/src/bakrest/backup.py b/src/bakrest/backup.py index c781598..9e3d594 100644 --- a/src/bakrest/backup.py +++ b/src/bakrest/backup.py @@ -10,7 +10,7 @@ from pathlib import Path from .config import AppConfig from .registry import ChangeEvent, ChangeRegistry -from .server_check import check_server +from .server_check import check_backup_target @dataclass(frozen=True) @@ -23,7 +23,7 @@ class BackupResult: def run_backup(config: AppConfig, logger: logging.Logger) -> BackupResult: - status = check_server(config.backup) + status = check_backup_target(config.backup) if not status.reachable: message = f"Server backup non raggiungibile: {status.message}" logger.error(message) diff --git a/src/bakrest/server_check.py b/src/bakrest/server_check.py index 8aa3f79..04a4552 100644 --- a/src/bakrest/server_check.py +++ b/src/bakrest/server_check.py @@ -2,6 +2,7 @@ from __future__ import annotations import json import socket +import subprocess from dataclasses import asdict, dataclass from datetime import datetime, timezone from pathlib import Path @@ -31,6 +32,49 @@ def check_server(config: BackupConfig) -> ServerStatus: return ServerStatus(False, checked_at, str(exc)) +def check_backup_target(config: BackupConfig) -> ServerStatus: + if config.engine != "robocopy": + return check_server(config) + + checked_at = datetime.now(timezone.utc).isoformat() + if not config.remote_destinations: + return ServerStatus(False, checked_at, "No backup shares configured") + + server_status = check_server(config) + if not server_status.reachable: + return server_status + + unreachable = [ + destination + for destination in config.remote_destinations + if not share_is_reachable(destination, config.server_check.timeout_seconds) + ] + if unreachable: + return ServerStatus( + False, + checked_at, + "Backup share unreachable: " + ", ".join(unreachable), + ) + + return ServerStatus(True, checked_at, "Backup shares reachable") + + +def share_is_reachable(destination: str, timeout_seconds: int) -> bool: + probe = destination.rstrip("\\/") + "\\." + command = ["cmd.exe", "/d", "/c", f'if exist "{probe}" (exit /b 0) else (exit /b 1)'] + try: + completed = subprocess.run( + command, + capture_output=True, + text=True, + timeout=max(timeout_seconds, 1), + check=False, + ) + except (OSError, subprocess.TimeoutExpired): + return False + return completed.returncode == 0 + + def write_status(path: Path, status: ServerStatus, pending_count: int) -> None: path.parent.mkdir(parents=True, exist_ok=True) payload = asdict(status) diff --git a/src/bakrest/server_notifier.py b/src/bakrest/server_notifier.py index a1ffea3..3921ea5 100644 --- a/src/bakrest/server_notifier.py +++ b/src/bakrest/server_notifier.py @@ -9,7 +9,7 @@ from tkinter import messagebox from .config import load_config from .logging_setup import close_logger, configure_logging from .registry import ChangeRegistry -from .server_check import ServerStatus, check_server, write_status +from .server_check import ServerStatus, check_backup_target, write_status ALERT_MESSAGE = "Accendi il server di backup o verifica che sia connesso alla rete" @@ -19,7 +19,7 @@ def run_server_notifier(always_alert: bool = False) -> int: config = load_config() logger = configure_logging(config.storage.logs_dir, "server-notifier") try: - status = check_server(config.backup) + status = check_backup_target(config.backup) pending_count = ChangeRegistry(config.storage.change_registry).pending_count() write_status(config.storage.status_file, status, pending_count) diff --git a/tasks/BakRestBackupOnLogoff.xml b/tasks/BakRestBackupOnLogoff.xml index b885288..976a4cc 100644 --- a/tasks/BakRestBackupOnLogoff.xml +++ b/tasks/BakRestBackupOnLogoff.xml @@ -39,7 +39,7 @@ C:\Python314\python.exe -m bakrest.tray_app --nogui - C:\devel\bak&rest + C:\programmi\bak-and-rest diff --git a/tasks/BakRestServerNotifierEvery30Minutes.xml b/tasks/BakRestServerNotifierEvery30Minutes.xml index 2c2acab..f2ed808 100644 --- a/tasks/BakRestServerNotifierEvery30Minutes.xml +++ b/tasks/BakRestServerNotifierEvery30Minutes.xml @@ -50,7 +50,7 @@ C:\Python314\python.exe -m bakrest.server_notifier - C:\devel\bak&rest + C:\programmi\bak-and-rest diff --git a/tests/test_server_check.py b/tests/test_server_check.py new file mode 100644 index 0000000..f72b1ff --- /dev/null +++ b/tests/test_server_check.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +import subprocess + +from bakrest.server_check import share_is_reachable + + +def test_share_is_reachable_returns_true_on_zero_exit(monkeypatch) -> None: + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess(args[0], 0) + + monkeypatch.setattr("bakrest.server_check.subprocess.run", fake_run) + + assert share_is_reachable("\\\\server\\share", 3) + + +def test_share_is_reachable_returns_false_on_nonzero_exit(monkeypatch) -> None: + def fake_run(*args, **kwargs): + return subprocess.CompletedProcess(args[0], 1) + + monkeypatch.setattr("bakrest.server_check.subprocess.run", fake_run) + + assert not share_is_reachable("\\\\server\\share", 3) + + +def test_share_is_reachable_returns_false_on_timeout(monkeypatch) -> None: + def fake_run(*args, **kwargs): + raise subprocess.TimeoutExpired(args[0], 3) + + monkeypatch.setattr("bakrest.server_check.subprocess.run", fake_run) + + assert not share_is_reachable("\\\\server\\share", 3)