"""Config Backup Management Methods for Glossarion These methods handle automatic and manual config backup/restore functionality. They are designed to be bound to the TranslatorGUI instance. """ import os import sys import time import shutil import json from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTreeWidget, QTreeWidgetItem, QMessageBox, QFrame, QGroupBox, QApplication ) from PySide6.QtCore import Qt from PySide6.QtGui import QIcon, QFont # Import required from translator_gui from translator_gui import CONFIG_FILE, decrypt_config def _backup_config_file(self): """Create backup of the existing config file before saving.""" try: # Skip if config file doesn't exist yet if not os.path.exists(CONFIG_FILE): return # Get base directory that works in both development and frozen environments base_dir = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__))) # Resolve config file path for backup directory if os.path.isabs(CONFIG_FILE): config_dir = os.path.dirname(CONFIG_FILE) else: config_dir = os.path.dirname(os.path.abspath(CONFIG_FILE)) # Create backup directory backup_dir = os.path.join(config_dir, "config_backups") os.makedirs(backup_dir, exist_ok=True) # Create timestamped backup name backup_name = f"config_{time.strftime('%Y%m%d_%H%M%S')}.json.bak" backup_path = os.path.join(backup_dir, backup_name) # Copy the file shutil.copy2(CONFIG_FILE, backup_path) # Clean backups older than 72 hours cutoff_time = time.time() - (72 * 60 * 60) # 72 hours in seconds backups = [os.path.join(backup_dir, f) for f in os.listdir(backup_dir) if f.startswith("config_") and f.endswith(".json.bak")] # Remove backups older than 72 hours for backup_file in backups: try: if os.path.getmtime(backup_file) <= cutoff_time: os.remove(backup_file) except Exception: pass # Ignore errors when cleaning old backups except Exception as e: # Silent exception - don't interrupt normal operation if backup fails print(f"Warning: Could not create config backup: {e}") def _restore_config_from_backup(self): """Attempt to restore config from the most recent backup.""" try: # Locate backups directory if os.path.isabs(CONFIG_FILE): config_dir = os.path.dirname(CONFIG_FILE) else: config_dir = os.path.dirname(os.path.abspath(CONFIG_FILE)) backup_dir = os.path.join(config_dir, "config_backups") if not os.path.exists(backup_dir): return # Find most recent backup backups = [os.path.join(backup_dir, f) for f in os.listdir(backup_dir) if f.startswith("config_") and f.endswith(".json.bak")] if not backups: return backups.sort(key=lambda x: os.path.getmtime(x), reverse=True) latest_backup = backups[0] # Copy backup to config file shutil.copy2(latest_backup, CONFIG_FILE) from PySide6.QtWidgets import QMessageBox from PySide6.QtGui import QIcon # Get icon path icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "halgakos.ico") icon = QIcon(icon_path) if os.path.exists(icon_path) else QIcon() msg_box = QMessageBox() msg_box.setIcon(QMessageBox.Information) msg_box.setWindowTitle("Config Restored") msg_box.setText(f"Configuration was restored from backup: {os.path.basename(latest_backup)}") msg_box.setWindowIcon(icon) msg_box.exec() # Reload config try: with open(CONFIG_FILE, 'r', encoding='utf-8') as f: self.config = json.load(f) self.config = decrypt_config(self.config) except Exception as e: msg_box = QMessageBox() msg_box.setIcon(QMessageBox.Critical) msg_box.setWindowTitle("Error") msg_box.setText(f"Failed to reload configuration: {e}") msg_box.setWindowIcon(icon) msg_box.exec() except Exception as e: from PySide6.QtWidgets import QMessageBox from PySide6.QtGui import QIcon icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "halgakos.ico") icon = QIcon(icon_path) if os.path.exists(icon_path) else QIcon() msg_box = QMessageBox() msg_box.setIcon(QMessageBox.Critical) msg_box.setWindowTitle("Restore Failed") msg_box.setText(f"Could not restore config from backup: {e}") msg_box.setWindowIcon(icon) msg_box.exec() def _create_manual_config_backup(self): """Create a manual config backup.""" try: # Force create backup even if config file doesn't exist self._backup_config_file() from PySide6.QtWidgets import QMessageBox from PySide6.QtGui import QIcon # Get icon path icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "halgakos.ico") icon = QIcon(icon_path) if os.path.exists(icon_path) else QIcon() msg_box = QMessageBox() msg_box.setIcon(QMessageBox.Information) msg_box.setWindowTitle("Backup Created") msg_box.setText("Configuration backup created successfully!") msg_box.setWindowIcon(icon) msg_box.exec() except Exception as e: from PySide6.QtWidgets import QMessageBox from PySide6.QtGui import QIcon icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "halgakos.ico") icon = QIcon(icon_path) if os.path.exists(icon_path) else QIcon() msg_box = QMessageBox() msg_box.setIcon(QMessageBox.Critical) msg_box.setWindowTitle("Backup Failed") msg_box.setText(f"Failed to create backup: {e}") msg_box.setWindowIcon(icon) msg_box.exec() def _open_backup_folder(self): """Open the config backups folder in file explorer.""" try: from PySide6.QtGui import QIcon # Get icon path icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "halgakos.ico") icon = QIcon(icon_path) if os.path.exists(icon_path) else QIcon() if os.path.isabs(CONFIG_FILE): config_dir = os.path.dirname(CONFIG_FILE) else: config_dir = os.path.dirname(os.path.abspath(CONFIG_FILE)) backup_dir = os.path.join(config_dir, "config_backups") if not os.path.exists(backup_dir): os.makedirs(backup_dir, exist_ok=True) from PySide6.QtWidgets import QMessageBox msg_box = QMessageBox() msg_box.setIcon(QMessageBox.Information) msg_box.setWindowTitle("Backup Folder") msg_box.setText(f"Created backup folder: {backup_dir}") msg_box.setWindowIcon(icon) msg_box.exec() # Open folder in explorer (cross-platform) import subprocess import platform if platform.system() == "Windows": os.startfile(backup_dir) elif platform.system() == "Darwin": # macOS subprocess.run(["open", backup_dir]) else: # Linux subprocess.run(["xdg-open", backup_dir]) except Exception as e: from PySide6.QtWidgets import QMessageBox from PySide6.QtGui import QIcon icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "halgakos.ico") icon = QIcon(icon_path) if os.path.exists(icon_path) else QIcon() msg_box = QMessageBox() msg_box.setIcon(QMessageBox.Critical) msg_box.setWindowTitle("Error") msg_box.setText(f"Could not open backup folder: {e}") msg_box.setWindowIcon(icon) msg_box.exec() def _manual_restore_config(self): """Show dialog to manually select and restore a config backup.""" try: # Ensure QApplication exists app = QApplication.instance() if not app: try: QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) except: pass app = QApplication(sys.argv) if os.path.isabs(CONFIG_FILE): config_dir = os.path.dirname(CONFIG_FILE) else: config_dir = os.path.dirname(os.path.abspath(CONFIG_FILE)) backup_dir = os.path.join(config_dir, "config_backups") icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "halgakos.ico") icon = QIcon(icon_path) if os.path.exists(icon_path) else QIcon() if not os.path.exists(backup_dir): msg_box = QMessageBox() msg_box.setIcon(QMessageBox.Information) msg_box.setWindowTitle("No Backups") msg_box.setText("No backup folder found. No backups have been created yet.") msg_box.setWindowIcon(icon) msg_box.exec() return # Get list of available backups backups = [f for f in os.listdir(backup_dir) if f.startswith("config_") and f.endswith(".json.bak")] if not backups: msg_box = QMessageBox() msg_box.setIcon(QMessageBox.Information) msg_box.setWindowTitle("No Backups") msg_box.setText("No config backups found.") msg_box.setWindowIcon(icon) msg_box.exec() return # Sort by creation time (newest first) backups.sort(key=lambda x: os.path.getmtime(os.path.join(backup_dir, x)), reverse=True) # Create PySide6 dialog dialog = QDialog(None) dialog.setWindowTitle("Config Backup Manager") dialog.setWindowIcon(icon) # Get screen dimensions and calculate size screen = app.primaryScreen().geometry() dialog_width = int(screen.width() * 0.315) # Reduced from 0.6 to 0.3 dialog_height = int(screen.height() * 0.4) # Reduced from 0.8 to 0.4 dialog.resize(dialog_width, dialog_height) # Main layout main_layout = QVBoxLayout() main_layout.setContentsMargins(20, 20, 20, 20) main_layout.setSpacing(15) # Header title_label = QLabel("Configuration Backup Manager") title_font = QFont() title_font.setPointSize(14) title_font.setBold(True) title_label.setFont(title_font) main_layout.addWidget(title_label) desc_label = QLabel("Select a backup to restore or manage your configuration backups.") desc_label.setStyleSheet("color: gray; font-size: 10pt;") main_layout.addWidget(desc_label) # Info section info_group = QGroupBox("Backup Information") info_layout = QVBoxLayout() info_layout.setContentsMargins(10, 10, 10, 10) info_text = f"šŸ“ Backup Location: {backup_dir}\nšŸ“Š Total Backups: {len(backups)}" info_label = QLabel(info_text) info_label.setStyleSheet("color: white; font-size: 10pt;") info_layout.addWidget(info_label) info_group.setLayout(info_layout) main_layout.addWidget(info_group) # Backup list section list_group = QGroupBox("Available Backups (Newest First)") list_layout = QVBoxLayout() list_layout.setContentsMargins(10, 10, 10, 10) # Create QTreeWidget for better display tree = QTreeWidget() tree.setColumnCount(3) tree.setHeaderLabels(['Date & Time', 'Backup File', 'Size']) tree.setColumnWidth(0, 200) tree.setColumnWidth(1, 300) tree.setColumnWidth(2, 100) tree.setMinimumHeight(300) tree.setSelectionMode(QTreeWidget.SingleSelection) tree.setAlternatingRowColors(True) # Populate tree with backup information backup_items = [] for backup in backups: backup_path = os.path.join(backup_dir, backup) # Extract timestamp from filename try: timestamp_part = backup.replace("config_", "").replace(".json.bak", "") formatted_time = time.strftime("%Y-%m-%d %H:%M:%S", time.strptime(timestamp_part, "%Y%m%d_%H%M%S")) except: formatted_time = "Unknown" # Get file size try: size_bytes = os.path.getsize(backup_path) if size_bytes < 1024: size_str = f"{size_bytes} B" elif size_bytes < 1024 * 1024: size_str = f"{size_bytes // 1024} KB" else: size_str = f"{size_bytes // (1024 * 1024)} MB" except: size_str = "Unknown" # Insert into tree item = QTreeWidgetItem([formatted_time, backup, size_str]) tree.addTopLevelItem(item) backup_items.append((item, backup, formatted_time)) # Select first item by default if backup_items: tree.setCurrentItem(backup_items[0][0]) list_layout.addWidget(tree) list_group.setLayout(list_layout) main_layout.addWidget(list_group) # Action buttons button_group = QGroupBox("Actions") button_main_layout = QVBoxLayout() button_main_layout.setContentsMargins(10, 10, 10, 10) button_main_layout.setSpacing(10) # Button row 1 button_row1 = QHBoxLayout() button_row1.setSpacing(10) # Button row 2 button_row2 = QHBoxLayout() button_row2.setSpacing(10) def get_selected_backup(): """Get currently selected backup from tree""" current_item = tree.currentItem() if not current_item: return None for item, backup_filename, formatted_time in backup_items: if item == current_item: return backup_filename, formatted_time return None def restore_selected(): selected = get_selected_backup() if not selected: msg = QMessageBox(dialog) msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("No Selection") msg.setText("Please select a backup to restore.") msg.setWindowIcon(icon) msg.exec() return selected_backup, formatted_time = selected backup_path = os.path.join(backup_dir, selected_backup) # Confirm restore confirm = QMessageBox(dialog) confirm.setIcon(QMessageBox.Question) confirm.setWindowTitle("Confirm Restore") confirm.setText(f"This will replace your current configuration with the backup from:\n\n" f"{formatted_time}\n{selected_backup}\n\n" f"A backup of your current config will be created first.\n\n" f"Are you sure you want to continue?") confirm.setStandardButtons(QMessageBox.Yes | QMessageBox.No) confirm.setDefaultButton(QMessageBox.No) confirm.setWindowIcon(icon) if confirm.exec() == QMessageBox.Yes: try: # Create backup of current config before restore self._backup_config_file() # Copy backup to config file shutil.copy2(backup_path, CONFIG_FILE) msg = QMessageBox(dialog) msg.setIcon(QMessageBox.Information) msg.setWindowTitle("Restore Complete") msg.setText(f"Configuration restored from: {selected_backup}\n\n" f"The application will now restart for changes to take effect.") msg.setWindowIcon(icon) msg.exec() dialog.close() # Restart application import sys import subprocess subprocess.Popen([sys.executable] + sys.argv) sys.exit(0) except Exception as e: msg = QMessageBox(dialog) msg.setIcon(QMessageBox.Critical) msg.setWindowTitle("Restore Failed") msg.setText(f"Failed to restore backup: {e}") msg.setWindowIcon(icon) msg.exec() def delete_selected(): selected = get_selected_backup() if not selected: msg = QMessageBox(dialog) msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("No Selection") msg.setText("Please select a backup to delete.") msg.setWindowIcon(icon) msg.exec() return selected_backup, formatted_time = selected confirm = QMessageBox(dialog) confirm.setIcon(QMessageBox.Question) confirm.setWindowTitle("Confirm Delete") confirm.setText(f"Delete backup from {formatted_time}?\n\n{selected_backup}\n\n" f"This action cannot be undone.") confirm.setStandardButtons(QMessageBox.Yes | QMessageBox.No) confirm.setDefaultButton(QMessageBox.No) confirm.setWindowIcon(icon) if confirm.exec() == QMessageBox.Yes: try: os.remove(os.path.join(backup_dir, selected_backup)) # Remove from tree current_item = tree.currentItem() if current_item: index = tree.indexOfTopLevelItem(current_item) tree.takeTopLevelItem(index) # Update backup items list backup_items[:] = [(item, backup, time_str) for item, backup, time_str in backup_items if backup != selected_backup] msg = QMessageBox(dialog) msg.setIcon(QMessageBox.Information) msg.setWindowTitle("Deleted") msg.setText("Backup deleted successfully.") msg.setWindowIcon(icon) msg.exec() except Exception as e: msg = QMessageBox(dialog) msg.setIcon(QMessageBox.Critical) msg.setWindowTitle("Delete Failed") msg.setText(f"Failed to delete backup: {e}") msg.setWindowIcon(icon) msg.exec() def create_new_backup(): """Create a new manual backup""" try: self._backup_config_file() msg = QMessageBox(dialog) msg.setIcon(QMessageBox.Information) msg.setWindowTitle("Backup Created") msg.setText("New configuration backup created successfully!") msg.setWindowIcon(icon) msg.exec() # Refresh the dialog dialog.close() self._manual_restore_config() # Reopen with updated list except Exception as e: msg = QMessageBox(dialog) msg.setIcon(QMessageBox.Critical) msg.setWindowTitle("Backup Failed") msg.setText(f"Failed to create backup: {e}") msg.setWindowIcon(icon) msg.exec() def open_backup_folder(): """Open backup folder in file explorer""" self._open_backup_folder() # Primary action buttons (Row 1) restore_btn = QPushButton("āœ… Restore Selected") restore_btn.setMinimumWidth(180) restore_btn.setMinimumHeight(35) restore_btn.clicked.connect(restore_selected) restore_btn.setStyleSheet(""" QPushButton { background-color: #28a745; color: white; padding: 8px 20px; font-size: 11pt; font-weight: bold; border-radius: 4px; } QPushButton:hover { background-color: #218838; } """) button_row1.addWidget(restore_btn) create_btn = QPushButton("šŸ’¾ Create New Backup") create_btn.setMinimumWidth(180) create_btn.setMinimumHeight(35) create_btn.clicked.connect(create_new_backup) create_btn.setStyleSheet(""" QPushButton { background-color: #007bff; color: white; padding: 8px 20px; font-size: 11pt; font-weight: bold; border-radius: 4px; } QPushButton:hover { background-color: #0056b3; } """) button_row1.addWidget(create_btn) folder_btn = QPushButton("šŸ“ Open Folder") folder_btn.setMinimumWidth(180) folder_btn.setMinimumHeight(35) folder_btn.clicked.connect(open_backup_folder) folder_btn.setStyleSheet(""" QPushButton { background-color: #17a2b8; color: white; padding: 8px 20px; font-size: 11pt; font-weight: bold; border-radius: 4px; } QPushButton:hover { background-color: #117a8b; } """) button_row1.addWidget(folder_btn) button_row1.addStretch() # Secondary action buttons (Row 2) delete_btn = QPushButton("šŸ—‘ļø Delete Selected") delete_btn.setMinimumWidth(180) delete_btn.setMinimumHeight(35) delete_btn.clicked.connect(delete_selected) delete_btn.setStyleSheet(""" QPushButton { background-color: #dc3545; color: white; padding: 8px 20px; font-size: 11pt; font-weight: bold; border-radius: 4px; } QPushButton:hover { background-color: #c82333; } """) button_row2.addWidget(delete_btn) button_row2.addStretch() close_btn = QPushButton("āŒ Close") close_btn.setMinimumWidth(120) close_btn.setMinimumHeight(35) close_btn.clicked.connect(dialog.close) close_btn.setStyleSheet(""" QPushButton { background-color: #6c757d; color: white; padding: 8px 20px; font-size: 11pt; font-weight: bold; border-radius: 4px; } QPushButton:hover { background-color: #5a6268; } """) button_row2.addWidget(close_btn) button_main_layout.addLayout(button_row1) button_main_layout.addLayout(button_row2) button_group.setLayout(button_main_layout) main_layout.addWidget(button_group) # Set dialog layout and show dialog.setLayout(main_layout) # Run dialog in separate thread to avoid GIL conflicts import threading def run_dialog(): dialog.exec() thread = threading.Thread(target=run_dialog, daemon=True) thread.start() # Keep reference to prevent garbage collection self._backup_dialog = dialog except Exception as e: msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setWindowTitle("Error") msg.setText(f"Failed to open backup restore dialog: {e}") if icon: msg.setWindowIcon(icon) msg.exec()