Compare commits

...

6 Commits

Author SHA1 Message Date
7decaf46ef add mount/mount all button 2025-09-14 10:41:22 +02:00
507c1554ee add functions in bash skript 2025-09-14 10:41:22 +02:00
ff08c9b646 fix: Advanced settings not saving and UI update issues
This commit addresses several bugs related to the Advanced Settings functionality:

- **Fix:** Ensure saved default settings (e.g., force encryption) are correctly applied to the main UI on application startup.
- **Fix:** Prevent the UI from automatically switching away from the Advanced Settings view after clicking "Apply". Settings are now saved in the background, and a temporary success message is displayed in the header.
- **Fix:** Ensure settings are correctly reloaded and displayed when switching between tabs within the Advanced Settings view, resolving inconsistencies after a reset operation.
2025-09-14 10:41:22 +02:00
9b9b0743a8 new script vor encryption manager and add combobox fo encrypt profiles part two 2025-09-14 10:41:22 +02:00
41d63743c1 new script vor encryption manager and add combobox fo encrypt profiles part one 2025-09-14 10:41:22 +02:00
eb970733bc fix error on delete backup 2025-09-14 10:41:22 +02:00
10 changed files with 667 additions and 253 deletions

View File

@@ -150,12 +150,12 @@ class BackupManager:
base_dest_path, is_system, source_name, is_encrypted)
os.makedirs(profile_path, exist_ok=True)
latest_full_backup_path = self._find_latest_backup(
latest_backup_path = self._find_latest_backup(
profile_path, source_name)
if mode == "incremental" and not latest_full_backup_path:
if mode == "incremental" and not latest_backup_path:
self.logger.log(
f"Mode is incremental, but no full backup found for source '{source_name}'. Forcing full backup.")
f"Mode is incremental, but no previous backup found for source '{source_name}'. Forcing full backup.")
mode = "full"
now = datetime.datetime.now()
@@ -168,9 +168,9 @@ class BackupManager:
rsync_command_parts = [
'rsync', '-aAXHv'] if is_system else ['rsync', '-aLv']
if mode == "incremental" and latest_full_backup_path and not is_dry_run:
if mode == "incremental" and latest_backup_path and not is_dry_run:
rsync_command_parts.append(
f"--link-dest={latest_full_backup_path}")
f"--link-dest={latest_backup_path}")
rsync_command_parts.extend(['--info=progress2'])
if exclude_files:
@@ -215,13 +215,13 @@ class BackupManager:
status = 'success' if return_code == 0 else 'warning' if return_code in [
23, 24] else 'cancelled' if return_code in [143, -15, 15, -9] else 'error'
if status in ['success', 'warning'] and not is_dry_run:
if mode == 'incremental' and latest_full_backup_path:
if mode == 'incremental' and latest_backup_path:
if is_system:
final_size = self._get_incremental_size_system(
rsync_dest)
else:
final_size = self._get_incremental_size_user(
rsync_dest, latest_full_backup_path)
rsync_dest, latest_backup_path)
else:
final_size = self._get_directory_size(rsync_dest)
self._create_info_json(
@@ -233,7 +233,7 @@ class BackupManager:
size_bytes=final_size,
is_encrypted=is_encrypted,
based_on=os.path.basename(
latest_full_backup_path) if latest_full_backup_path and mode == 'incremental' else None
latest_backup_path) if latest_backup_path and mode == 'incremental' else None
)
queue.put(
('completion', {'status': status, 'returncode': return_code}))
@@ -383,13 +383,15 @@ class BackupManager:
f"Failed to calculate incremental system backup size: {e}")
return self._get_directory_size(inc_path)
def list_all_backups(self, base_dest_path: str, mount_if_needed: bool = True):
def list_all_backups(self, base_dest_path: str):
pybackup_dir = os.path.join(base_dest_path, "pybackup")
metadata_dir = os.path.join(pybackup_dir, "metadata")
if not os.path.isdir(metadata_dir):
return [], []
return {"system_backups": [], "user_backups": [], "encrypted_profiles": {}}
all_backups = []
encrypted_profiles = {}
for info_file_name in os.listdir(metadata_dir):
if not info_file_name.endswith(".json"):
continue
@@ -402,29 +404,33 @@ class BackupManager:
is_encrypted = info_data.get("is_encrypted", False)
is_system = info_data.get("backup_type") == "system"
source_name = info_data.get("source_name", "N/A")
backup_dir_name = info_file_name.replace(".json", "")
profile_name = "system" if is_system else source_name
if is_encrypted:
is_mounted = self.encryption_manager.is_mounted(
base_dest_path, profile_name)
if profile_name not in encrypted_profiles:
encrypted_profiles[profile_name] = {
"is_mounted": is_mounted,
"base_dest_path": base_dest_path,
"is_system": is_system
}
else:
is_mounted = False # Not relevant for unencrypted
profile_path = self._get_profile_path(
base_dest_path, is_system, source_name, is_encrypted)
backup_dir_name = info_file_name.replace(".json", "")
full_path = os.path.join(profile_path, backup_dir_name)
# For encrypted backups, only add them to the list if their container is mounted.
if is_encrypted and not is_mounted:
continue
if not os.path.isdir(full_path):
if not is_encrypted:
self.logger.log(
f"Metadata file found for {backup_dir_name} but data directory not found at {full_path}. Skipping.")
continue
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 container for profile {profile_name} at {base_dest_path} to check for backup data...")
self.encryption_manager.prepare_encrypted_destination(
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.")
continue
self.logger.log(
f"Metadata file found for {backup_dir_name} but data directory not found at {full_path}. Skipping.")
continue
dt_obj = datetime.datetime.fromisoformat(
info_data["creation_date"])
@@ -511,30 +517,34 @@ class BackupManager:
for group in grouped_source_backups:
final_user_list.extend(group)
return final_system_list, final_user_list
return {
"system_backups": final_system_list,
"user_backups": final_user_list,
"encrypted_profiles": encrypted_profiles
}
def _find_latest_backup(self, profile_path: str, source_name: str) -> Optional[str]:
self.logger.log(
f"Searching for latest full backup for source '{source_name}' in: {profile_path}")
full_backups = []
f"Searching for latest backup for source '{source_name}' in: {profile_path}")
backups = []
if os.path.isdir(profile_path):
pattern = re.compile(
rf"^(\d{{8}}-\d{{6}})_{re.escape(source_name)}_full_(plain|enc)$")
rf"^(\d{{8}}-\d{{6}})_{re.escape(source_name)}_(full|incremental)_(plain|enc)$")
for item in os.listdir(profile_path):
item_path = os.path.join(profile_path, item)
if os.path.isdir(item_path) and pattern.match(item):
full_backups.append(item)
backups.append(item)
if not full_backups:
self.logger.log("No full backups found.")
if not backups:
self.logger.log("No backups found.")
return None
full_backups.sort(reverse=True)
latest_backup_dir = full_backups[0]
backups.sort(reverse=True)
latest_backup_dir = backups[0]
latest_backup_path = os.path.join(profile_path, latest_backup_dir)
self.logger.log(
f"Found latest full backup for --link-dest: {latest_backup_path}")
f"Found latest backup for --link-dest: {latest_backup_path}")
return latest_backup_path
def _create_info_json(self, base_dest_path: str, backup_dir_name: str, source_name: str, backup_type: str, mode: str, size_bytes: int, is_encrypted: bool, based_on: Optional[str] = None, comment: str = ""):
@@ -672,9 +682,9 @@ class BackupManager:
self.logger.log(f"Error loading cron jobs: {e}")
return jobs_list
def start_delete_backup(self, path_to_delete: str, is_encrypted: bool, is_system: bool, base_dest_path: str, queue, password: Optional[str] = None):
def start_delete_backup(self, path_to_delete: str, is_encrypted: bool, is_system: bool, source_name: str, base_dest_path: str, queue, password: Optional[str] = None):
thread = threading.Thread(target=self._run_delete, args=(
path_to_delete, is_encrypted, is_system, base_dest_path, queue, password))
path_to_delete, is_encrypted, is_system, source_name, base_dest_path, queue, password))
thread.daemon = True
thread.start()
@@ -690,14 +700,16 @@ class BackupManager:
profile_name = "system" if is_system else source_name
mount_point = self.encryption_manager.get_mount_point(
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.")
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
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)

