secure unmount on close the app
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
44
main_app.py
44
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)
|
||||
|
||||
Reference in New Issue
Block a user