""" Backup Profile Editor Dialog Allows creating and editing backup profiles with source paths and exclude patterns """ import json from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QGroupBox, QLineEdit, QSpinBox, QCheckBox, QListWidget, QPushButton, QDialogButtonBox, QFileDialog, QMessageBox, QLabel ) from PyQt6.QtCore import Qt from loguru import logger from core.database import Database class BackupProfileDialog(QDialog): """Dialog for creating/editing backup profiles""" def __init__(self, parent, db: Database, profile_id: int = None): super().__init__(parent) self.db = db self.profile_id = profile_id # Determine mode self.is_edit = profile_id is not None self.setWindowTitle("Edit Backup Profile" if self.is_edit else "New Backup Profile") self.setMinimumSize(700, 600) self._setup_ui() if self.is_edit: self._load_profile_data() def _setup_ui(self): """Setup dialog UI""" layout = QVBoxLayout(self) layout.setSpacing(15) # Group 1: Basic Info basic_group = QGroupBox("Basic Information") basic_layout = QFormLayout(basic_group) self.name_edit = QLineEdit() self.name_edit.setPlaceholderText("e.g., Daily Backup, Weekly Archive") basic_layout.addRow("Profile Name *:", self.name_edit) self.description_edit = QLineEdit() self.description_edit.setPlaceholderText("Optional description") basic_layout.addRow("Description:", self.description_edit) layout.addWidget(basic_group) # Group 2: Backup Settings settings_group = QGroupBox("Backup Settings") settings_layout = QFormLayout(settings_group) # Backup root with browse button backup_root_layout = QHBoxLayout() self.backup_root_edit = QLineEdit() self.backup_root_edit.setPlaceholderText("e.g., D:\\backups") backup_root_layout.addWidget(self.backup_root_edit) browse_root_btn = QPushButton("Browse...") browse_root_btn.clicked.connect(self._browse_backup_root) backup_root_layout.addWidget(browse_root_btn) settings_layout.addRow("Backup Root *:", backup_root_layout) self.retention_spin = QSpinBox() self.retention_spin.setRange(1, 365) self.retention_spin.setValue(30) self.retention_spin.setSuffix(" days") settings_layout.addRow("Retention:", self.retention_spin) self.enabled_check = QCheckBox("Enable this profile") self.enabled_check.setChecked(True) settings_layout.addRow("", self.enabled_check) layout.addWidget(settings_group) # Group 3: Source Paths sources_group = QGroupBox("Source Paths") sources_layout = QVBoxLayout(sources_group) sources_label = QLabel("Directories to backup (at least one required) *:") sources_layout.addWidget(sources_label) self.sources_list = QListWidget() self.sources_list.setSelectionMode(QListWidget.SelectionMode.SingleSelection) sources_layout.addWidget(self.sources_list) sources_buttons_layout = QHBoxLayout() add_source_btn = QPushButton("Add Directory...") add_source_btn.clicked.connect(self._add_source) sources_buttons_layout.addWidget(add_source_btn) remove_source_btn = QPushButton("Remove Selected") remove_source_btn.clicked.connect(self._remove_source) sources_buttons_layout.addWidget(remove_source_btn) sources_buttons_layout.addStretch() sources_layout.addLayout(sources_buttons_layout) layout.addWidget(sources_group) # Group 4: Exclude Patterns exclude_group = QGroupBox("Exclude Patterns") exclude_layout = QVBoxLayout(exclude_group) exclude_label = QLabel("File patterns to exclude (glob syntax):") exclude_layout.addWidget(exclude_label) self.patterns_list = QListWidget() self.patterns_list.setSelectionMode(QListWidget.SelectionMode.SingleSelection) exclude_layout.addWidget(self.patterns_list) patterns_buttons_layout = QHBoxLayout() add_pattern_btn = QPushButton("Add Pattern...") add_pattern_btn.clicked.connect(self._add_pattern) patterns_buttons_layout.addWidget(add_pattern_btn) remove_pattern_btn = QPushButton("Remove Selected") remove_pattern_btn.clicked.connect(self._remove_pattern) patterns_buttons_layout.addWidget(remove_pattern_btn) add_common_btn = QPushButton("Add Common Patterns") add_common_btn.clicked.connect(self._add_common_patterns) patterns_buttons_layout.addWidget(add_common_btn) patterns_buttons_layout.addStretch() exclude_layout.addLayout(patterns_buttons_layout) layout.addWidget(exclude_group) # Dialog buttons button_box = QDialogButtonBox( QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel ) button_box.accepted.connect(self._save_profile) button_box.rejected.connect(self.reject) layout.addWidget(button_box) def _load_profile_data(self): """Load profile data in edit mode""" try: profile = self.db.get_backup_profile(self.profile_id) if not profile: QMessageBox.critical(self, "Error", f"Profile {self.profile_id} not found") self.reject() return self.name_edit.setText(profile['name']) self.description_edit.setText(profile.get('description', '')) self.backup_root_edit.setText(profile['backup_root']) self.retention_spin.setValue(profile['retention_days']) self.enabled_check.setChecked(bool(profile['enabled'])) # Load sources sources = profile['sources'] if isinstance(sources, str): sources = json.loads(sources) for source in sources: self.sources_list.addItem(source) # Load exclude patterns patterns = profile.get('exclude_patterns', []) if isinstance(patterns, str): patterns = json.loads(patterns) for pattern in patterns: self.patterns_list.addItem(pattern) except Exception as e: logger.error(f"Error loading profile data: {e}") QMessageBox.critical(self, "Error", f"Failed to load profile: {e}") self.reject() def _browse_backup_root(self): """Browse for backup root directory""" directory = QFileDialog.getExistingDirectory( self, "Select Backup Root Directory", self.backup_root_edit.text() or "" ) if directory: self.backup_root_edit.setText(directory) def _add_source(self): """Add source directory""" directory = QFileDialog.getExistingDirectory( self, "Select Source Directory", "" ) if directory: # Check for duplicates items = [self.sources_list.item(i).text() for i in range(self.sources_list.count())] if directory not in items: self.sources_list.addItem(directory) else: QMessageBox.information(self, "Duplicate", "This directory is already in the list") def _remove_source(self): """Remove selected source directory""" current = self.sources_list.currentRow() if current >= 0: self.sources_list.takeItem(current) def _add_pattern(self): """Add exclude pattern""" from PyQt6.QtWidgets import QInputDialog pattern, ok = QInputDialog.getText( self, "Add Exclude Pattern", "Enter glob pattern (e.g., *.log, */.git/*, */temp/*):" ) if ok and pattern.strip(): pattern = pattern.strip() # Check for duplicates items = [self.patterns_list.item(i).text() for i in range(self.patterns_list.count())] if pattern not in items: self.patterns_list.addItem(pattern) else: QMessageBox.information(self, "Duplicate", "This pattern is already in the list") def _remove_pattern(self): """Remove selected exclude pattern""" current = self.patterns_list.currentRow() if current >= 0: self.patterns_list.takeItem(current) def _add_common_patterns(self): """Add common exclude patterns""" common_patterns = [ "*/.git/*", "*/venv/*", "*/__pycache__/*", "*/node_modules/*", "*.tmp", "*.log", "*/.cache/*", "*/temp/*" ] # Get existing patterns existing = [self.patterns_list.item(i).text() for i in range(self.patterns_list.count())] # Add patterns that don't exist added_count = 0 for pattern in common_patterns: if pattern not in existing: self.patterns_list.addItem(pattern) added_count += 1 if added_count > 0: QMessageBox.information( self, "Patterns Added", f"Added {added_count} common pattern(s)" ) else: QMessageBox.information( self, "No Changes", "All common patterns are already in the list" ) def _validate_inputs(self) -> bool: """Validate form inputs""" # Name required if not self.name_edit.text().strip(): QMessageBox.warning(self, "Validation Error", "Profile name is required") self.name_edit.setFocus() return False # Backup root required if not self.backup_root_edit.text().strip(): QMessageBox.warning(self, "Validation Error", "Backup root directory is required") self.backup_root_edit.setFocus() return False # At least one source required if self.sources_list.count() == 0: QMessageBox.warning( self, "Validation Error", "At least one source directory is required" ) return False return True def _save_profile(self): """Save profile to database""" if not self._validate_inputs(): return try: # Collect form data name = self.name_edit.text().strip() description = self.description_edit.text().strip() backup_root = self.backup_root_edit.text().strip() retention_days = self.retention_spin.value() enabled = self.enabled_check.isChecked() # Collect sources sources = [ self.sources_list.item(i).text() for i in range(self.sources_list.count()) ] # Collect patterns exclude_patterns = [ self.patterns_list.item(i).text() for i in range(self.patterns_list.count()) ] if self.is_edit: # Update existing profile self.db.update_backup_profile( profile_id=self.profile_id, name=name, backup_root=backup_root, sources=sources, description=description, exclude_patterns=exclude_patterns, retention_days=retention_days, enabled=enabled ) logger.info(f"Updated backup profile {self.profile_id}: {name}") else: # Create new profile profile_id = self.db.create_backup_profile( name=name, backup_root=backup_root, sources=sources, description=description, exclude_patterns=exclude_patterns, retention_days=retention_days, enabled=enabled ) logger.info(f"Created backup profile {profile_id}: {name}") self.accept() except Exception as e: logger.error(f"Error saving profile: {e}") QMessageBox.critical(self, "Error", f"Failed to save profile: {e}")