diff --git a/windows-agent/agent/config.py b/windows-agent/agent/config.py index e124d8f..f6a3f9d 100644 --- a/windows-agent/agent/config.py +++ b/windows-agent/agent/config.py @@ -45,6 +45,7 @@ class AgentConfig(BaseModel): min_stable_time_seconds: int = 60 log_level: str = "INFO" local_folders: List[LocalFolderJob] = [] + enable_global_exclusions: bool = True # Notification preferences (configurable from GUI and Tray) enable_notifications: bool = True diff --git a/windows-agent/agent/scanner.py b/windows-agent/agent/scanner.py index c6c2136..76c9ad2 100644 --- a/windows-agent/agent/scanner.py +++ b/windows-agent/agent/scanner.py @@ -48,6 +48,18 @@ def is_file_stable(filepath: Path, min_stable_seconds: int = 60, sample_interval import platform import stat +DEFAULT_GLOBAL_EXCLUSIONS = [ + "pagefile.sys", + "hiberfil.sys", + "thumbs.db", + "desktop.ini", + "recycle.bin", + "recycler", + "$recycle.bin", + "temp", + "*.tmp" +] + class DirectoryScanner: """Scans Windows source paths for files matching specific backup patterns.""" @@ -59,7 +71,8 @@ class DirectoryScanner: skip_hidden: bool = False, skip_system: bool = False, skip_readonly: bool = False, - min_stable_seconds: int = 60 + min_stable_seconds: int = 60, + enable_global_exclusions: bool = True ): self.source_path = Path(source_path).resolve() self.patterns = [p.strip() for p in file_patterns.split(",") if p.strip()] @@ -68,6 +81,7 @@ class DirectoryScanner: self.skip_system = skip_system self.skip_readonly = skip_readonly self.min_stable_seconds = min_stable_seconds + self.enable_global_exclusions = enable_global_exclusions def scan(self) -> List[Path]: """Returns list of all matching files that are stable and ready for backup.""" @@ -78,16 +92,19 @@ class DirectoryScanner: if self.source_path.is_file(): if self._matches_patterns(self.source_path.name) and not self._matches_exclude(self.source_path.name): - if not self._should_skip_by_attributes(self.source_path): - matched_files.append(self.source_path) + if not (self.enable_global_exclusions and self._is_globally_excluded(self.source_path.name)): + if not self._should_skip_by_attributes(self.source_path): + matched_files.append(self.source_path) return matched_files for root, dirs, files in os.walk(self.source_path): # Prune excluded directories from search in-place - dirs[:] = [d for d in dirs if not self._matches_exclude(d)] + dirs[:] = [d for d in dirs if not self._matches_exclude(d) and not (self.enable_global_exclusions and self._is_globally_excluded(d))] for file in files: if self._matches_patterns(file) and not self._matches_exclude(file): + if self.enable_global_exclusions and self._is_globally_excluded(file): + continue full_path = Path(root) / file if not self._should_skip_by_attributes(full_path): matched_files.append(full_path) @@ -108,6 +125,12 @@ class DirectoryScanner: return True return False + def _is_globally_excluded(self, name: str) -> bool: + for pattern in DEFAULT_GLOBAL_EXCLUSIONS: + if fnmatch.fnmatch(name.lower(), pattern.lower()): + return True + return False + def _should_skip_by_attributes(self, filepath: Path) -> bool: if platform.system() != "Windows": return False diff --git a/windows-agent/agent/service.py b/windows-agent/agent/service.py index 22d26b2..f64e33b 100644 --- a/windows-agent/agent/service.py +++ b/windows-agent/agent/service.py @@ -416,7 +416,8 @@ class AgentDaemon: skip_hidden=getattr(job, "skip_hidden", False), skip_system=getattr(job, "skip_system", False), skip_readonly=getattr(job, "skip_readonly", False), - min_stable_seconds=min_stable + min_stable_seconds=min_stable, + enable_global_exclusions=getattr(self.config, "enable_global_exclusions", True) ) files = scanner.scan() diff --git a/windows-agent/agent_app_pyqt.py b/windows-agent/agent_app_pyqt.py index b83fb59..15272d5 100644 --- a/windows-agent/agent_app_pyqt.py +++ b/windows-agent/agent_app_pyqt.py @@ -615,6 +615,27 @@ class OnEverDriveMainWindow(QMainWindow): notif_layout.addWidget(self.chk_notif_error) layout.addWidget(card_notif) + + # Global Exclusions Card (Karen parity) + card_excl = QFrame() + card_excl.setProperty("class", "card") + excl_layout = QVBoxLayout(card_excl) + excl_layout.setSpacing(10) + + lbl_excl = QLabel("Exclusiones Globales de Archivos") + lbl_excl.setFont(QFont("Segoe UI", 12, QFont.Weight.Bold)) + excl_layout.addWidget(lbl_excl) + + self.chk_global_exclusions = QCheckBox("Omitir archivos de volcado, temporales y del sistema (pagefile.sys, Temp, Thumbs.db)") + self.chk_global_exclusions.setChecked(self.config.enable_global_exclusions) + self.chk_global_exclusions.stateChanged.connect(self._save_notif_settings) + excl_layout.addWidget(self.chk_global_exclusions) + + lbl_excl_desc = QLabel("Omitirá automáticamente archivos bloqueados del sistema (pagefile.sys, hiberfil.sys) y temporales (*.tmp, Thumbs.db, Desktop.ini) en todos los backups.") + lbl_excl_desc.setStyleSheet("color: #64748B; font-size: 11px;") + excl_layout.addWidget(lbl_excl_desc) + + layout.addWidget(card_excl) layout.addStretch() # --- TAB 4: HISTORIAL --- @@ -689,6 +710,7 @@ class OnEverDriveMainWindow(QMainWindow): self.config.notify_on_start = self.chk_notif_start.isChecked() self.config.notify_on_complete = self.chk_notif_complete.isChecked() self.config.notify_on_error = self.chk_notif_error.isChecked() + self.config.enable_global_exclusions = self.chk_global_exclusions.isChecked() save_config(self.config) self._build_tray_menu()