221
core/encryption_helper.sh Executable file
View File

@@ -0,0 +1,221 @@
#!/bin/bash
# Exit immediately if a command exits with a non-zero status.
set -e
# Print each command to stderr before executing.
set -x
# --- Logger ---
log() {
echo "[HELPER] $1" >&2
}
log "Helper script started. All arguments: $@"
# --- Argument Parsing ---
COMMAND="$1"
log "Attempting to read password from stdin..."
# Temporarily disable exit-on-error for the read command, as it can fail in non-interactive shells
set +e
# Read the LUKS password from stdin. The -s flag prevents it from being echoed.
read -s LUKSPASS
# Re-enable exit-on-error
set -e
log "Password read from stdin."
shift # Remove command from arguments
log "Executing command: $COMMAND"
# --- Command Functions ---
do_mount() {
local container_path="$1"
local mapper_name="$2"
local mount_point="$3"
local uid="$4"
local gid="$5"
local key_file_arg="$6"
log "Mounting $container_path"
umount "$mount_point" 2>/dev/null || true
cryptsetup luksClose "$mapper_name" 2>/dev/null || true
mkdir -p "$mount_point"
if [ -n "$LUKSPASS" ]; then
log "Opening LUKS container with password."
echo -n "$LUKSPASS" | cryptsetup luksOpen "$container_path" "$mapper_name" -
elif [ -n "$key_file_arg" ]; then
log "Opening LUKS container with keyfile: $key_file_arg"
cryptsetup luksOpen "$container_path" "$mapper_name" --key-file "$key_file_arg"
else
log "Mount command needs either a password or a keyfile."
exit 1
fi
mount "/dev/mapper/$mapper_name" "$mount_point"
log "Mounted on $mount_point"
if [ -n "$uid" ] && [ -n "$gid" ]; then
log "Changing owner of $mount_point to $uid:$gid"
chown "$uid:$gid" "$mount_point"
fi
}
do_unmount() {
local mount_point="$1"
local mapper_name="$2"
log "Unmounting $mount_point"
if mountpoint -q "$mount_point"; then
chown root:root "$mount_point"
umount "$mount_point"
fi
if [ -e "/dev/mapper/$mapper_name" ]; then
cryptsetup luksClose "$mapper_name"
fi
log "Unmount complete."
}
do_create() {
local container_path="$1"
local mount_point="$2"
local size_gb="$3"
local mapper_name="$4"
local uid="$5"
local gid="$6"
log "Creating new container $container_path with size ${size_gb}G"
mkdir -p "$(dirname "$container_path")"
mkdir -p "$mount_point"
truncate -s "${size_gb}G" "$container_path"
log "Formatting LUKS container."
echo -n "$LUKSPASS" | cryptsetup luksFormat "$container_path" -
log "Opening new LUKS container."
echo -n "$LUKSPASS" | cryptsetup luksOpen "$container_path" "$mapper_name" -
log "Creating filesystem."
mkfs.ext4 "/dev/mapper/$mapper_name"
sleep 1
mount "/dev/mapper/$mapper_name" "$mount_point"
log "Mounted on $mount_point"
if [ -n "$uid" ] && [ -n "$gid" ]; then
log "Changing owner of $mount_point to $uid:$gid"
chown "$uid:$gid" "$mount_point"
fi
}
do_resize() {
local container_path="$1"
local mapper_name="$2"
local mount_point="$3"
local new_total_size="$4"
local uid="$5"
local gid="$6"
local key_file_arg="$7"
log "Resizing container $container_path to $new_total_size bytes"
# 1. Unmount
do_unmount "$mount_point" "$mapper_name"
# 2. Resize container file
log "Truncating container file."
truncate -s "$new_total_size" "$container_path"
sleep 1
# 3. Re-open, check, and resize filesystem
log "Re-opening container to resize filesystem."
if [ -n "$LUKSPASS" ]; then
echo -n "$LUKSPASS" | cryptsetup luksOpen "$container_path" "$mapper_name" -
elif [ -n "$key_file_arg" ]; then
cryptsetup luksOpen "$container_path" "$mapper_name" --key-file "$key_file_arg"
else
log "Resize command needs either a password or a keyfile."
exit 1
fi
log "Resizing LUKS volume."
cryptsetup resize "$mapper_name"
log "Checking and resizing filesystem."
e2fsck -fy "/dev/mapper/$mapper_name"
resize2fs "/dev/mapper/$mapper_name"
# 4. Mount it again
log "Remounting container."
mount "/dev/mapper/$mapper_name" "$mount_point"
if [ -n "$uid" ] && [ -n "$gid" ]; then
log "Changing owner of $mount_point to $uid:$gid"
chown "$uid:$gid" "$mount_point"
fi
log "Resize and remount complete."
}
do_unmount_all() {
log "Unmounting all provided pairs."
for pair in "$@"; do
# Split the pair 'mapper:mount_point'
MAPPER_NAME="${pair%%:*}"
MOUNT_POINT="${pair#*:}"
log "Unmounting $MOUNT_POINT ($MAPPER_NAME)"
if mountpoint -q "$MOUNT_POINT"; then
chown root:root "$MOUNT_POINT"
umount "$MOUNT_POINT"
fi
if [ -e "/dev/mapper/$MAPPER_NAME" ]; then
cryptsetup luksClose "$MAPPER_NAME"
fi
done
log "Bulk unmount complete."
}
do_mount_all() {
log "Mounting all provided profiles."
# Don't exit on error inside the loop
set +e
for profile_info in "$@"; do
# Split the info string container_path:mapper_name:mount_point:uid:gid
IFS=':' read -r container_path mapper_name mount_point uid gid <<< "$profile_info"
log "Attempting to mount $container_path"
# Call do_mount with the parsed arguments. Note: keyfile not supported in bulk mount.
do_mount "$container_path" "$mapper_name" "$mount_point" "$uid" "$gid" ""
done
set -e
log "Bulk mount process complete."
}
# --- Main Command Dispatcher ---
case "$COMMAND" in
mount)
do_mount "$@"
;;
unmount)
do_unmount "$@"
;;
create)
do_create "$@"
;;
resize)
do_resize "$@"
;;
unmount_all)
do_unmount_all "$@"
;;
mount_all)
do_mount_all "$@"
;;
*)
log "Unknown command: $COMMAND"
exit 1
;;
esac
exit 0

