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 core.pbp_app_config import AppConfig
|
||||||
from pyimage_ui.password_dialog import PasswordDialog
|
from pyimage_ui.password_dialog import PasswordDialog
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
class EncryptionManager:
|
class EncryptionManager:
|
||||||
@@ -26,6 +27,31 @@ class EncryptionManager:
|
|||||||
self.service_id = "py-backup-encryption"
|
self.service_id = "py-backup-encryption"
|
||||||
self.mounted_destinations = set()
|
self.mounted_destinations = set()
|
||||||
self.password_cache = {}
|
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]:
|
def get_password_from_keyring(self, username: str) -> Optional[str]:
|
||||||
try:
|
try:
|
||||||
@@ -252,7 +278,10 @@ mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\"
|
|||||||
mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\"
|
mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\"
|
||||||
{chown_cmd}
|
{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):
|
def unmount_and_reset_owner(self, base_dest_path: str, force_unmap=False):
|
||||||
username = os.path.basename(base_dest_path.rstrip('/'))
|
username = os.path.basename(base_dest_path.rstrip('/'))
|
||||||
@@ -268,11 +297,12 @@ mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\"
|
|||||||
|
|
||||||
script = f"""
|
script = f"""
|
||||||
chown root:root \"{mount_point}\" || true
|
chown root:root \"{mount_point}\" || true
|
||||||
umount -l \"{mount_point}\" || true
|
umount -l \"{mount_point}\"
|
||||||
cryptsetup luksClose {mapper_name} || true
|
cryptsetup luksClose {mapper_name}
|
||||||
"""
|
"""
|
||||||
password = self.password_cache.get(username)
|
password = self.password_cache.get(username)
|
||||||
self._execute_as_root(script, password)
|
self._execute_as_root(script, password)
|
||||||
|
self.remove_from_lock_file(base_dest_path)
|
||||||
|
|
||||||
if base_dest_path in self.mounted_destinations:
|
if base_dest_path in self.mounted_destinations:
|
||||||
self.mounted_destinations.remove(base_dest_path)
|
self.mounted_destinations.remove(base_dest_path)
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ class AppConfig:
|
|||||||
|
|
||||||
# --- Core Paths ---
|
# --- Core Paths ---
|
||||||
BASE_DIR: Path = Path.home()
|
BASE_DIR: Path = Path.home()
|
||||||
CONFIG_DIR: Path = BASE_DIR / ".config/lx_pyimage"
|
APP_DIR: Path = BASE_DIR / ".config/py_backup"
|
||||||
SETTINGS_FILE: Path = CONFIG_DIR / "settings.json"
|
SETTINGS_FILE: Path = APP_DIR / "pbp_settings.json"
|
||||||
GENERATED_EXCLUDE_LIST_PATH: Path = CONFIG_DIR / "rsync-generated-excludes.conf"
|
GENERATED_EXCLUDE_LIST_PATH: Path = APP_DIR / "rsync-generated-excludes.conf"
|
||||||
USER_EXCLUDE_LIST_PATH: Path = CONFIG_DIR / \
|
USER_EXCLUDE_LIST_PATH: Path = APP_DIR / "user_excludes.txt"
|
||||||
"rsync-user-excludes.conf" # Single file
|
MANUAL_EXCLUDE_LIST_PATH: Path = APP_DIR / "manual_excludes.txt"
|
||||||
MANUAL_EXCLUDE_LIST_PATH: Path = CONFIG_DIR / \
|
LOG_FILE_PATH: Path = APP_DIR / "py-backup.log"
|
||||||
"rsync-manual-excludes.conf" # Single file
|
LOCK_FILE_PATH: Path = APP_DIR / "pybackup.lock"
|
||||||
APP_ICONS_DIR: Path = Path(__file__).parent / "lx-icons"
|
APP_ICONS_DIR: Path = Path(__file__).parent / "lx-icons"
|
||||||
|
|
||||||
# --- Application Info ---
|
# --- Application Info ---
|
||||||
@@ -125,8 +125,8 @@ class AppConfig:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def ensure_directories(cls) -> None:
|
def ensure_directories(cls) -> None:
|
||||||
"""Ensures that all required application directories exist."""
|
"""Ensures that all required application directories exist."""
|
||||||
if not cls.CONFIG_DIR.exists():
|
if not cls.APP_DIR.exists():
|
||||||
cls.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
cls.APP_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
# In the future, we can create a default settings file here
|
# In the future, we can create a default settings file here
|
||||||
|
|
||||||
# Generate/update the final exclude list on every start
|
# 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
|
import datetime
|
||||||
from queue import Queue, Empty
|
from queue import Queue, Empty
|
||||||
import shutil
|
import shutil
|
||||||
|
import signal
|
||||||
|
|
||||||
from shared_libs.log_window import LogWindow
|
from shared_libs.log_window import LogWindow
|
||||||
from shared_libs.logger import app_logger
|
from shared_libs.logger import app_logger
|
||||||
@@ -75,6 +76,10 @@ class MainApplication(tk.Tk):
|
|||||||
|
|
||||||
self.backup_manager = BackupManager(app_logger, self)
|
self.backup_manager = BackupManager(app_logger, self)
|
||||||
self.queue = Queue()
|
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.image_manager = IconManager()
|
||||||
|
|
||||||
self.config_manager = ConfigManager(AppConfig.SETTINGS_FILE)
|
self.config_manager = ConfigManager(AppConfig.SETTINGS_FILE)
|
||||||
@@ -336,6 +341,14 @@ class MainApplication(tk.Tk):
|
|||||||
self.destination_total_bytes = total
|
self.destination_total_bytes = total
|
||||||
self.destination_used_bytes = used
|
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'):
|
if hasattr(self, 'header_frame'):
|
||||||
self.header_frame.refresh_status()
|
self.header_frame.refresh_status()
|
||||||
|
|
||||||
@@ -733,6 +746,37 @@ class MainApplication(tk.Tk):
|
|||||||
def quit(self):
|
def quit(self):
|
||||||
self.on_closing()
|
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):
|
def update_backup_options_from_config(self):
|
||||||
force_full = self.config_manager.get_setting(
|
force_full = self.config_manager.get_setting(
|
||||||
"force_full_backup", False)
|
"force_full_backup", False)
|
||||||
|
|||||||
Reference in New Issue
Block a user