From 7b609a8d65ad1086d01eac36be619af2055f126a Mon Sep 17 00:00:00 2001 From: allebonvi Date: Thu, 2 Jul 2026 10:05:18 +0200 Subject: [PATCH] Evita baseline completa del watchdog --- SPECIFICHE.md | 7 +++++-- config/config.example.toml | 1 + src/bakrest/config.py | 2 ++ src/bakrest/config_app.py | 21 +++++++++++++++++++++ src/bakrest/default_config.py | 1 + src/bakrest/watcher.py | 29 +++++++++++++++-------------- tests/test_watcher.py | 17 +++++++++++++++++ 7 files changed, 62 insertions(+), 16 deletions(-) create mode 100644 tests/test_watcher.py diff --git a/SPECIFICHE.md b/SPECIFICHE.md index 97cf589..f8e88ca 100644 --- a/SPECIFICHE.md +++ b/SPECIFICHE.md @@ -317,9 +317,12 @@ unchanged_checks_required: 2 ## Riduzione falsi positivi -Quando si monitora una directory molto ampia, ad esempio un'intera unita' `D:\`, la semplice navigazione con Explorer o altri programmi puo' generare eventi `modified` non rilevanti. Per ridurre questi falsi positivi, il servizio deve mantenere uno snapshot locale di dimensione e timestamp di modifica dei file monitorati. +Quando si monitora una directory molto ampia, ad esempio un'intera unita' `D:\`, la semplice navigazione con Explorer o altri programmi puo' generare eventi `modified` non rilevanti. Per ridurre questi falsi positivi senza costruire una baseline completa del disco, il servizio deve applicare una strategia pigra: -All'avvio del servizio viene popolata una baseline degli snapshot per i file gia' presenti, senza inserirli nella coda di backup. Un evento `modified` viene registrato solo se dimensione o timestamp di scrittura risultano diversi dallo snapshot conosciuto. +- non deve scansionare all'avvio tutti i file gia' presenti; +- un evento `modified` viene registrato solo se il timestamp di modifica del file e' recente rispetto all'evento; +- se esiste gia' uno snapshot locale per quel file, l'evento viene registrato solo se dimensione o timestamp risultano diversi dallo snapshot conosciuto; +- i file gia' presenti nel disco entrano quindi in coda solo quando vengono realmente salvati/modificati, non quando vengono solo navigati. ## Configurazione diff --git a/config/config.example.toml b/config/config.example.toml index ffbfb27..2aca588 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -6,6 +6,7 @@ autostart = true [watch] target_user = "" +modified_event_max_age_seconds = 300 include_dirs = [ "%USERPROFILE%\\Desktop", "%USERPROFILE%\\Documents", diff --git a/src/bakrest/config.py b/src/bakrest/config.py index 3757e28..8798059 100644 --- a/src/bakrest/config.py +++ b/src/bakrest/config.py @@ -40,6 +40,7 @@ class WatchConfig: include_extensions: frozenset[str] exclude_dirs: tuple[Path | str, ...] exclude_patterns: tuple[str, ...] + modified_event_max_age_seconds: int @dataclass(frozen=True) @@ -187,6 +188,7 @@ def parse_config(data: dict[str, Any], path: Path) -> AppConfig: include_extensions=include_extensions, exclude_dirs=exclude_dirs, exclude_patterns=tuple(watch.get("exclude_patterns", [])), + modified_event_max_age_seconds=int(watch.get("modified_event_max_age_seconds", 300)), ), backup=BackupConfig( engine=str(backup.get("engine", "robocopy")).lower(), diff --git a/src/bakrest/config_app.py b/src/bakrest/config_app.py index 7557ea9..479e997 100644 --- a/src/bakrest/config_app.py +++ b/src/bakrest/config_app.py @@ -61,8 +61,24 @@ class BakRestConfigApp(ctk.CTk): target_frame = ctk.CTkFrame(self.monitor_tab) target_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=8, pady=(8, 0)) target_frame.grid_columnconfigure(1, weight=1) + target_frame.grid_columnconfigure(3, weight=1) self.target_user_var = tk.StringVar() + self.modified_event_max_age_var = tk.StringVar() _label_entry(target_frame, "Utente percorsi", self.target_user_var, 0) + ctk.CTkLabel(target_frame, text="Eta max modifica (sec)", anchor="w").grid( + row=0, + column=2, + sticky="w", + padx=12, + pady=6, + ) + ctk.CTkEntry(target_frame, textvariable=self.modified_event_max_age_var).grid( + row=0, + column=3, + sticky="ew", + padx=12, + pady=6, + ) left = ctk.CTkFrame(self.monitor_tab) left.grid(row=1, column=0, sticky="nsew", padx=(0, 8), pady=8) @@ -181,6 +197,7 @@ class BakRestConfigApp(ctk.CTk): server_check = backup.setdefault("server_check", {}) self.target_user_var.set(str(watch.get("target_user", ""))) + self.modified_event_max_age_var.set(str(watch.get("modified_event_max_age_seconds", 300))) self.include_dirs.set_items(watch.get("include_dirs", [])) self.extensions.set_items(watch.get("include_extensions", [])) self.exclude_dirs.set_items(watch.get("exclude_dirs", [])) @@ -200,6 +217,10 @@ class BakRestConfigApp(ctk.CTk): watch["include_dirs"] = self.include_dirs.items() watch["target_user"] = self.target_user_var.get().strip() + watch["modified_event_max_age_seconds"] = _safe_int( + self.modified_event_max_age_var.get(), + 300, + ) watch["include_extensions"] = self.extensions.items() watch["exclude_dirs"] = self.exclude_dirs.items() watch["exclude_patterns"] = self.exclude_patterns.items() diff --git a/src/bakrest/default_config.py b/src/bakrest/default_config.py index b22efe8..89427c6 100644 --- a/src/bakrest/default_config.py +++ b/src/bakrest/default_config.py @@ -6,6 +6,7 @@ autostart = true [watch] target_user = "" +modified_event_max_age_seconds = 300 include_dirs = [ "%USERPROFILE%\\\\Desktop", "%USERPROFILE%\\\\Documents", diff --git a/src/bakrest/watcher.py b/src/bakrest/watcher.py index 01f6674..af90cfa 100644 --- a/src/bakrest/watcher.py +++ b/src/bakrest/watcher.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging import threading +import time from pathlib import Path from .config import AppConfig @@ -35,7 +36,6 @@ class WatchdogRuntime: watched = 0 for include_dir in self.config.watch.include_dirs: if include_dir.exists(): - self._prime_snapshots(include_dir) observer.schedule(Handler(), str(include_dir), recursive=True) self.logger.info("Monitoring directory: %s", include_dir) watched += 1 @@ -72,6 +72,13 @@ class WatchdogRuntime: return size, mtime_ns = _safe_snapshot(target_path) + if event_type == "modified" and not _mtime_is_recent( + mtime_ns, + self.config.watch.modified_event_max_age_seconds, + ): + self.logger.info("Ignored stale modified event: %s", target_path) + return + if event_type == "modified" and not self.registry.has_snapshot_changed( str(target_path), size, @@ -94,19 +101,6 @@ class WatchdogRuntime: while not self.stop_event.wait(60): pass - def _prime_snapshots(self, root: Path) -> None: - self.logger.info("Priming file snapshots: %s", root) - count = 0 - for path in root.rglob("*"): - if self.stop_event.is_set(): - break - if not self.filter.should_track(path, is_directory=path.is_dir()): - continue - size, mtime_ns = _safe_snapshot(path) - self.registry.update_snapshot(str(path), size, mtime_ns) - count += 1 - self.logger.info("Primed %s file snapshot(s): %s", count, root) - def _safe_snapshot(path: Path) -> tuple[int | None, int | None]: try: @@ -114,3 +108,10 @@ def _safe_snapshot(path: Path) -> tuple[int | None, int | None]: return stat.st_size, stat.st_mtime_ns except OSError: return None, None + + +def _mtime_is_recent(mtime_ns: int | None, max_age_seconds: int) -> bool: + if mtime_ns is None: + return False + age_seconds = time.time() - (mtime_ns / 1_000_000_000) + return age_seconds <= max_age_seconds diff --git a/tests/test_watcher.py b/tests/test_watcher.py new file mode 100644 index 0000000..17f8ad9 --- /dev/null +++ b/tests/test_watcher.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import time + +from bakrest.watcher import _mtime_is_recent + + +def test_mtime_is_recent_rejects_old_timestamps() -> None: + old_mtime_ns = int((time.time() - 3600) * 1_000_000_000) + + assert not _mtime_is_recent(old_mtime_ns, 300) + + +def test_mtime_is_recent_accepts_recent_timestamps() -> None: + recent_mtime_ns = int(time.time() * 1_000_000_000) + + assert _mtime_is_recent(recent_mtime_ns, 300)