Compare commits
5 Commits
4a96fd1547
...
aa604efc86
Author | SHA1 | Date | |
---|---|---|---|
aa604efc86 | |||
d5e08853a5 | |||
9983a9769f | |||
9682e41710 | |||
64374c221e |
@@ -80,8 +80,9 @@ class BackupManager:
|
||||
"""Helper function to construct the path to a specific backup profile directory."""
|
||||
pybackup_dir = os.path.join(base_dest_path, "pybackup")
|
||||
if is_encrypted:
|
||||
profile_name = "system" if is_system else source_name
|
||||
base_data_dir = self.encryption_manager.get_mount_point(
|
||||
base_dest_path)
|
||||
base_dest_path, profile_name)
|
||||
else:
|
||||
base_data_dir = os.path.join(pybackup_dir, "unencrypted")
|
||||
|
||||
@@ -124,8 +125,9 @@ class BackupManager:
|
||||
|
||||
mount_point = None
|
||||
if is_encrypted:
|
||||
profile_name = "system" if is_system else source_name
|
||||
mount_point = self.encryption_manager.prepare_encrypted_destination(
|
||||
dest_path, is_system, source_size, queue)
|
||||
dest_path, profile_name, is_system, source_size, queue)
|
||||
|
||||
if not mount_point:
|
||||
self.logger.log(
|
||||
@@ -411,11 +413,14 @@ class BackupManager:
|
||||
self.logger.log(
|
||||
f"Metadata file found for {backup_dir_name} but data directory not found at {full_path}. Skipping.")
|
||||
continue
|
||||
if not self.encryption_manager.is_mounted(base_dest_path):
|
||||
|
||||
profile_name = "system" if is_system else source_name
|
||||
if not self.encryption_manager.is_mounted(base_dest_path, profile_name):
|
||||
self.logger.log(
|
||||
f"Mounting {base_dest_path} to check for encrypted backup data...")
|
||||
f"Mounting container for profile {profile_name} at {base_dest_path} to check for backup data...")
|
||||
self.encryption_manager.prepare_encrypted_destination(
|
||||
base_dest_path, is_system, 0, self.app.queue if self.app else None)
|
||||
base_dest_path, profile_name, is_system, 0, self.app.queue if self.app else None)
|
||||
|
||||
if not os.path.isdir(full_path):
|
||||
self.logger.log(
|
||||
f"Data directory {full_path} still not found after mount attempt. Skipping.")
|
||||
@@ -673,7 +678,7 @@ class BackupManager:
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def _run_delete(self, path_to_delete: str, is_encrypted: bool, is_system: bool, base_dest_path: str, queue, password: Optional[str]):
|
||||
def _run_delete(self, path_to_delete: str, is_encrypted: bool, is_system: bool, source_name: str, base_dest_path: str, queue, password: Optional[str]):
|
||||
try:
|
||||
backup_dir_name = os.path.basename(path_to_delete.rstrip('/'))
|
||||
metadata_file_path = os.path.join(
|
||||
@@ -682,20 +687,17 @@ class BackupManager:
|
||||
if is_encrypted:
|
||||
self.logger.log(
|
||||
f"Starting encrypted deletion for {path_to_delete}")
|
||||
profile_name = "system" if is_system else source_name
|
||||
mount_point = self.encryption_manager.get_mount_point(
|
||||
base_dest_path)
|
||||
if not mount_point or not self.encryption_manager.is_mounted(base_dest_path):
|
||||
if password:
|
||||
mount_point = self.encryption_manager.mount_for_deletion(
|
||||
base_dest_path, is_system, password)
|
||||
else:
|
||||
self.logger.log(
|
||||
"Password not provided for encrypted deletion.")
|
||||
|
||||
if not mount_point:
|
||||
self.logger.log("Failed to unlock container for deletion.")
|
||||
queue.put(('deletion_complete', False))
|
||||
return
|
||||
base_dest_path, profile_name)
|
||||
|
||||
if not self.encryption_manager.is_mounted(base_dest_path, profile_name):
|
||||
self.logger.log(f"Container for profile {profile_name} not mounted. Mounting for deletion.")
|
||||
# Note: mount_for_deletion is not profile-aware yet, we are calling _open_and_mount directly
|
||||
if not self.encryption_manager._open_and_mount(base_dest_path, profile_name, is_system, password_override=password):
|
||||
self.logger.log("Failed to unlock container for deletion.")
|
||||
queue.put(('deletion_complete', False))
|
||||
return
|
||||
|
||||
internal_path_to_delete = os.path.join(
|
||||
mount_point, os.path.basename(os.path.dirname(path_to_delete)), backup_dir_name)
|
||||
|
@@ -1,4 +1,3 @@
|
||||
|
||||
import keyring
|
||||
import keyring.errors
|
||||
from keyring.backends import SecretService
|
||||
@@ -39,16 +38,17 @@ class EncryptionManager:
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
|
||||
def add_to_lock_file(self, base_path, mapper_name):
|
||||
def add_to_lock_file(self, base_path, profile_name, 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})
|
||||
if not any(l['mapper_name'] == mapper_name for l in locks):
|
||||
locks.append(
|
||||
{"base_path": base_path, "profile_name": profile_name, "mapper_name": mapper_name})
|
||||
self._write_lock_file(locks)
|
||||
|
||||
def remove_from_lock_file(self, base_path):
|
||||
def remove_from_lock_file(self, mapper_name_to_remove: str):
|
||||
locks = self._read_lock_file()
|
||||
updated_locks = [
|
||||
lock for lock in locks if lock['base_path'] != base_path]
|
||||
updated_locks = [l for l in locks if l['mapper_name']
|
||||
!= mapper_name_to_remove]
|
||||
self._write_lock_file(updated_locks)
|
||||
|
||||
def get_password_from_keyring(self, username: str) -> Optional[str]:
|
||||
@@ -86,18 +86,19 @@ class EncryptionManager:
|
||||
self.set_password_in_keyring(username, password)
|
||||
return password
|
||||
|
||||
def get_container_path(self, base_dest_path: str) -> str:
|
||||
"""Returns the path for the LUKS container file itself."""
|
||||
def get_container_path(self, base_dest_path: str, profile_name: str) -> str:
|
||||
pybackup_dir = os.path.join(base_dest_path, "pybackup")
|
||||
return os.path.join(pybackup_dir, "pybackup_encrypted.luks")
|
||||
container_filename = f"pybackup_encrypted_{profile_name}.luks"
|
||||
return os.path.join(pybackup_dir, container_filename)
|
||||
|
||||
def get_key_file_path(self, base_dest_path: str) -> str:
|
||||
def get_key_file_path(self, base_dest_path: str, profile_name: str) -> str:
|
||||
pybackup_dir = os.path.join(base_dest_path, "pybackup")
|
||||
return os.path.join(pybackup_dir, "luks.keyfile")
|
||||
key_filename = f"luks_{profile_name}.keyfile"
|
||||
return os.path.join(pybackup_dir, key_filename)
|
||||
|
||||
def create_and_add_key_file(self, base_dest_path: str, password: str) -> Optional[str]:
|
||||
key_file_path = self.get_key_file_path(base_dest_path)
|
||||
container_path = self.get_container_path(base_dest_path)
|
||||
def create_and_add_key_file(self, base_dest_path: str, profile_name: str, password: str) -> Optional[str]:
|
||||
key_file_path = self.get_key_file_path(base_dest_path, profile_name)
|
||||
container_path = self.get_container_path(base_dest_path, profile_name)
|
||||
try:
|
||||
with open(key_file_path, 'wb') as f:
|
||||
f.write(os.urandom(4096))
|
||||
@@ -119,67 +120,50 @@ class EncryptionManager:
|
||||
os.remove(key_file_path)
|
||||
return None
|
||||
|
||||
def _get_password_or_key_cmd(self, base_dest_path: str, username: str) -> Tuple[str, Optional[str]]:
|
||||
# 1. Check cache and keyring (without triggering dialog)
|
||||
password = self.password_cache.get(
|
||||
username) or self.get_password_from_keyring(username)
|
||||
def _get_password_or_key_cmd(self, base_dest_path: str, profile_name: str, username: str) -> Tuple[str, Optional[str]]:
|
||||
password = self.password_cache.get(username)
|
||||
if password:
|
||||
self.logger.log(
|
||||
"Using password from cache or keyring for LUKS operation.")
|
||||
self.password_cache[username] = password # ensure it's cached
|
||||
return "-", password
|
||||
|
||||
# 2. Check for key file
|
||||
key_file_path = self.get_key_file_path(base_dest_path)
|
||||
password = self.get_password_from_keyring(username)
|
||||
if password:
|
||||
self.password_cache[username] = password
|
||||
return "-", password
|
||||
|
||||
key_file_path = self.get_key_file_path(base_dest_path, profile_name)
|
||||
if os.path.exists(key_file_path):
|
||||
self.logger.log(
|
||||
f"Using key file for LUKS operation: {key_file_path}")
|
||||
return f'--key-file "{key_file_path}"'
|
||||
|
||||
# 3. If nothing found, prompt for password
|
||||
self.logger.log(
|
||||
"No password in keyring and no keyfile found. Prompting user.")
|
||||
# This will now definitely open the dialog
|
||||
password = self.get_password(username, confirm=False)
|
||||
if not password:
|
||||
return "", None
|
||||
return "-", password
|
||||
|
||||
def is_encrypted(self, base_dest_path: str) -> bool:
|
||||
return os.path.exists(self.get_container_path(base_dest_path))
|
||||
def is_encrypted(self, base_dest_path: str, profile_name: str) -> bool:
|
||||
return os.path.exists(self.get_container_path(base_dest_path, profile_name))
|
||||
|
||||
def get_mount_point(self, base_dest_path: str) -> str:
|
||||
"""Constructs the unique, static mount point path for a given destination."""
|
||||
return os.path.join(base_dest_path, "pybackup", "encrypted")
|
||||
def get_mount_point(self, base_dest_path: str, profile_name: str) -> str:
|
||||
return os.path.join(base_dest_path, "pybackup", f"encrypted_{profile_name.lower()}")
|
||||
|
||||
def is_mounted(self, base_dest_path: str) -> bool:
|
||||
mount_point = self.get_mount_point(base_dest_path)
|
||||
return os.path.ismount(mount_point) or base_dest_path in self.mounted_destinations
|
||||
def is_mounted(self, base_dest_path: str, profile_name: str) -> bool:
|
||||
mount_point = self.get_mount_point(base_dest_path, profile_name)
|
||||
return os.path.ismount(mount_point) or (base_dest_path, profile_name) in self.mounted_destinations
|
||||
|
||||
def mount_for_deletion(self, base_dest_path: str, is_system: bool, password: str) -> Optional[str]:
|
||||
self.logger.log("Mounting container for deletion operation.")
|
||||
if self._open_and_mount(base_dest_path, is_system, password):
|
||||
mount_point = self.get_mount_point(base_dest_path)
|
||||
self.mounted_destinations.add(base_dest_path)
|
||||
return mount_point
|
||||
self.logger.log("Failed to mount container for deletion.")
|
||||
return None
|
||||
|
||||
def prepare_encrypted_destination(self, base_dest_path: str, is_system: bool, source_size: int, queue) -> Optional[str]:
|
||||
container_path = self.get_container_path(base_dest_path)
|
||||
def prepare_encrypted_destination(self, base_dest_path: str, profile_name: str, is_system: bool, source_size: int, queue) -> Optional[str]:
|
||||
container_path = self.get_container_path(base_dest_path, profile_name)
|
||||
if os.path.exists(container_path):
|
||||
return self._handle_existing_container(base_dest_path, is_system, source_size, queue)
|
||||
return self._handle_existing_container(base_dest_path, profile_name, is_system, source_size, queue)
|
||||
else:
|
||||
return self._handle_new_container(base_dest_path, is_system, source_size, queue)
|
||||
|
||||
def _handle_existing_container(self, base_dest_path: str, is_system: bool, source_size: int, queue) -> Optional[str]:
|
||||
self.logger.log("Handling existing container.")
|
||||
return self._handle_new_container(base_dest_path, profile_name, is_system, source_size, queue)
|
||||
|
||||
def _handle_existing_container(self, base_dest_path: str, profile_name: str, is_system: bool, source_size: int, queue) -> Optional[str]:
|
||||
self.logger.log(
|
||||
f"Handling existing container for profile: {profile_name}")
|
||||
username = os.path.basename(base_dest_path.rstrip('/'))
|
||||
mount_point = self.get_mount_point(base_dest_path)
|
||||
mount_point = self.get_mount_point(base_dest_path, profile_name)
|
||||
|
||||
if not self.is_mounted(base_dest_path):
|
||||
if not self._open_and_mount(base_dest_path, is_system):
|
||||
if not self.is_mounted(base_dest_path, profile_name):
|
||||
if not self._open_and_mount(base_dest_path, profile_name, is_system):
|
||||
self.logger.log("Failed to mount container for size check.")
|
||||
return None
|
||||
|
||||
@@ -188,152 +172,166 @@ class EncryptionManager:
|
||||
|
||||
if required_space > free_space:
|
||||
self.logger.log(
|
||||
f"Resize needed. Free: {free_space}, Required: {required_space}")
|
||||
queue.put(('status_update', "Container zu klein. Vergrößere..."))
|
||||
f"Resize needed for {profile_name}. Free: {free_space}, Required: {required_space}")
|
||||
queue.put(
|
||||
('status_update', f"Container für {profile_name} zu klein. Vergrößere..."))
|
||||
|
||||
key_or_pass_arg, password = self._get_password_or_key_cmd(
|
||||
base_dest_path, profile_name, username)
|
||||
if not key_or_pass_arg:
|
||||
return None
|
||||
|
||||
current_total = shutil.disk_usage(mount_point).total
|
||||
needed_additional = required_space - free_space
|
||||
new_total_size = current_total + needed_additional
|
||||
new_total_size = math.ceil(new_total_size / 4096) * 4096
|
||||
|
||||
container_path = self.get_container_path(base_dest_path)
|
||||
mapper_name = f"pybackup_luks_{username}"
|
||||
container_path = self.get_container_path(
|
||||
base_dest_path, profile_name)
|
||||
mapper_name = f"pybackup_luks_{username}_{profile_name}"
|
||||
chown_cmd = self._get_chown_command(mount_point, is_system)
|
||||
|
||||
key_or_pass_arg, password = self._get_password_or_key_cmd(
|
||||
base_dest_path, username)
|
||||
if not key_or_pass_arg:
|
||||
return None
|
||||
luks_open_cmd = f'echo -n "$LUKSPASS" | cryptsetup luksOpen "{container_path}" {mapper_name} {key_or_pass_arg}' if password else f'cryptsetup luksOpen "{container_path}" {mapper_name} {key_or_pass_arg}'
|
||||
|
||||
resize_script = f"""
|
||||
resize_script = f"""set -e
|
||||
# Unmount cleanly first
|
||||
umount -l \"{mount_point}\" || true
|
||||
cryptsetup luksClose {mapper_name} || true
|
||||
umount "{mount_point}"
|
||||
cryptsetup luksClose {mapper_name}
|
||||
|
||||
# Resize container file
|
||||
truncate -s {int(new_total_size)} \"{container_path}\"
|
||||
truncate -s {int(new_total_size)} "{container_path}"
|
||||
sleep 1
|
||||
|
||||
# Re-open, check, and resize filesystem
|
||||
{luks_open_cmd}
|
||||
e2fsck -fy \"/dev/mapper/{mapper_name}\"
|
||||
resize2fs \"/dev/mapper/{mapper_name}\"
|
||||
cryptsetup resize {mapper_name}
|
||||
e2fsck -fy "/dev/mapper/{mapper_name}"
|
||||
resize2fs "/dev/mapper/{mapper_name}"
|
||||
|
||||
# Now mount it
|
||||
mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\"
|
||||
mount "/dev/mapper/{mapper_name}" "{mount_point}"
|
||||
{chown_cmd}
|
||||
"""
|
||||
|
||||
if not self._execute_as_root(resize_script, password):
|
||||
self.logger.log("Failed to execute resize and remount script.")
|
||||
return None
|
||||
self._open_and_mount(
|
||||
base_dest_path, profile_name, is_system, password_override=password)
|
||||
return mount_point
|
||||
|
||||
if not self.is_mounted(base_dest_path):
|
||||
self.logger.log(
|
||||
"CRITICAL: Mount failed after resize script, but script reported success. Aborting.")
|
||||
return None
|
||||
|
||||
self.mounted_destinations.add(base_dest_path)
|
||||
self.mounted_destinations.add((base_dest_path, profile_name))
|
||||
return mount_point
|
||||
|
||||
def _handle_new_container(self, base_dest_path: str, is_system: bool, source_size: int, queue) -> Optional[str]:
|
||||
self.logger.log("Handling new container creation.")
|
||||
def _handle_new_container(self, base_dest_path: str, profile_name: str, is_system: bool, source_size: int, queue) -> Optional[str]:
|
||||
self.logger.log(
|
||||
f"Handling new container creation for profile: {profile_name}")
|
||||
size_gb = math.ceil((source_size * 1.2) / (1024**3)) + 5
|
||||
|
||||
username = os.path.basename(base_dest_path.rstrip('/'))
|
||||
password = self.get_password(username, confirm=True)
|
||||
if not password:
|
||||
return None
|
||||
|
||||
container_path = self.get_container_path(base_dest_path)
|
||||
mount_point = self.get_mount_point(base_dest_path)
|
||||
mapper_name = f"pybackup_luks_{username}"
|
||||
container_path = self.get_container_path(base_dest_path, profile_name)
|
||||
mount_point = self.get_mount_point(base_dest_path, profile_name)
|
||||
mapper_name = f"pybackup_luks_{username}_{profile_name}"
|
||||
chown_cmd = self._get_chown_command(mount_point, is_system)
|
||||
|
||||
script = f"""
|
||||
mkdir -p \"{os.path.dirname(container_path)}\"\n mkdir -p \"{mount_point}\"\n truncate -s {int(size_gb)}G \"{container_path}\"\n echo -n \"$LUKSPASS\" | cryptsetup luksFormat \"{container_path}\" -
|
||||
echo -n \"$LUKSPASS\" | cryptsetup luksOpen \"{container_path}\" {mapper_name} -
|
||||
mkfs.ext4 \"/dev/mapper/{mapper_name}\"
|
||||
mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\"\n {chown_cmd}\n """
|
||||
script = f"""set -e
|
||||
mkdir -p "{os.path.dirname(container_path)}"
|
||||
mkdir -p "{mount_point}"
|
||||
truncate -s {int(size_gb)}G "{container_path}"
|
||||
echo -n "$LUKSPASS" | cryptsetup luksFormat "{container_path}" -
|
||||
echo -n "$LUKSPASS" | cryptsetup luksOpen "{container_path}" {mapper_name} -
|
||||
mkfs.ext4 "/dev/mapper/{mapper_name}"
|
||||
sleep 1
|
||||
mount "/dev/mapper/{mapper_name}" "{mount_point}"
|
||||
{chown_cmd}"""
|
||||
if not self._execute_as_root(script, password):
|
||||
return None
|
||||
|
||||
self.mounted_destinations.add(base_dest_path)
|
||||
self.mounted_destinations.add((base_dest_path, profile_name))
|
||||
return mount_point
|
||||
|
||||
def _open_and_mount(self, base_dest_path: str, is_system: bool, password_override: Optional[str] = None) -> bool:
|
||||
def _open_and_mount(self, base_dest_path: str, profile_name: str, is_system: bool, password_override: Optional[str] = None) -> bool:
|
||||
username = os.path.basename(base_dest_path.rstrip('/'))
|
||||
|
||||
key_or_pass_arg, password = "", None
|
||||
if password_override:
|
||||
password = password_override
|
||||
key_or_pass_arg = "-"
|
||||
else:
|
||||
key_or_pass_arg, password = self._get_password_or_key_cmd(
|
||||
base_dest_path, username)
|
||||
base_dest_path, profile_name, username)
|
||||
|
||||
if not key_or_pass_arg:
|
||||
return False
|
||||
|
||||
container_path = self.get_container_path(base_dest_path)
|
||||
mount_point = self.get_mount_point(base_dest_path)
|
||||
mapper_name = f"pybackup_luks_{username}"
|
||||
container_path = self.get_container_path(base_dest_path, profile_name)
|
||||
mount_point = self.get_mount_point(base_dest_path, profile_name)
|
||||
mapper_name = f"pybackup_luks_{username}_{profile_name}"
|
||||
chown_cmd = self._get_chown_command(mount_point, is_system)
|
||||
luks_open_cmd = f'echo -n "$LUKSPASS" | cryptsetup luksOpen "{container_path}" {mapper_name} {key_or_pass_arg}' if password else f'cryptsetup luksOpen "{container_path}" {mapper_name} {key_or_pass_arg}'
|
||||
|
||||
luks_open_cmd = f'echo -n \"$LUKSPASS\" | cryptsetup luksOpen \"{container_path}\" {mapper_name} {key_or_pass_arg}' if password else f'cryptsetup luksOpen \"{container_path}\" {mapper_name} {key_or_pass_arg}'
|
||||
|
||||
script = f"""
|
||||
umount -l \"{mount_point}\" || true
|
||||
script = f"""set -e
|
||||
umount "{mount_point}" || true
|
||||
cryptsetup luksClose {mapper_name} || true
|
||||
mkdir -p \"{mount_point}\"
|
||||
mkdir -p "{mount_point}"
|
||||
{luks_open_cmd}
|
||||
mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\"
|
||||
{chown_cmd}
|
||||
"""
|
||||
mount "/dev/mapper/{mapper_name}" "{mount_point}"
|
||||
{chown_cmd}"""
|
||||
if self._execute_as_root(script, password):
|
||||
self.add_to_lock_file(base_dest_path, mapper_name)
|
||||
self.add_to_lock_file(base_dest_path, profile_name, mapper_name)
|
||||
if self.app and hasattr(self.app, 'header_frame'):
|
||||
self.app.header_frame.refresh_status()
|
||||
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, profile_name: str, force_unmap=False) -> bool:
|
||||
username = os.path.basename(base_dest_path.rstrip('/'))
|
||||
mapper_name = f"pybackup_luks_{username}"
|
||||
if not os.path.exists(f"/dev/mapper/{mapper_name}") and not self.is_mounted(base_dest_path):
|
||||
mapper_name = f"pybackup_luks_{username}_{profile_name}"
|
||||
if not os.path.exists(f"/dev/mapper/{mapper_name}") and not self.is_mounted(base_dest_path, profile_name):
|
||||
if not force_unmap:
|
||||
return
|
||||
return True # Already unmounted or not present, consider it successful
|
||||
|
||||
self.logger.log(f"Unmounting and resetting owner for {base_dest_path}")
|
||||
mount_point = self.get_mount_point(base_dest_path)
|
||||
self.logger.log(
|
||||
f"Unmounting and resetting owner for {base_dest_path}/{profile_name}")
|
||||
mount_point = self.get_mount_point(base_dest_path, profile_name)
|
||||
|
||||
script = f"""
|
||||
chown root:root \"{mount_point}\" || true
|
||||
umount -l \"{mount_point}\"
|
||||
script = f"""chown root:root "{mount_point}"
|
||||
umount "{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)
|
||||
success = self._execute_as_root(script, password)
|
||||
if success:
|
||||
self.remove_from_lock_file(mapper_name)
|
||||
if (base_dest_path, profile_name) in self.mounted_destinations:
|
||||
self.mounted_destinations.remove(
|
||||
(base_dest_path, profile_name))
|
||||
self.logger.log(
|
||||
f"Successfully unmounted {profile_name} at {base_dest_path}")
|
||||
if self.app and hasattr(self.app, 'header_frame'):
|
||||
self.app.header_frame.refresh_status()
|
||||
else:
|
||||
self.logger.log(
|
||||
f"Failed to unmount {profile_name} at {base_dest_path}")
|
||||
return success
|
||||
# Do not clear password cache here, it might be needed by another profile
|
||||
|
||||
if base_dest_path in self.mounted_destinations:
|
||||
self.mounted_destinations.remove(base_dest_path)
|
||||
if username in self.password_cache:
|
||||
del self.password_cache[username]
|
||||
|
||||
def unmount_all(self):
|
||||
def unmount_all(self) -> bool:
|
||||
self.logger.log(f"Unmounting all: {self.mounted_destinations}")
|
||||
for path in list(self.mounted_destinations):
|
||||
self.unmount_and_reset_owner(path, force_unmap=True)
|
||||
|
||||
def unmount_all_encrypted_drives(self, password: str) -> Tuple[bool, str]:
|
||||
for path in list(self.mounted_destinations):
|
||||
self.unmount_and_reset_owner(path, force_unmap=True)
|
||||
return True, "Successfully unmounted all drives."
|
||||
all_unmounted_successfully = True
|
||||
# Create a copy of the set to avoid issues with modifying it while iterating
|
||||
for base_path, profile_name in list(self.mounted_destinations):
|
||||
if not self.unmount_and_reset_owner(
|
||||
base_path, profile_name, force_unmap=True):
|
||||
all_unmounted_successfully = False
|
||||
return all_unmounted_successfully
|
||||
|
||||
def _get_chown_command(self, mount_point: str, is_system: bool) -> str:
|
||||
if not is_system:
|
||||
try:
|
||||
uid = os.getuid()
|
||||
gid = os.getgid()
|
||||
return f"chown {uid}:{gid} \"{mount_point}\""
|
||||
return f'chown {uid}:{gid} "{mount_point}"'
|
||||
except Exception as e:
|
||||
self.logger.log(
|
||||
f"Could not get current user UID/GID for chown: {e}")
|
||||
@@ -341,9 +339,7 @@ mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\"
|
||||
|
||||
def _execute_as_root(self, script_content: str, password_for_stdin: Optional[str] = None) -> bool:
|
||||
try:
|
||||
if password_for_stdin:
|
||||
escaped_password = password_for_stdin.replace("'", "'\\\''")
|
||||
script_content = f"LUKSPASS='{escaped_password}'\n{script_content}"
|
||||
password = password_for_stdin if password_for_stdin is not None else ""
|
||||
|
||||
project_root = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), '..'))
|
||||
@@ -355,26 +351,19 @@ mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\"
|
||||
f"CRITICAL: Privileged script runner not found at {runner_script_path}")
|
||||
return False
|
||||
|
||||
command = ['pkexec', runner_script_path]
|
||||
|
||||
log_lines = []
|
||||
for line in script_content.split('\n'):
|
||||
if "LUKSPASS=" in line:
|
||||
log_lines.append("LUKSPASS='[REDACTED]'")
|
||||
else:
|
||||
log_lines.append(line)
|
||||
sanitized_script_content = "\n".join(log_lines)
|
||||
command = ['pkexec', runner_script_path, password, script_content]
|
||||
|
||||
self.logger.log(
|
||||
f"Executing privileged command via runner: {runner_script_path}")
|
||||
# Simplified logging to avoid complexity
|
||||
self.logger.log(
|
||||
f"Script content to be piped:\n---\n{sanitized_script_content}\n---")
|
||||
f"""Script content passed as argument:\n---\n{script_content}\n---""")
|
||||
|
||||
result = subprocess.run(
|
||||
command, input=script_content, capture_output=True, text=True, check=False)
|
||||
command, capture_output=True, text=True, check=False)
|
||||
|
||||
if result.returncode == 0:
|
||||
log_output = f"Privileged script executed successfully."
|
||||
log_output = ("Privileged script executed successfully.")
|
||||
if result.stdout:
|
||||
log_output += f"\nStdout:\n{result.stdout}"
|
||||
if result.stderr:
|
||||
@@ -383,7 +372,7 @@ mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\"
|
||||
return True
|
||||
else:
|
||||
self.logger.log(
|
||||
f"Privileged script failed. Return code: {result.returncode}\nStderr: {result.stderr}\nStdout: {result.stdout}")
|
||||
f"Privileged script failed. Return code: {result.returncode} Stderr: {result.stderr} Stdout: {result.stdout}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.log(
|
||||
|
@@ -324,6 +324,8 @@ class Msg:
|
||||
"err_no_backup_selected": _("Please select a backup from the list."),
|
||||
"err_unlock_failed": _("Failed to unlock the container. Please check the password and try again."),
|
||||
"err_encrypted_not_mounted": _("Encrypted container is not unlocked. Please unlock it first from the header bar."),
|
||||
"unmount_failed_title": _("Unmount Failed"),
|
||||
"unmount_failed_message": _("Failed to unmount all encrypted drives. Please unmount them manually or try again."),
|
||||
"confirm_user_restore_title": _("Confirm User Data Restore"),
|
||||
"confirm_user_restore_msg": _("Do you really want to restore the backup of '{backup_name}' to its original location? Any newer files may be overwritten."),
|
||||
"confirm_delete_title": _("Confirm Deletion"),
|
||||
|
@@ -1,5 +1,10 @@
|
||||
#!/bin/bash
|
||||
# This script executes commands passed to its standard input.
|
||||
# This script executes commands passed as arguments.
|
||||
# The 'set -e' command ensures that the script will exit immediately if any command fails.
|
||||
set -e
|
||||
/bin/bash
|
||||
|
||||
# The password is the first argument, script content is the second.
|
||||
export LUKSPASS="$1"
|
||||
SCRIPT_TO_RUN="$2"
|
||||
|
||||
/bin/bash -c "$SCRIPT_TO_RUN"
|
40
main_app.py
40
main_app.py
@@ -11,6 +11,7 @@ from shared_libs.log_window import LogWindow
|
||||
from shared_libs.logger import app_logger
|
||||
from shared_libs.animated_icon import AnimatedIcon
|
||||
from shared_libs.common_tools import IconManager
|
||||
from shared_libs.message import MessageDialog
|
||||
from core.config_manager import ConfigManager
|
||||
from core.backup_manager import BackupManager
|
||||
from core.pbp_app_config import AppConfig, Msg
|
||||
@@ -331,6 +332,8 @@ class MainApplication(tk.Tk):
|
||||
self.restore_size_frame_before.grid_remove()
|
||||
self.restore_size_frame_after.grid_remove()
|
||||
|
||||
self._process_queue_id = None # Initialize the ID for the scheduled queue processing
|
||||
|
||||
self._load_state_and_initialize()
|
||||
self.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||
|
||||
@@ -371,13 +374,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)
|
||||
# Check for any pre-existing mounts for all known profiles
|
||||
known_profiles = list(AppConfig.FOLDER_PATHS.keys()) + ["system"]
|
||||
for profile_name in known_profiles:
|
||||
if self.backup_manager.encryption_manager.is_mounted(backup_dest_path, profile_name):
|
||||
app_logger.log(
|
||||
f"Adopting pre-existing mount for profile {profile_name} at {backup_dest_path} into session.")
|
||||
self.backup_manager.encryption_manager.mounted_destinations.add(
|
||||
(backup_dest_path, profile_name))
|
||||
|
||||
if hasattr(self, 'header_frame'):
|
||||
self.header_frame.refresh_status()
|
||||
@@ -537,7 +541,17 @@ class MainApplication(tk.Tk):
|
||||
def on_closing(self):
|
||||
self.config_manager.set_setting(
|
||||
"refresh_log", self.refresh_log_var.get())
|
||||
self.backup_manager.encryption_manager.unmount_all()
|
||||
|
||||
# Attempt to unmount all encrypted drives
|
||||
unmount_success = self.backup_manager.encryption_manager.unmount_all()
|
||||
|
||||
# Check if any drives are still mounted after the attempt
|
||||
if not unmount_success and self.backup_manager.encryption_manager.mounted_destinations:
|
||||
app_logger.log(
|
||||
"WARNING: Not all encrypted drives could be unmounted. Preventing application closure.")
|
||||
MessageDialog(
|
||||
message_type="error", title=Msg.STR["unmount_failed_title"], text=Msg.STR["unmount_failed_message"]).show()
|
||||
return # Prevent application from closing
|
||||
|
||||
self.config_manager.set_setting("last_mode", self.mode)
|
||||
|
||||
@@ -578,6 +592,9 @@ class MainApplication(tk.Tk):
|
||||
self.animated_icon.destroy()
|
||||
self.animated_icon = None
|
||||
|
||||
if self._process_queue_id:
|
||||
self.after_cancel(self._process_queue_id)
|
||||
|
||||
app_logger.log(Msg.STR["app_quit"])
|
||||
try:
|
||||
self.destroy()
|
||||
@@ -745,7 +762,7 @@ class MainApplication(tk.Tk):
|
||||
except Empty:
|
||||
pass
|
||||
finally:
|
||||
self.after(100, self._process_queue)
|
||||
self._process_queue_id = self.after(100, self._process_queue)
|
||||
|
||||
def _update_duration(self):
|
||||
if self.backup_is_running and self.start_time:
|
||||
@@ -775,13 +792,14 @@ class MainApplication(tk.Tk):
|
||||
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.")
|
||||
f"Found stale mount: {lock['mapper_name']} for path {lock['base_path']} and profile {lock['profile_name']}. Attempting to close.")
|
||||
self.backup_manager.encryption_manager.unmount_and_reset_owner(
|
||||
lock['base_path'], force_unmap=True)
|
||||
lock['base_path'], lock['profile_name'], force_unmap=True)
|
||||
|
||||
if not stale_mounts_found:
|
||||
app_logger.log("No stale mounts detected.")
|
||||
if locks:
|
||||
# If no stale mounts were found, the lock file should be empty.
|
||||
self.backup_manager.encryption_manager._write_lock_file([])
|
||||
|
||||
except Exception as e:
|
||||
|
@@ -85,9 +85,16 @@ class AdvancedSettingsFrame(ttk.Frame):
|
||||
self.manual_excludes_listbox.pack(
|
||||
fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
manual_exclude_buttons_frame = ttk.Frame(self.manual_excludes_frame)
|
||||
manual_exclude_buttons_frame.pack(pady=5)
|
||||
|
||||
delete_button = ttk.Button(
|
||||
self.manual_excludes_frame, text=Msg.STR["delete"], command=self._delete_manual_exclude)
|
||||
delete_button.pack(pady=5)
|
||||
manual_exclude_buttons_frame, text=Msg.STR["delete"], command=self._delete_manual_exclude)
|
||||
delete_button.pack(side=tk.LEFT, padx=5, pady=5)
|
||||
|
||||
cancel_button = ttk.Button(
|
||||
manual_exclude_buttons_frame, text=Msg.STR["cancel"], command=self._cancel_changes)
|
||||
cancel_button.pack(side=tk.LEFT, padx=5, pady=5)
|
||||
|
||||
self.animation_settings_frame = ttk.LabelFrame(
|
||||
view_container, text=Msg.STR["animation_settings_title"], padding=10)
|
||||
@@ -181,12 +188,12 @@ class AdvancedSettingsFrame(ttk.Frame):
|
||||
|
||||
self.keyfile_settings_frame.columnconfigure(1, weight=1)
|
||||
|
||||
button_frame = ttk.Frame(self)
|
||||
button_frame.pack(pady=10)
|
||||
self.bottom_button_frame = ttk.Frame(self)
|
||||
self.bottom_button_frame.pack(pady=10)
|
||||
|
||||
ttk.Button(button_frame, text=Msg.STR["apply"], command=self._apply_changes).pack(
|
||||
ttk.Button(self.bottom_button_frame, text=Msg.STR["apply"], command=self._apply_changes).pack(
|
||||
side=tk.LEFT, padx=5)
|
||||
ttk.Button(button_frame, text=Msg.STR["cancel"], command=self._cancel_changes).pack(
|
||||
ttk.Button(self.bottom_button_frame, text=Msg.STR["cancel"], command=self._cancel_changes).pack(
|
||||
side=tk.LEFT, padx=5)
|
||||
|
||||
self.tree_frame.pack(fill=tk.BOTH, expand=True)
|
||||
@@ -275,8 +282,17 @@ class AdvancedSettingsFrame(ttk.Frame):
|
||||
"Key file status unknown (no destination set).")
|
||||
return
|
||||
|
||||
# Determine the profile_name based on the current source folder
|
||||
source_name = self.app_instance.left_canvas_data.get('folder')
|
||||
if not source_name:
|
||||
# Fallback or handle case where no source is selected, perhaps default to "system" or log an error
|
||||
# For now, let's assume "system" if no specific folder is selected for keyfile status
|
||||
profile_name = "system"
|
||||
else:
|
||||
profile_name = "system" if source_name == "Computer" else source_name
|
||||
|
||||
key_file_path = self.app_instance.backup_manager.encryption_manager.get_key_file_path(
|
||||
self.app_instance.destination_path)
|
||||
self.app_instance.destination_path, profile_name)
|
||||
if os.path.exists(key_file_path):
|
||||
self.key_file_status_var.set(f"Key file exists: {key_file_path}")
|
||||
else:
|
||||
@@ -293,6 +309,12 @@ class AdvancedSettingsFrame(ttk.Frame):
|
||||
self.animation_settings_frame.pack_forget()
|
||||
self.backup_defaults_frame.pack_forget()
|
||||
|
||||
# Show/hide the main action buttons based on the view
|
||||
if index == 1: # Manual Excludes view
|
||||
self.bottom_button_frame.pack_forget()
|
||||
else:
|
||||
self.bottom_button_frame.pack(pady=10)
|
||||
|
||||
if index == 0:
|
||||
self.tree_frame.pack(fill=tk.BOTH, expand=True)
|
||||
self.info_label.config(text=Msg.STR["advanced_settings_warning"])
|
||||
@@ -332,6 +354,7 @@ class AdvancedSettingsFrame(ttk.Frame):
|
||||
selected_indices = self.manual_excludes_listbox.curselection()
|
||||
for i in reversed(selected_indices):
|
||||
self.manual_excludes_listbox.delete(i)
|
||||
self._apply_changes()
|
||||
|
||||
def _reset_animation_settings(self):
|
||||
self.config_manager.remove_setting("backup_animation_type")
|
||||
|
@@ -145,9 +145,12 @@ class BackupContentFrame(ttk.Frame):
|
||||
|
||||
self.base_backup_path = backup_path
|
||||
|
||||
source_name = self.app.left_canvas_data.get('folder')
|
||||
profile_name = "system" if source_name == "Computer" else source_name
|
||||
|
||||
# Check if the destination is encrypted and trigger mount if necessary
|
||||
is_encrypted = self.backup_manager.encryption_manager.is_encrypted(
|
||||
backup_path)
|
||||
backup_path, profile_name)
|
||||
self.viewing_encrypted = is_encrypted # Set this flag for remembering the view
|
||||
|
||||
pybackup_dir = os.path.join(backup_path, "pybackup")
|
||||
|
@@ -69,12 +69,19 @@ class HeaderFrame(tk.Frame):
|
||||
self.refresh_status()
|
||||
|
||||
def refresh_status(self):
|
||||
"""Checks the keyring status based on the current destination and updates the label."""
|
||||
"""Checks the keyring and mount status based on the current destination and updates the label."""
|
||||
app_logger.log("HeaderFrame: Refreshing status...")
|
||||
dest_path = self.app.destination_path
|
||||
app_logger.log(f"HeaderFrame: Destination path is '{dest_path}'")
|
||||
|
||||
if not dest_path or not self.encryption_manager.is_encrypted(dest_path):
|
||||
source_name = self.app.left_canvas_data.get('folder')
|
||||
if not source_name:
|
||||
self.keyring_status_label.config(text="")
|
||||
return
|
||||
|
||||
profile_name = "system" if source_name == "Computer" else source_name
|
||||
|
||||
if not dest_path or not self.encryption_manager.is_encrypted(dest_path, profile_name):
|
||||
app_logger.log(
|
||||
"HeaderFrame: No destination path or not encrypted. Clearing status.")
|
||||
# Clear status if not encrypted
|
||||
@@ -85,41 +92,35 @@ class HeaderFrame(tk.Frame):
|
||||
username = os.path.basename(dest_path.rstrip('/'))
|
||||
app_logger.log(f"HeaderFrame: Username is '{username}'")
|
||||
|
||||
is_mounted = self.encryption_manager.is_mounted(dest_path)
|
||||
is_mounted = self.encryption_manager.is_mounted(
|
||||
dest_path, profile_name)
|
||||
app_logger.log(f"HeaderFrame: Is mounted? {is_mounted}")
|
||||
|
||||
if is_mounted:
|
||||
status_text = "Key: In Use"
|
||||
auth_method = getattr(self.encryption_manager, 'auth_method', None)
|
||||
if auth_method == 'keyring':
|
||||
status_text += " (Keyring)"
|
||||
elif auth_method == 'keyfile':
|
||||
status_text += " (Keyfile)"
|
||||
self.keyring_status_label.config(
|
||||
text=status_text,
|
||||
fg="#2E8B57" # SeaGreen
|
||||
)
|
||||
else:
|
||||
key_in_keyring = self.encryption_manager.is_key_in_keyring(
|
||||
username)
|
||||
app_logger.log(f"HeaderFrame: Key in keyring? {key_in_keyring}")
|
||||
key_file_exists = os.path.exists(
|
||||
self.encryption_manager.get_key_file_path(dest_path))
|
||||
app_logger.log(f"HeaderFrame: Key file exists? {key_file_exists}")
|
||||
status_text = ""
|
||||
fg_color = ""
|
||||
|
||||
if key_in_keyring:
|
||||
self.keyring_status_label.config(
|
||||
text="Key: Available (Keyring)",
|
||||
fg="#FFD700" # Gold
|
||||
)
|
||||
elif key_file_exists:
|
||||
self.keyring_status_label.config(
|
||||
text="Key: Available (Keyfile)",
|
||||
fg="#FFD700" # Gold
|
||||
)
|
||||
else:
|
||||
self.keyring_status_label.config(
|
||||
text="Key: Not Available",
|
||||
fg="#A9A9A9" # DarkGray
|
||||
)
|
||||
if is_mounted:
|
||||
status_text = "Status: Mounted"
|
||||
fg_color = "#6bbbff" # LightBlue
|
||||
else:
|
||||
status_text = "Status: Not Mounted"
|
||||
fg_color = "#eb7f11" # Orange
|
||||
|
||||
key_in_keyring = self.encryption_manager.is_key_in_keyring(username)
|
||||
key_file_exists = os.path.exists(
|
||||
self.encryption_manager.get_key_file_path(dest_path, profile_name))
|
||||
|
||||
if key_in_keyring:
|
||||
status_text += " (Keyring Available)"
|
||||
elif key_file_exists:
|
||||
status_text += " (Keyfile Available)"
|
||||
else:
|
||||
status_text += " (Key Not Available)"
|
||||
if not is_mounted: # If not mounted and key not available, make it more prominent
|
||||
fg_color = "#DC143C" # Crimson
|
||||
|
||||
self.keyring_status_label.config(
|
||||
text=status_text,
|
||||
fg=fg_color
|
||||
)
|
||||
app_logger.log("HeaderFrame: Status refresh complete.")
|
||||
|
@@ -161,6 +161,7 @@ class SystemBackupContentFrame(ttk.Frame):
|
||||
path_to_delete=folder_to_delete,
|
||||
is_encrypted=is_encrypted,
|
||||
is_system=True,
|
||||
source_name="system",
|
||||
base_dest_path=self.backup_path,
|
||||
password=password,
|
||||
queue=self.winfo_toplevel().queue
|
||||
|
@@ -148,6 +148,7 @@ class UserBackupContentFrame(ttk.Frame):
|
||||
path_to_delete=folder_to_delete,
|
||||
is_encrypted=is_encrypted,
|
||||
is_system=False,
|
||||
source_name=selected_backup.get('source'),
|
||||
base_dest_path=self.backup_path,
|
||||
password=password,
|
||||
queue=self.winfo_toplevel().queue
|
||||
|
Reference in New Issue
Block a user