Compare commits
6 Commits
b3723ccc2f
...
7decaf46ef
Author | SHA1 | Date | |
---|---|---|---|
7decaf46ef | |||
507c1554ee | |||
ff08c9b646 | |||
9b9b0743a8 | |||
41d63743c1 | |||
eb970733bc |
@@ -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
221
core/encryption_helper.sh
Executable 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
|
@@ -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(
|
||||
|
@@ -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"
|
13
main_app.py
13
main_app.py
@@ -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)
|
||||
|
@@ -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')
|
||||
|
@@ -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):
|
||||
|
@@ -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.")
|
||||
|
@@ -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)
|
||||
|
@@ -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.")
|
||||
|
Reference in New Issue
Block a user