View File

@@ -5,6 +5,7 @@ import os
import shutil
import subprocess
import math
import shlex
from typing import Optional, Tuple
from core.pbp_app_config import AppConfig, Msg
@@ -120,24 +121,30 @@ class EncryptionManager:
os.remove(key_file_path)
return None
def _get_password_or_key_cmd(self, base_dest_path: str, profile_name: str, username: str) -> Tuple[str, Optional[str]]:
def _get_password_and_keyfile(self, base_dest_path: str, profile_name: str, username: str) -> Tuple[Optional[str], Optional[str]]:
"""Gets the password (from cache, keyring, or user) or the keyfile path."""
# 1. Try password from cache
password = self.password_cache.get(username)
if password:
return "-", password
return password, None
# 2. Try password from keyring
password = self.get_password_from_keyring(username)
if password:
self.password_cache[username] = password
return "-", password
return password, None
# 3. Check for key file
key_file_path = self.get_key_file_path(base_dest_path, profile_name)
if os.path.exists(key_file_path):
return f'--key-file "{key_file_path}"'
return None, key_file_path
# 4. Ask user for password
password = self.get_password(username, confirm=False)
if not password:
return "", None
return "-", password
if password:
return password, None
return None, None
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))
@@ -168,7 +175,7 @@ class EncryptionManager:
return None
free_space = shutil.disk_usage(mount_point).free
required_space = int(source_size * 1.15)
required_space = int(source_size * 1.10)
if required_space > free_space:
self.logger.log(
@@ -176,44 +183,33 @@ class EncryptionManager:
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(
password, key_file_path = self._get_password_and_keyfile(
base_dest_path, profile_name, username)
if not key_or_pass_arg:
if not password and not key_file_path:
self.logger.log(
"Could not get password or keyfile to resize container.")
return None
current_total = shutil.disk_usage(mount_point).total
needed_additional = required_space - free_space
needed_additional = required_space - free_space - \
(4 * 1024 * 1024 * 1024) # (4 * 1024 * 1024 * 1024) = 4 GB
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, 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}'
uid, gid = self._get_chown_ids(is_system)
resize_script = f"""set -e
# Unmount cleanly first
umount "{mount_point}"
cryptsetup luksClose {mapper_name}
helper_path = os.path.join(os.path.dirname(
__file__), 'encryption_helper.sh')
# Resize container file
truncate -s {int(new_total_size)} "{container_path}"
sleep 1
script = f'"{helper_path}" resize "{container_path}" "{mapper_name}" "{mount_point}" "{int(new_total_size)}" "{uid or ''}" "{gid or ''}" "{key_file_path or ''}"'
# Re-open, check, and resize filesystem
{luks_open_cmd}
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}"
{chown_cmd}
"""
if not self._execute_as_root(resize_script, password):
if not self._execute_as_root(script, password):
self.logger.log("Failed to execute resize and remount script.")
# Attempt to remount in the old state if resize fails
self._open_and_mount(
base_dest_path, profile_name, is_system, password_override=password)
return mount_point
@@ -224,7 +220,7 @@ mount "/dev/mapper/{mapper_name}" "{mount_point}"
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
size_gb = math.ceil((source_size * 1.2) / (1024**3)) + 2
username = os.path.basename(base_dest_path.rstrip('/'))
password = self.get_password(username, confirm=True)
if not password:
@@ -233,18 +229,13 @@ mount "/dev/mapper/{mapper_name}" "{mount_point}"
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)
uid, gid = self._get_chown_ids(is_system)
helper_path = os.path.join(os.path.dirname(
__file__), 'encryption_helper.sh')
script = f'"{helper_path}" create "{container_path}" "{mount_point}" "{int(size_gb)}" "{mapper_name}" "{uid or ''}" "{gid or ''}"'
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
@@ -253,32 +244,32 @@ mount "/dev/mapper/{mapper_name}" "{mount_point}"
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
password, key_file_path = None, None
if password_override:
password = password_override
key_or_pass_arg = "-"
else:
key_or_pass_arg, password = self._get_password_or_key_cmd(
password, key_file_path = self._get_password_and_keyfile(
base_dest_path, profile_name, username)
if not key_or_pass_arg:
if not password and not key_file_path:
self.logger.log(
"Could not get password or keyfile to mount container.")
return False
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}'
uid, gid = self._get_chown_ids(is_system)
helper_path = os.path.join(os.path.dirname(
__file__), 'encryption_helper.sh')
script = f'"{helper_path}" mount "{container_path}" "{mapper_name}" "{mount_point}" "{uid or ''}" "{gid or ''}" "{key_file_path or ''}"'
script = f"""set -e
umount "{mount_point}" || true
cryptsetup luksClose {mapper_name} || true
mkdir -p "{mount_point}"
{luks_open_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, profile_name, mapper_name)
self.mounted_destinations.add((base_dest_path, profile_name))
if self.app and hasattr(self.app, 'header_frame'):
self.app.header_frame.refresh_status()
return True
@@ -287,19 +278,25 @@ mount "/dev/mapper/{mapper_name}" "{mount_point}"
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}_{profile_name}"
if not os.path.exists(f"/dev/mapper/{mapper_name}") and not self.is_mounted(base_dest_path, profile_name):
mount_point = self.get_mount_point(base_dest_path, profile_name)
if not os.path.exists(f"/dev/mapper/{mapper_name}") and not os.path.ismount(mount_point):
if not force_unmap:
return True # Already unmounted or not present, consider it successful
self.logger.log(
f"Container {profile_name} at {base_dest_path} is already unmounted.")
return True
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}"
umount "{mount_point}"
cryptsetup luksClose {mapper_name}
"""
helper_path = os.path.join(os.path.dirname(
__file__), 'encryption_helper.sh')
# The unmount command doesn't need the LUKS password, but pkexec might need the user's password.
password = self.password_cache.get(username)
script = f'"{helper_path}" unmount "{mount_point}" "{mapper_name}"'
success = self._execute_as_root(script, password)
if success:
self.remove_from_lock_file(mapper_name)
@@ -314,65 +311,132 @@ mount "/dev/mapper/{mapper_name}" "{mount_point}"
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
def unmount_all(self) -> bool:
self.logger.log(f"Unmounting all: {self.mounted_destinations}")
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
if not self.mounted_destinations:
return True
def _get_chown_command(self, mount_point: str, is_system: bool) -> str:
self.logger.log(f"Bulk unmounting: {self.mounted_destinations}")
pairs_to_unmount = []
for base_path, profile_name in list(self.mounted_destinations):
username = os.path.basename(base_path.rstrip('/'))
mapper_name = f"pybackup_luks_{username}_{profile_name}"
mount_point = self.get_mount_point(base_path, profile_name)
pairs_to_unmount.append(f'{mapper_name}:{mount_point}')
helper_path = os.path.join(os.path.dirname(
__file__), 'encryption_helper.sh')
# This command doesn't need a LUKS password, but pkexec might need the user's password.
password = None
# Build the command string with quoted arguments
script = f'"{helper_path}" unmount_all {" ".join(f"\"{pair}\"" for pair in pairs_to_unmount)}'
success = self._execute_as_root(script, password)
if success:
# Clear all locks and destinations on success
for base_path, profile_name in list(self.mounted_destinations):
username = os.path.basename(base_path.rstrip('/'))
mapper_name = f"pybackup_luks_{username}_{profile_name}"
self.remove_from_lock_file(mapper_name)
self.mounted_destinations.clear()
self.logger.log("Successfully unmounted all profiles.")
if self.app and hasattr(self.app, 'header_frame'):
self.app.header_frame.refresh_status()
else:
self.logger.log("Failed to unmount all profiles.")
return success
def mount_all_profiles(self, profiles: list, password: str) -> bool:
"""Attempts to mount a list of encrypted profiles using a single password."""
if not profiles:
return True
self.logger.log(f"Attempting to bulk mount {len(profiles)} profiles.")
profile_info_strings = []
for profile in profiles:
# Create the info string: container_path:mapper_name:mount_point:uid:gid
username = os.path.basename(profile['base_dest_path'].rstrip('/'))
mapper_name = f"pybackup_luks_{username}_{profile['profile_name']}"
container_path = self.get_container_path(
profile['base_dest_path'], profile['profile_name'])
mount_point = self.get_mount_point(
profile['base_dest_path'], profile['profile_name'])
uid, gid = self._get_chown_ids(profile['is_system'])
# Keyfiles are not supported in bulk mount, as it relies on a single shared password.
info_str = f'{container_path}:{mapper_name}:{mount_point}:{uid or ""}:{gid or ""}'
profile_info_strings.append(info_str)
helper_path = os.path.join(os.path.dirname(
__file__), 'encryption_helper.sh')
script = f'"{helper_path}" mount_all {" ".join(f"\"{info}\"" for info in profile_info_strings)}'
success = self._execute_as_root(script, password)
if success:
# After a bulk attempt, we need to refresh the internal state for all of them
for profile in profiles:
if os.path.ismount(self.get_mount_point(profile['base_dest_path'], profile['profile_name'])):
self.mounted_destinations.add(
(profile['base_dest_path'], profile['profile_name']))
self.add_to_lock_file(profile['base_dest_path'], profile['profile_name'],
f"pybackup_luks_{os.path.basename(profile['base_dest_path'].rstrip('/'))}_{profile['profile_name']}")
self.logger.log("Bulk mount script executed.")
else:
self.logger.log("Bulk mount script failed to execute.")
if self.app and hasattr(self.app, 'header_frame'):
self.app.header_frame.refresh_status()
return success
def _get_chown_ids(self, is_system: bool) -> Tuple[Optional[int], Optional[int]]:
if not is_system:
try:
uid = os.getuid()
gid = os.getgid()
return f'chown {uid}:{gid} "{mount_point}"'
return os.getuid(), os.getgid()
except Exception as e:
self.logger.log(
f"Could not get current user UID/GID for chown: {e}")
return ""
return None, None
def _execute_as_root(self, script_content: str, password_for_stdin: Optional[str] = None) -> bool:
def _execute_as_root(self, script_content: str, password_for_luks: Optional[str] = None) -> bool:
try:
password = password_for_stdin if password_for_stdin is not None else ""
project_root = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..'))
runner_script_path = os.path.join(
project_root, 'core', 'privileged_script_runner.sh')
if not os.path.exists(runner_script_path):
self.logger.log(
f"CRITICAL: Privileged script runner not found at {runner_script_path}")
return False
command = ['pkexec', runner_script_path, password, script_content]
command = ['pkexec', '/bin/bash', '-c', script_content]
self.logger.log(
f"Executing privileged command via runner: {runner_script_path}")
# Simplified logging to avoid complexity
f"Executing privileged command: {' '.join(command)}")
self.logger.log(
f"""Script content passed as argument:\n---\n{script_content}\n---""")
f"Script content passed to bash -c: {script_content}")
result = subprocess.run(
command, capture_output=True, text=True, check=False)
process = subprocess.Popen(
command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8'
)
if result.returncode == 0:
log_output = ("Privileged script executed successfully.")
if result.stdout:
log_output += f"\nStdout:\n{result.stdout}"
if result.stderr:
log_output += f"\nStderr:\n{result.stderr}"
# Write the LUKS password to the script's stdin, which the helper script now reads.
luks_password_input = password_for_luks if password_for_luks is not None else ""
stdout, stderr = process.communicate(input=luks_password_input)
if process.returncode == 0:
log_output = "Privileged script executed successfully."
if stdout:
log_output += f"\nStdout:\n{stdout}"
if stderr:
log_output += f"\nStderr:\n{stderr}"
self.logger.log(log_output)
return True
else:
self.logger.log(
f"Privileged script failed. Return code: {result.returncode} Stderr: {result.stderr} Stdout: {result.stdout}")
f"Privileged script failed. Return code: {process.returncode}\nStderr: {stderr}\nStdout: {stdout}")
return False
except Exception as e:
self.logger.log(

View File

@@ -1,10 +0,0 @@
#!/bin/bash
# This script executes commands passed as arguments.
# The 'set -e' command ensures that the script will exit immediately if any command fails.
set -e
# The password is the first argument, script content is the second.
export LUKSPASS="$1"
SCRIPT_TO_RUN="$2"
/bin/bash -c "$SCRIPT_TO_RUN"

View File

@@ -53,6 +53,18 @@ class MainApplication(tk.Tk):
self.style.configure("Green.Sidebar.TButton", foreground="green")
# Custom button styles for BackupContentFrame
self.style.configure("Success.TButton", foreground="#2E7D32") # Darker Green
self.style.configure("Danger.TButton", foreground="#C62828") # Darker Red
# Custom LabelFrame style for Mount frame
self.style.configure("Mount.TLabelFrame", bordercolor="#0078D7")
self.style.configure("Mount.TLabelFrame.Label", foreground="#0078D7")
# Custom button styles for BackupContentFrame
self.style.configure("Success.TButton", foreground="#2E7D32") # Darker Green
self.style.configure("Danger.TButton", foreground="#C62828") # Darker Red
self.style.configure("Switch2.TCheckbutton",
background="#2b3e4f", foreground="white")
self.style.map("Switch2.TCheckbutton",
@@ -423,6 +435,7 @@ class MainApplication(tk.Tk):
restore_dest_folder)
self._process_queue()
self._update_sync_mode_display() # Call after loading state
self.update_backup_options_from_config() # Apply defaults on startup
def _setup_log_window(self):
self.log_frame = ttk.Frame(self.content_frame)

View File

@@ -318,18 +318,23 @@ class AdvancedSettingsFrame(ttk.Frame):
if index == 0:
self.tree_frame.pack(fill=tk.BOTH, expand=True)
self.info_label.config(text=Msg.STR["advanced_settings_warning"])
self._load_system_folders()
elif index == 1:
self.manual_excludes_frame.pack(fill=tk.BOTH, expand=True)
self.info_label.config(text=Msg.STR["manual_excludes_info"])
self._load_manual_excludes()
elif index == 2:
self.keyfile_settings_frame.pack(fill=tk.BOTH, expand=True)
self.info_label.config(text="")
self._update_key_file_status()
elif index == 3:
self.animation_settings_frame.pack(fill=tk.BOTH, expand=True)
self.info_label.config(text="")
self._load_animation_settings()
elif index == 4:
self.backup_defaults_frame.pack(fill=tk.BOTH, expand=True)
self.info_label.config(text="")
self._load_backup_defaults()
def update_nav_buttons(self, active_index):
for i, button in enumerate(self.nav_buttons):
@@ -360,6 +365,7 @@ class AdvancedSettingsFrame(ttk.Frame):
self.config_manager.remove_setting("backup_animation_type")
self.config_manager.remove_setting("calculation_animation_type")
self._load_animation_settings()
self._load_backup_defaults() # Reload other settings too
def _reset_backup_defaults(self):
self.config_manager.remove_setting("force_full_backup")
@@ -367,6 +373,7 @@ class AdvancedSettingsFrame(ttk.Frame):
self.config_manager.remove_setting("force_compression")
self.config_manager.remove_setting("force_encryption")
self._load_backup_defaults()
self._load_animation_settings() # Reload other settings too
if self.app_instance:
self.app_instance.update_backup_options_from_config()
@@ -542,9 +549,8 @@ class AdvancedSettingsFrame(ttk.Frame):
for item in self.manual_excludes_listbox.get(0, tk.END):
f.write(f"{item}\n")
self.pack_forget()
if self.show_main_settings_callback:
self.show_main_settings_callback()
# Show temporary success message in header
self.app_instance.header_frame.show_temporary_message("Settings saved successfully!")
if self.app_instance:
current_source = self.app_instance.left_canvas_data.get('folder')

View File

@@ -1,11 +1,13 @@
import tkinter as tk
from tkinter import ttk
import os
import inspect
from shared_libs.animated_icon import AnimatedIcon
from core.pbp_app_config import Msg
from pyimage_ui.system_backup_content_frame import SystemBackupContentFrame
from pyimage_ui.user_backup_content_frame import UserBackupContentFrame
from shared_libs.logger import app_logger
from shared_libs.message import MessageDialog
class BackupContentFrame(ttk.Frame):
@@ -18,7 +20,7 @@ class BackupContentFrame(ttk.Frame):
self.base_backup_path = None
self.current_view_index = 0
self.viewing_encrypted = False
self.encrypted_profiles = {}
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)
@@ -80,22 +82,115 @@ class BackupContentFrame(ttk.Frame):
self.user_backups_frame.grid(row=0, column=0, sticky=tk.NSEW)
action_button_frame = ttk.Frame(self, padding=10)
action_button_frame.grid(row=2, column=0, sticky="ew")
action_button_frame.grid(row=2, column=0, sticky="nsew")
action_button_frame.columnconfigure(
1, weight=1) # Make middle column expandable
# --- Mount Controls ---
self.mount_labelframe = ttk.LabelFrame(
action_button_frame, text="Mount Encrypt")
# Blue border frame inside the LabelFrame
mount_border_frame = tk.Frame(
self.mount_labelframe, background="#6bbbff")
mount_border_frame.pack(padx=10, pady=10)
# Content frame inside the border frame
theme_bg_color = self.winfo_toplevel().style.lookup('TFrame', 'background')
mount_content_frame = tk.Frame(
mount_border_frame, background=theme_bg_color)
mount_content_frame.pack(padx=2, pady=2)
mount_label = ttk.Label(mount_content_frame, text="Profile")
mount_label.grid(row=0, column=0, sticky="w", padx=5, pady=5)
self.mount_all_button = ttk.Button(
mount_content_frame, text="Mount All", command=self._mount_all_profiles)
self.mount_all_button.grid(
row=0, column=1, sticky="ew", padx=5, pady=5)
self.profile_combobox = ttk.Combobox(
mount_content_frame, state="readonly", width=20)
self.profile_combobox.grid(row=1, column=0, sticky="w", padx=5, pady=5)
self.mount_button = ttk.Button(
mount_content_frame, text="Mount", command=self._mount_selected_profile)
self.mount_button.grid(row=1, column=1, sticky="ew", padx=5, pady=5)
# --- Action Buttons ---
self.action_buttons_container = ttk.Frame(action_button_frame)
self.restore_button = ttk.Button(
action_button_frame, text=Msg.STR["restore"], command=self._restore_selected, state="disabled")
self.action_buttons_container, text=Msg.STR["restore"], command=self._restore_selected, state="disabled", style="Success.TButton")
self.restore_button.pack(side=tk.LEFT, padx=5)
self.delete_button = ttk.Button(
action_button_frame, text=Msg.STR["delete"], command=self._delete_selected, state="disabled")
self.action_buttons_container, text=Msg.STR["delete"], command=self._delete_selected, state="disabled", style="Danger.TButton")
self.delete_button.pack(side=tk.LEFT, padx=5)
self.edit_comment_button = ttk.Button(
action_button_frame, text=Msg.STR["comment"], command=self._edit_comment, state="disabled")
self.action_buttons_container, text=Msg.STR["comment"], command=self._edit_comment, state="disabled")
self.edit_comment_button.pack(side=tk.LEFT, padx=5)
self._switch_view(0)
def _mount_selected_profile(self):
profile_name = self.profile_combobox.get()
if not profile_name:
return
self._mount_profile(profile_name, single_mount=True)
def _mount_all_profiles(self):
unmounted_profiles = [{'profile_name': name, **data} for name,
data in self.encrypted_profiles.items() if not data['is_mounted']]
if not unmounted_profiles:
MessageDialog(message_type="info", title="Info",
text="All encrypted profiles are already mounted.").show()
return
# Get password once. The username for the password prompt is taken from the destination path, which is common.
username = os.path.basename(self.base_backup_path.rstrip('/'))
password = self.backup_manager.encryption_manager.get_password(
username, confirm=False)
if not password:
app_logger.log("Mount All cancelled: No password provided.")
return
self.app.config(cursor="watch")
self.update()
self.backup_manager.encryption_manager.mount_all_profiles(
unmounted_profiles, password)
self.app.config(cursor="")
self.show(self.base_backup_path, self.current_view_index)
def _mount_profile(self, profile_name: str, single_mount: bool = False) -> bool:
profile_data = self.encrypted_profiles.get(profile_name)
if not profile_data:
return False
self.app.config(cursor="watch")
self.update()
success = self.backup_manager.encryption_manager._open_and_mount(
base_dest_path=profile_data['base_dest_path'],
profile_name=profile_name,
is_system=profile_data['is_system']
)
self.app.config(cursor="")
if not success:
MessageDialog(message_type="error", title="Error",
text=f"Failed to mount profile '{profile_name}'.\nPlease check the password and try again.").show()
if single_mount:
self.show(self.base_backup_path, self.current_view_index)
return success
def update_button_state(self, is_selected):
self.restore_button.config(
state="normal" if is_selected else "disabled")
@@ -142,39 +237,39 @@ class BackupContentFrame(ttk.Frame):
app_logger.log(
f"BackupContentFrame: show called with path {backup_path}")
self.grid(row=2, column=0, sticky="nsew")
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, profile_name)
self.viewing_encrypted = is_encrypted # Set this flag for remembering the view
pybackup_dir = os.path.join(backup_path, "pybackup")
if not os.path.isdir(pybackup_dir):
app_logger.log(
f"Backup path {pybackup_dir} does not exist or is not a directory.")
# Clear views if path is invalid
self.system_backups_frame.show(backup_path, [])
self.user_backups_frame.show(backup_path, [])
self.mount_labelframe.grid_remove()
self.action_buttons_container.grid_remove()
return
all_backups = self.backup_manager.list_all_backups(
backup_path, mount_if_needed=True)
if all_backups:
system_backups, user_backups = all_backups
self.system_backups_frame.show(backup_path, system_backups)
self.user_backups_frame.show(backup_path, user_backups)
else:
# Handle case where inspection returns None (e.g. encrypted and mount_if_needed=False)
self.system_backups_frame.show(backup_path, [])
self.user_backups_frame.show(backup_path, [])
backup_data = self.backup_manager.list_all_backups(backup_path)
self.system_backups_frame.show(
backup_path, backup_data["system_backups"])
self.user_backups_frame.show(backup_path, backup_data["user_backups"])
self.encrypted_profiles = backup_data["encrypted_profiles"]
unmounted_profiles = [
name for name, data in self.encrypted_profiles.items() if not data['is_mounted']
]
if unmounted_profiles:
self.mount_labelframe.grid(row=0, column=0, sticky="w")
self.profile_combobox['values'] = unmounted_profiles
self.profile_combobox.set(unmounted_profiles[0])
else:
self.mount_labelframe.grid_remove()
self.action_buttons_container.grid(
row=0, column=1, sticky="") # Center the buttons
# Use the passed index to switch to the correct view
self.after(10, lambda: self._switch_view(initial_tab_index))
def hide(self):

