diff --git a/core/encryption_manager.py b/core/encryption_manager.py index 2ce9464..7a36e87 100644 --- a/core/encryption_manager.py +++ b/core/encryption_manager.py @@ -13,6 +13,7 @@ from typing import Optional, List, Tuple from core.pbp_app_config import AppConfig from pyimage_ui.password_dialog import PasswordDialog +import json class EncryptionManager: @@ -26,6 +27,31 @@ class EncryptionManager: self.service_id = "py-backup-encryption" self.mounted_destinations = set() self.password_cache = {} + self.lock_file = AppConfig.LOCK_FILE_PATH + + def _write_lock_file(self, data): + with open(self.lock_file, 'w') as f: + json.dump(data, f) + + def _read_lock_file(self): + if not self.lock_file.exists(): + return [] + with open(self.lock_file, 'r') as f: + try: + return json.load(f) + except json.JSONDecodeError: + return [] + + def add_to_lock_file(self, base_path, mapper_name): + locks = self._read_lock_file() + if not any(lock['base_path'] == base_path for lock in locks): + locks.append({"base_path": base_path, "mapper_name": mapper_name}) + self._write_lock_file(locks) + + def remove_from_lock_file(self, base_path): + locks = self._read_lock_file() + updated_locks = [lock for lock in locks if lock['base_path'] != base_path] + self._write_lock_file(updated_locks) def get_password_from_keyring(self, username: str) -> Optional[str]: try: @@ -252,7 +278,10 @@ mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\" mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\" {chown_cmd} """ - return self._execute_as_root(script, password) + if self._execute_as_root(script, password): + self.add_to_lock_file(base_dest_path, mapper_name) + return True + return False def unmount_and_reset_owner(self, base_dest_path: str, force_unmap=False): username = os.path.basename(base_dest_path.rstrip('/')) @@ -268,11 +297,12 @@ mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\" script = f""" chown root:root \"{mount_point}\" || true - umount -l \"{mount_point}\" || true - cryptsetup luksClose {mapper_name} || true + umount -l \"{mount_point}\" + cryptsetup luksClose {mapper_name} """ password = self.password_cache.get(username) self._execute_as_root(script, password) + self.remove_from_lock_file(base_dest_path) if base_dest_path in self.mounted_destinations: self.mounted_destinations.remove(base_dest_path) diff --git a/core/pbp_app_config.py b/core/pbp_app_config.py index 83984a6..a82e3aa 100644 --- a/core/pbp_app_config.py +++ b/core/pbp_app_config.py @@ -10,13 +10,13 @@ class AppConfig: # --- Core Paths --- BASE_DIR: Path = Path.home() - CONFIG_DIR: Path = BASE_DIR / ".config/lx_pyimage" - SETTINGS_FILE: Path = CONFIG_DIR / "settings.json" - GENERATED_EXCLUDE_LIST_PATH: Path = CONFIG_DIR / "rsync-generated-excludes.conf" - USER_EXCLUDE_LIST_PATH: Path = CONFIG_DIR / \ - "rsync-user-excludes.conf" # Single file - MANUAL_EXCLUDE_LIST_PATH: Path = CONFIG_DIR / \ - "rsync-manual-excludes.conf" # Single file + APP_DIR: Path = BASE_DIR / ".config/py_backup" + SETTINGS_FILE: Path = APP_DIR / "pbp_settings.json" + GENERATED_EXCLUDE_LIST_PATH: Path = APP_DIR / "rsync-generated-excludes.conf" + USER_EXCLUDE_LIST_PATH: Path = APP_DIR / "user_excludes.txt" + MANUAL_EXCLUDE_LIST_PATH: Path = APP_DIR / "manual_excludes.txt" + LOG_FILE_PATH: Path = APP_DIR / "py-backup.log" + LOCK_FILE_PATH: Path = APP_DIR / "pybackup.lock" APP_ICONS_DIR: Path = Path(__file__).parent / "lx-icons" # --- Application Info --- @@ -125,8 +125,8 @@ class AppConfig: @classmethod def ensure_directories(cls) -> None: """Ensures that all required application directories exist.""" - if not cls.CONFIG_DIR.exists(): - cls.CONFIG_DIR.mkdir(parents=True, exist_ok=True) + if not cls.APP_DIR.exists(): + cls.APP_DIR.mkdir(parents=True, exist_ok=True) # In the future, we can create a default settings file here # Generate/update the final exclude list on every start diff --git a/main_app.py b/main_app.py index e10ffb8..67b620f 100644 --- a/main_app.py +++ b/main_app.py @@ -5,6 +5,7 @@ import os import datetime from queue import Queue, Empty import shutil +import signal from shared_libs.log_window import LogWindow from shared_libs.logger import app_logger @@ -75,6 +76,10 @@ class MainApplication(tk.Tk): self.backup_manager = BackupManager(app_logger, self) self.queue = Queue() + + self._check_for_stale_mounts() + signal.signal(signal.SIGTERM, self._handle_signal) + signal.signal(signal.SIGINT, self._handle_signal) self.image_manager = IconManager() self.config_manager = ConfigManager(AppConfig.SETTINGS_FILE) @@ -336,6 +341,14 @@ class MainApplication(tk.Tk): self.destination_total_bytes = total self.destination_used_bytes = used + # If the destination is already mounted from a previous session, + # adopt it into the current session's state so it can be cleaned up properly. + if self.backup_manager.encryption_manager.is_mounted(backup_dest_path): + app_logger.log( + f"Adopting pre-existing mount for {backup_dest_path} into session.") + self.backup_manager.encryption_manager.mounted_destinations.add( + backup_dest_path) + if hasattr(self, 'header_frame'): self.header_frame.refresh_status() @@ -733,6 +746,37 @@ class MainApplication(tk.Tk): def quit(self): self.on_closing() + def _check_for_stale_mounts(self): + app_logger.log("Checking for stale mounts from previous sessions...") + try: + locks = self.backup_manager.encryption_manager._read_lock_file() + if not locks: + app_logger.log( + "No lock file found or lock file is empty. Clean state.") + return + + stale_mounts_found = False + for lock in locks: + mapper_path = f"/dev/mapper/{lock['mapper_name']}" + if os.path.exists(mapper_path): + stale_mounts_found = True + app_logger.log( + f"Found stale mount: {lock['mapper_name']} for path {lock['base_path']}. Attempting to close.") + self.backup_manager.encryption_manager.unmount_and_reset_owner( + lock['base_path'], force_unmap=True) + + if not stale_mounts_found: + app_logger.log("No stale mounts detected.") + if locks: + self.backup_manager.encryption_manager._write_lock_file([]) + + except Exception as e: + app_logger.log(f"Error during stale mount check: {e}") + + def _handle_signal(self, signum, frame): + app_logger.log(f"Received signal {signum}. Cleaning up and exiting.") + self.on_closing() + def update_backup_options_from_config(self): force_full = self.config_manager.get_setting( "force_full_backup", False)