View File

@@ -12,24 +12,23 @@ class HeaderFrame(tk.Frame):
self.image_manager = image_manager
self.encryption_manager = encryption_manager
self.app = app
self.original_subtitle_text = Msg.STR["header_subtitle"]
self._temp_message_after_id = None
# Configure grid weights for internal layout
self.columnconfigure(1, weight=1) # Make the middle column expand
self.rowconfigure(0, weight=1) # Make the top row expand
self.columnconfigure(1, weight=1)
# Left side: Icon and Main Title/Subtitle
left_frame = tk.Frame(self, bg="#455A64")
left_frame.grid(row=0, column=0, rowspan=2, sticky="nsew")
left_frame.columnconfigure(0, weight=1)
left_frame.rowconfigure(0, weight=1)
left_frame.grid(row=0, column=0, rowspan=2, sticky="ns")
icon_label = tk.Label(
left_frame,
image=self.image_manager.get_icon(
"backup_extralarge"), # Using a generic backup icon
"backup_extralarge"),
bg="#455A64",
)
icon_label.grid(row=0, column=0, sticky="e", padx=10, pady=5)
icon_label.pack(side=tk.LEFT, padx=10, pady=5)
title_label = tk.Label(
self,
@@ -41,86 +40,100 @@ class HeaderFrame(tk.Frame):
title_label.grid(row=0, column=1, sticky="w",
padx=(5, 20), pady=(15, 5))
subtitle_label = tk.Label(
self.subtitle_label = tk.Label(
self,
text=Msg.STR["header_subtitle"],
text=self.original_subtitle_text,
font=("Helvetica", 10),
fg="#bdc3c7",
bg="#455A64",
)
subtitle_label.grid(row=1, column=1, sticky="w",
self.subtitle_label.grid(row=1, column=1, sticky="nw",
padx=(5, 20), pady=(0, 10))
# Right side: Keyring status
# Right side: Status labels
right_frame = tk.Frame(self, bg="#455A64")
right_frame.grid(row=0, column=2, rowspan=2, sticky="nsew")
right_frame.columnconfigure(0, weight=1)
right_frame.rowconfigure(0, weight=1)
right_frame.grid(row=0, column=2, rowspan=2, sticky="nse", padx=10, pady=5)
self.keyring_status_label = tk.Label(
self.key_status_label = tk.Label(
right_frame,
text="",
font=("Helvetica", 10, "bold"),
bg="#455A64",
)
self.keyring_status_label.grid(
row=0, column=0, sticky="ne", padx=(10, 10), pady=(10, 0))
self.key_status_label.pack(anchor="e")
self.mount_status_label = tk.Label(
right_frame,
text="",
font=("Helvetica", 10, "bold"),
bg="#455A64",
)
self.mount_status_label.pack(anchor="e")
self.refresh_status()
def show_temporary_message(self, message: str, duration_ms: int = 4000):
"""Displays a temporary message in the subtitle area."""
# Cancel any previous after job to prevent conflicts
if self._temp_message_after_id:
self.after_cancel(self._temp_message_after_id)
self.subtitle_label.config(text=message, fg="#2ECC71") # Green color for success
self._temp_message_after_id = self.after(duration_ms, self._restore_subtitle)
def _restore_subtitle(self):
"""Restores the original subtitle text and color."""
self.subtitle_label.config(text=self.original_subtitle_text, fg="#bdc3c7")
self._temp_message_after_id = None
def refresh_status(self):
"""Checks the keyring and mount status based on the current destination and updates the label."""
"""Checks key and mount status of all encrypted profiles and updates the labels."""
app_logger.log("HeaderFrame: Refreshing status...")
dest_path = self.app.destination_path
app_logger.log(f"HeaderFrame: Destination path is '{dest_path}'")
source_name = self.app.left_canvas_data.get('folder')
if not source_name:
self.keyring_status_label.config(text="")
if not dest_path or not os.path.isdir(dest_path):
self.key_status_label.config(text="")
self.mount_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
self.keyring_status_label.config(text="")
return
app_logger.log("HeaderFrame: Destination is encrypted.")
# --- Key Status Logic ---
username = os.path.basename(dest_path.rstrip('/'))
app_logger.log(f"HeaderFrame: Username is '{username}'")
is_mounted = self.encryption_manager.is_mounted(
dest_path, profile_name)
app_logger.log(f"HeaderFrame: Is mounted? {is_mounted}")
status_text = ""
fg_color = ""
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))
self.encryption_manager.get_key_file_path(dest_path, username))
key_status_text = ""
if key_in_keyring:
status_text += " (Keyring Available)"
key_status_text = "Key: In Keyring"
elif key_file_exists:
status_text += " (Keyfile Available)"
key_status_text = "Key: File 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
key_status_text = "Key: Not Available"
self.key_status_label.config(text=key_status_text, fg="#bdc3c7")
self.keyring_status_label.config(
text=status_text,
# --- Mount Status Logic ---
backup_data = self.app.backup_manager.list_all_backups(dest_path)
encrypted_profiles = backup_data.get("encrypted_profiles", {})
if not encrypted_profiles:
self.mount_status_label.config(text="")
return
total_count = len(encrypted_profiles)
mounted_count = sum(
1 for profile in encrypted_profiles.values() if profile['is_mounted'])
mount_status_text = f"Mounted: {mounted_count}/{total_count}"
fg_color = ""
if mounted_count == total_count:
fg_color = "#6bbbff" # Bright Blue (All Mounted)
elif mounted_count > 0:
fg_color = "#FFD700" # Gold/Yellow (Partially Mounted)
else:
fg_color = "#FFA500" # Orange (None Mounted)
self.mount_status_label.config(
text=mount_status_text,
fg=fg_color
)
app_logger.log("HeaderFrame: Status refresh complete.")

View File

@@ -48,9 +48,9 @@ class SettingsFrame(ttk.Frame):
ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack(
side=tk.LEFT, ipady=15, padx=5)
advanced_button = ttk.Button(
self.button_frame, text=Msg.STR["advanced"], command=self._open_advanced_settings, style="Gray.Toolbutton")
advanced_button.pack(side=tk.LEFT, padx=5)
reset_button = ttk.Button(
self.button_frame, text=Msg.STR["default_settings"], command=self.actions.reset_to_default_settings, style="Gray.Toolbutton")
reset_button.pack(side=tk.LEFT)
ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack(
side=tk.LEFT, ipady=15, padx=5)
@@ -63,9 +63,9 @@ class SettingsFrame(ttk.Frame):
ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack(
side=tk.LEFT, ipady=15, padx=5)
reset_button = ttk.Button(
self.button_frame, text=Msg.STR["default_settings"], command=self.actions.reset_to_default_settings, style="Gray.Toolbutton")
reset_button.pack(side=tk.LEFT)
advanced_button = ttk.Button(
self.button_frame, text=Msg.STR["advanced"], command=self._open_advanced_settings, style="Gray.Toolbutton")
advanced_button.pack(side=tk.LEFT, padx=5)
# --- Container for Treeviews ---
self.trees_container = ttk.Frame(self)

View File

@@ -134,7 +134,7 @@ class UserBackupContentFrame(ttk.Frame):
if is_encrypted:
password = self.backup_manager.encryption_manager.get_password(
confirm=False)
selected_backup.get('source'), confirm=False)
if not password:
self.actions.logger.log(
"Password entry cancelled, aborting deletion.")