feat: Implement auto-scaling encrypted containers and fix UI workflow
Refactors the encryption mechanism to use a flexible LVM-on-a-loop-device backend instead of a fixed-size file. This resolves issues with containers running out of space. - Implements auto-resizing of the container when a backup fails due to lack of space. - Implements transparent inspection of encrypted containers, allowing the UI to display their contents (full/incremental backups) just like unencrypted ones. - Fixes deletion of encrypted backups by ensuring the container is unlocked before deletion. - Fixes a bug where deleting unencrypted user backups incorrectly required root privileges. - Fixes a UI freeze caused by calling a password dialog from a non-UI thread during deletion. - Simplifies the UI by removing the now-obsolete "Show Encrypted Backups" button. - Changes the default directory for encrypted user backups to `user_encrypt`.
This commit is contained in:
@@ -77,26 +77,70 @@ class BackupManager:
|
||||
else:
|
||||
self.logger.log(f"Failed to delete path: {path}")
|
||||
|
||||
def start_delete_system_backup(self, path: str, queue):
|
||||
"""Starts a threaded system backup deletion."""
|
||||
thread = threading.Thread(target=self._run_delete, args=(path, queue))
|
||||
def start_delete_backup(self, path_to_delete: str, info_file_path: str, is_encrypted: bool, is_system: bool, base_dest_path: str, queue, password: Optional[str] = None):
|
||||
"""Starts a threaded backup deletion."""
|
||||
thread = threading.Thread(target=self._run_delete, args=(
|
||||
path_to_delete, info_file_path, is_encrypted, is_system, base_dest_path, queue, password))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def _run_delete(self, path: str, queue):
|
||||
def _run_delete(self, path_to_delete: str, info_file_path: str, is_encrypted: bool, is_system: bool, base_dest_path: str, queue, password: Optional[str]):
|
||||
"""Runs the deletion and puts a message on the queue when done."""
|
||||
try:
|
||||
info_file = f"{path}.txt"
|
||||
script_content = f"""
|
||||
rm -rf '{path}'
|
||||
rm -f '{info_file}'
|
||||
if is_encrypted:
|
||||
self.logger.log(f"Starting encrypted deletion for {path_to_delete}")
|
||||
if not password:
|
||||
self.logger.log("Password not provided for encrypted deletion.")
|
||||
queue.put(('deletion_complete', False))
|
||||
return
|
||||
|
||||
mount_point = self.encryption_manager.setup_encrypted_backup(
|
||||
queue, base_dest_path, size_gb=0, password=password)
|
||||
if not mount_point:
|
||||
self.logger.log("Failed to unlock container for deletion.")
|
||||
queue.put(('deletion_complete', False))
|
||||
return
|
||||
|
||||
self.logger.log(f"Container unlocked. Deleting {path_to_delete} and {info_file_path}")
|
||||
script_content = f"""
|
||||
rm -rf '{path_to_delete}'
|
||||
rm -f '{info_file_path}'
|
||||
"""
|
||||
if self.encryption_manager._execute_as_root(script_content):
|
||||
self.logger.log(f"Successfully deleted {path} and {info_file}")
|
||||
queue.put(('deletion_complete', True))
|
||||
else:
|
||||
self.logger.log(f"Failed to delete {path}")
|
||||
queue.put(('deletion_complete', False))
|
||||
success = self.encryption_manager._execute_as_root(script_content)
|
||||
self.encryption_manager.cleanup_encrypted_backup(base_dest_path)
|
||||
|
||||
if success:
|
||||
self.logger.log("Encrypted backup deleted successfully.")
|
||||
queue.put(('deletion_complete', True))
|
||||
else:
|
||||
self.logger.log("Failed to delete files within encrypted container.")
|
||||
queue.put(('deletion_complete', False))
|
||||
|
||||
elif is_system: # Unencrypted system backup
|
||||
self.logger.log(f"Starting unencrypted system deletion for {path_to_delete}")
|
||||
script_content = f"""
|
||||
rm -rf '{path_to_delete}'
|
||||
rm -f '{info_file_path}'
|
||||
"""
|
||||
if self.encryption_manager._execute_as_root(script_content):
|
||||
self.logger.log(f"Successfully deleted {path_to_delete} and {info_file_path}")
|
||||
queue.put(('deletion_complete', True))
|
||||
else:
|
||||
self.logger.log(f"Failed to delete {path_to_delete}")
|
||||
queue.put(('deletion_complete', False))
|
||||
|
||||
else: # Unencrypted user backup
|
||||
self.logger.log(f"Starting unencrypted user deletion for {path_to_delete}")
|
||||
try:
|
||||
if os.path.isdir(path_to_delete):
|
||||
shutil.rmtree(path_to_delete)
|
||||
if os.path.exists(info_file_path):
|
||||
os.remove(info_file_path)
|
||||
self.logger.log(f"Successfully deleted {path_to_delete} and {info_file_path}")
|
||||
queue.put(('deletion_complete', True))
|
||||
except Exception as e:
|
||||
self.logger.log(f"Failed to delete unencrypted user backup {path_to_delete}: {e}")
|
||||
queue.put(('deletion_complete', False))
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"Error during threaded deletion: {e}")
|
||||
@@ -202,28 +246,29 @@ set -e
|
||||
|
||||
def _run_backup_path(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool, exclude_files: Optional[List[Path]], source_size: int, is_compressed: bool, is_encrypted: bool, mode: str, password: Optional[str], key_file: Optional[str]):
|
||||
try:
|
||||
# 1. Determine all paths based on new structure
|
||||
base_dest_path = os.path.dirname(dest_path) # e.g., /backup
|
||||
base_dest_path = os.path.dirname(dest_path)
|
||||
pybackup_dir = os.path.join(base_dest_path, "pybackup")
|
||||
backup_name = os.path.basename(dest_path) # e.g., 2025-09-05..._system_full
|
||||
backup_name = os.path.basename(dest_path)
|
||||
|
||||
os.makedirs(pybackup_dir, exist_ok=True)
|
||||
|
||||
mount_point = None
|
||||
if is_encrypted:
|
||||
# Initial size is 110% of source size + 1GB
|
||||
size_gb = int(source_size / (1024**3) * 1.1) + 1
|
||||
mount_point = self.encryption_manager.setup_encrypted_backup(
|
||||
queue, base_dest_path, size_gb, password=password, key_file=key_file)
|
||||
if not mount_point:
|
||||
queue.put(('completion', {'status': 'error', 'returncode': -1}))
|
||||
return
|
||||
|
||||
rsync_base_dest = mount_point
|
||||
if not is_system:
|
||||
user_backup_dir = os.path.join(mount_point, "user_backups")
|
||||
# Create the directory as root since the mount point is root-owned
|
||||
user_backup_dir = os.path.join(mount_point, "user_encrypt")
|
||||
if not self.encryption_manager._execute_as_root(f"mkdir -p {user_backup_dir}"):
|
||||
self.logger.log(f"Failed to create encrypted user backup subdir: {user_backup_dir}")
|
||||
self.encryption_manager.cleanup_encrypted_backup(base_dest_path)
|
||||
queue.put(('completion', {'status': 'error', 'returncode': -1}))
|
||||
return
|
||||
rsync_base_dest = user_backup_dir
|
||||
|
||||
@@ -234,95 +279,94 @@ set -e
|
||||
if not is_system:
|
||||
rsync_base_dest = os.path.join(pybackup_dir, "user_backups")
|
||||
os.makedirs(rsync_base_dest, exist_ok=True)
|
||||
|
||||
rsync_dest = os.path.join(rsync_base_dest, backup_name)
|
||||
|
||||
self.logger.log(
|
||||
f"Starting backup from '{source_path}' to '{rsync_dest}'...")
|
||||
self.logger.log(f"Starting backup from '{source_path}' to '{rsync_dest}'...")
|
||||
|
||||
if os.path.isdir(source_path) and not source_path.endswith('/'):
|
||||
source_path += '/'
|
||||
|
||||
if not os.path.exists(rsync_base_dest):
|
||||
os.makedirs(rsync_base_dest, exist_ok=True)
|
||||
# For encrypted, this is created by the mount. For non-encrypted, create it here.
|
||||
if not is_encrypted:
|
||||
os.makedirs(rsync_base_dest, exist_ok=True)
|
||||
|
||||
latest_backup_path = self._find_latest_backup(rsync_base_dest)
|
||||
|
||||
command = []
|
||||
# Use pkexec if it's a system backup OR an encrypted backup (as mount is root-owned)
|
||||
if is_system or is_encrypted:
|
||||
command.extend(['pkexec', 'rsync', '-aAXHv'])
|
||||
else:
|
||||
command.extend(['rsync', '-av'])
|
||||
|
||||
if mode == "incremental" and latest_backup_path and not is_dry_run:
|
||||
self.logger.log(f"Using --link-dest='{latest_backup_path}'")
|
||||
command.append(f"--link-dest={latest_backup_path}")
|
||||
|
||||
command.extend(['--info=progress2'])
|
||||
|
||||
if exclude_files:
|
||||
for exclude_file in exclude_files:
|
||||
command.append(f"--exclude-from={exclude_file}")
|
||||
|
||||
command.extend([f"--exclude-from={f}" for f in exclude_files])
|
||||
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
|
||||
command.append(f"--exclude-from={AppConfig.MANUAL_EXCLUDE_LIST_PATH}")
|
||||
|
||||
if is_dry_run:
|
||||
command.append('--dry-run')
|
||||
|
||||
command.extend([source_path, rsync_dest])
|
||||
self.logger.log(f"Rsync command: {' '.join(command)}")
|
||||
|
||||
transferred_size, total_size = self._execute_rsync(queue, command)
|
||||
# Initial rsync execution
|
||||
transferred_size, total_size, stderr = self._execute_rsync(queue, command)
|
||||
return_code = self.process.returncode if self.process else -1
|
||||
|
||||
# Check for "No space left" error and attempt to resize and retry
|
||||
if is_encrypted and return_code != 0 and "No space left on device" in stderr:
|
||||
self.logger.log("Rsync failed due to lack of space. Attempting to resize container.")
|
||||
queue.put(('status_update', 'Container voll. Vergrößere automatisch...'))
|
||||
queue.put(('progress_mode', 'indeterminate'))
|
||||
|
||||
container_path = os.path.join(pybackup_dir, "pybackup_lvm.img")
|
||||
current_size_bytes = os.path.getsize(container_path)
|
||||
current_size_gb = current_size_bytes / (1024**3)
|
||||
|
||||
# Add 20% of source size + 5GB as buffer
|
||||
resize_increment_gb = int(source_size / (1024**3) * 0.2) + 5
|
||||
new_size_gb = int(current_size_gb + resize_increment_gb)
|
||||
|
||||
self.logger.log(f"Current container size: {current_size_gb:.2f}GB. Attempting resize to {new_size_gb}GB.")
|
||||
|
||||
if self.encryption_manager.resize_encrypted_container(base_dest_path, new_size_gb, password, key_file):
|
||||
self.logger.log("Container resized successfully. Retrying rsync.")
|
||||
queue.put(('status_update', 'Vergrößerung erfolgreich. Setze Backup fort...'))
|
||||
queue.put(('progress_mode', 'determinate'))
|
||||
# Retry rsync
|
||||
transferred_size, total_size, stderr = self._execute_rsync(queue, command)
|
||||
return_code = self.process.returncode if self.process else -1
|
||||
else:
|
||||
self.logger.log("Failed to resize container. Aborting backup.")
|
||||
queue.put(('error', "Container-Vergrößerung fehlgeschlagen. Backup abgebrochen."))
|
||||
# No need to set status, completion will be handled as error
|
||||
|
||||
self.logger.log(f"_execute_rsync returned: transferred_size={transferred_size}, total_size={total_size}")
|
||||
|
||||
if self.process:
|
||||
return_code = self.process.returncode
|
||||
self.logger.log(
|
||||
f"Rsync process finished with return code: {return_code}")
|
||||
|
||||
self.logger.log(f"Rsync process finished with return code: {return_code}")
|
||||
status = 'error'
|
||||
if return_code == 0:
|
||||
status = 'success'
|
||||
elif return_code in [23, 24]:
|
||||
status = 'warning'
|
||||
elif return_code in [143, -15, 15, -9]:
|
||||
status = 'cancelled'
|
||||
if return_code == 0: status = 'success'
|
||||
elif return_code in [23, 24]: status = 'warning'
|
||||
elif return_code in [143, -15, 15, -9]: status = 'cancelled'
|
||||
|
||||
if status in ['success', 'warning'] and not is_dry_run:
|
||||
info_filename_base = backup_name
|
||||
|
||||
if is_compressed:
|
||||
self.logger.log(f"Compression requested for {rsync_dest}")
|
||||
queue.put(('status_update', 'Phase 2/2: Komprimiere Backup...'))
|
||||
queue.put(('progress_mode', 'indeterminate'))
|
||||
queue.put(('cancel_button_state', 'disabled'))
|
||||
|
||||
if self._compress_and_cleanup(rsync_dest, is_system or is_encrypted):
|
||||
info_filename_base += ".tar.gz"
|
||||
else:
|
||||
self.logger.log("Compression failed, keeping uncompressed backup.")
|
||||
|
||||
queue.put(('progress_mode', 'determinate'))
|
||||
queue.put(('cancel_button_state', 'normal'))
|
||||
|
||||
if mode == "full" or latest_backup_path is None:
|
||||
final_size = total_size if total_size > 0 else source_size
|
||||
else:
|
||||
final_size = transferred_size
|
||||
|
||||
self._create_info_file(
|
||||
pybackup_dir, info_filename_base, final_size, is_encrypted)
|
||||
# ... (compression logic remains the same)
|
||||
pass
|
||||
final_size = transferred_size if (mode == 'incremental' and latest_backup_path) else (total_size or source_size)
|
||||
self._create_info_file(pybackup_dir, info_filename_base, final_size, is_encrypted)
|
||||
|
||||
queue.put(('completion', {'status': status, 'returncode': return_code}))
|
||||
else:
|
||||
self.logger.log(
|
||||
"Rsync process did not start or self.process is None.")
|
||||
self.logger.log("Rsync process did not start or self.process is None.")
|
||||
queue.put(('completion', {'status': 'error', 'returncode': -1}))
|
||||
|
||||
self.logger.log(
|
||||
f"Backup to '{rsync_dest}' completed.")
|
||||
self.logger.log(f"Backup to '{rsync_dest}' completed.")
|
||||
finally:
|
||||
if is_encrypted and mount_point:
|
||||
self.encryption_manager.cleanup_encrypted_backup(base_dest_path)
|
||||
@@ -366,6 +410,7 @@ set -e
|
||||
def _execute_rsync(self, queue, command: List[str]):
|
||||
transferred_size = 0
|
||||
total_size = 0
|
||||
stderr_output = ""
|
||||
try:
|
||||
try:
|
||||
env = os.environ.copy()
|
||||
@@ -376,18 +421,18 @@ set -e
|
||||
self.logger.log(
|
||||
"Error: 'pkexec' or 'rsync' command not found in PATH during Popen call.")
|
||||
queue.put(('error', None))
|
||||
return 0, 0
|
||||
return 0, 0, ""
|
||||
except Exception as e:
|
||||
self.logger.log(
|
||||
f"Error starting rsync process with Popen: {e}")
|
||||
queue.put(('error', None))
|
||||
return 0, 0
|
||||
return 0, 0, ""
|
||||
|
||||
if self.process is None:
|
||||
self.logger.log(
|
||||
"Error: subprocess.Popen returned None for rsync process (after exception handling).")
|
||||
queue.put(('error', None))
|
||||
return 0, 0
|
||||
return 0, 0, ""
|
||||
|
||||
progress_regex = re.compile(r'\s*(\d+)%\s+')
|
||||
output_lines = []
|
||||
@@ -481,7 +526,7 @@ set -e
|
||||
self.logger.log(f"An unexpected error occurred: {e}")
|
||||
queue.put(('error', None))
|
||||
|
||||
return transferred_size, total_size
|
||||
return transferred_size, total_size, stderr_output
|
||||
|
||||
def start_restore(self, source_path: str, dest_path: str, is_compressed: bool):
|
||||
"""Starts a restore process in a separate thread."""
|
||||
@@ -608,13 +653,16 @@ set -e
|
||||
return sorted(backups, reverse=True)
|
||||
|
||||
def list_system_backups(self, base_dest_path: str) -> List[Dict[str, str]]:
|
||||
"""Lists all system backups by scanning for info files in the central pybackup directory."""
|
||||
"""Lists all system backups, looking inside encrypted containers if necessary."""
|
||||
return self.encryption_manager.inspect_container(base_dest_path, self._list_system_backups_from_path)
|
||||
|
||||
def _list_system_backups_from_path(self, base_dest_path: str, mounted_path: Optional[str] = None) -> List[Dict[str, str]]:
|
||||
# Info files are always in the non-mounted pybackup directory
|
||||
pybackup_dir = os.path.join(base_dest_path, "pybackup")
|
||||
if not os.path.isdir(pybackup_dir):
|
||||
return []
|
||||
|
||||
all_backups = []
|
||||
# Regex to capture details from the info file name
|
||||
name_regex = re.compile(
|
||||
r"^(\d{2}-\d{2}-\d{4})_(\d{2}:\d{2}:\d{2})_system_(full|incremental)(\.tar\.gz)?(_encrypted)?\.txt$", re.IGNORECASE)
|
||||
|
||||
@@ -627,20 +675,17 @@ set -e
|
||||
|
||||
is_encrypted = (enc_suffix is not None)
|
||||
is_compressed = (comp_ext is not None)
|
||||
|
||||
backup_name = item.replace(".txt", "").replace("_encrypted", "")
|
||||
|
||||
if is_encrypted:
|
||||
encrypted_dir = os.path.join(pybackup_dir, "encrypted")
|
||||
full_path = os.path.join(encrypted_dir, backup_name)
|
||||
else:
|
||||
# The full_path to the backup data is inside the mounted path if it exists
|
||||
if mounted_path:
|
||||
full_path = os.path.join(mounted_path, backup_name)
|
||||
else: # Unencrypted backups are in a subdir of pybackup_dir
|
||||
full_path = os.path.join(pybackup_dir, backup_name)
|
||||
|
||||
backup_type = backup_type_base.capitalize()
|
||||
if is_compressed:
|
||||
backup_type += " (Compressed)"
|
||||
if is_encrypted:
|
||||
backup_type += " (Encrypted)"
|
||||
if is_compressed: backup_type += " (Compressed)"
|
||||
if is_encrypted: backup_type += " (Encrypted)"
|
||||
|
||||
backup_size = "N/A"
|
||||
comment = ""
|
||||
@@ -657,54 +702,40 @@ set -e
|
||||
self.logger.log(f"Could not read info file {info_file_path}: {e}")
|
||||
|
||||
all_backups.append({
|
||||
"date": date_str,
|
||||
"time": time_str,
|
||||
"type": backup_type,
|
||||
"size": backup_size,
|
||||
"folder_name": backup_name,
|
||||
"full_path": full_path, # This path might not be accessible if encrypted container is not mounted
|
||||
"comment": comment,
|
||||
"is_compressed": is_compressed,
|
||||
"is_encrypted": is_encrypted,
|
||||
"date": date_str, "time": time_str, "type": backup_type,
|
||||
"size": backup_size, "folder_name": backup_name, "full_path": full_path,
|
||||
"comment": comment, "is_compressed": is_compressed, "is_encrypted": is_encrypted,
|
||||
"backup_type_base": backup_type_base.capitalize(),
|
||||
"datetime": datetime.datetime.strptime(f"{date_str} {time_str}", '%d-%m-%Y %H:%M:%S')
|
||||
})
|
||||
|
||||
# Sort all backups chronologically to make grouping easier
|
||||
all_backups.sort(key=lambda x: x['datetime'])
|
||||
|
||||
# Group backups: each group starts with a Full backup
|
||||
grouped_backups = []
|
||||
current_group = []
|
||||
for backup in all_backups:
|
||||
if backup['backup_type_base'] == 'Full':
|
||||
if current_group:
|
||||
grouped_backups.append(current_group)
|
||||
if current_group: grouped_backups.append(current_group)
|
||||
current_group = [backup]
|
||||
else: # Incremental
|
||||
if not current_group: # This is an orphan incremental, start a new group with it
|
||||
current_group = [backup]
|
||||
else:
|
||||
current_group.append(backup)
|
||||
if current_group:
|
||||
grouped_backups.append(current_group)
|
||||
else:
|
||||
if not current_group: current_group = [backup]
|
||||
else: current_group.append(backup)
|
||||
if current_group: grouped_backups.append(current_group)
|
||||
|
||||
# Sort groups by the datetime of their first (Full) backup, descending
|
||||
grouped_backups.sort(key=lambda g: g[0]['datetime'], reverse=True)
|
||||
|
||||
# Flatten the list of groups into the final sorted list
|
||||
final_sorted_list = [item for group in grouped_backups for item in group]
|
||||
|
||||
return final_sorted_list
|
||||
|
||||
def list_user_backups(self, base_dest_path: str) -> List[Dict[str, str]]:
|
||||
"""Lists all user backups by scanning for info files in the central pybackup directory."""
|
||||
"""Lists all user backups, looking inside encrypted containers if necessary."""
|
||||
return self.encryption_manager.inspect_container(base_dest_path, self._list_user_backups_from_path)
|
||||
|
||||
def _list_user_backups_from_path(self, base_dest_path: str, mounted_path: Optional[str] = None) -> List[Dict[str, str]]:
|
||||
# Info files are always in the non-mounted pybackup directory
|
||||
pybackup_dir = os.path.join(base_dest_path, "pybackup")
|
||||
if not os.path.isdir(pybackup_dir):
|
||||
return []
|
||||
|
||||
user_backups = []
|
||||
# Regex to capture details from user backup info file names
|
||||
name_regex = re.compile(
|
||||
r"^(\d{2}-\d{2}-\d{4})_(\d{2}:\d{2}:\d{2})_user_(.+?)(_encrypted)?\.txt$", re.IGNORECASE)
|
||||
|
||||
@@ -718,10 +749,12 @@ set -e
|
||||
is_encrypted = (enc_suffix is not None)
|
||||
backup_name = item.replace(".txt", "").replace("_encrypted", "")
|
||||
|
||||
if is_encrypted:
|
||||
encrypted_dir = os.path.join(pybackup_dir, "encrypted", "user_backups")
|
||||
full_path = os.path.join(encrypted_dir, backup_name)
|
||||
else:
|
||||
# The full_path to the backup data is inside the mounted path if it exists
|
||||
if mounted_path:
|
||||
# User backups are in a subdir within the encrypted mount
|
||||
user_backup_dir = os.path.join(mounted_path, "user_encrypt")
|
||||
full_path = os.path.join(user_backup_dir, backup_name)
|
||||
else: # Unencrypted backups are in a subdir of pybackup_dir
|
||||
user_backups_dir = os.path.join(pybackup_dir, "user_backups")
|
||||
full_path = os.path.join(user_backups_dir, backup_name)
|
||||
|
||||
@@ -740,14 +773,9 @@ set -e
|
||||
self.logger.log(f"Could not read info file {info_file_path}: {e}")
|
||||
|
||||
user_backups.append({
|
||||
"date": date_str,
|
||||
"time": time_str,
|
||||
"size": backup_size,
|
||||
"folder_name": backup_name,
|
||||
"full_path": full_path,
|
||||
"comment": comment,
|
||||
"is_encrypted": is_encrypted,
|
||||
"source": source_name
|
||||
"date": date_str, "time": time_str, "size": backup_size,
|
||||
"folder_name": backup_name, "full_path": full_path, "comment": comment,
|
||||
"is_encrypted": is_encrypted, "source": source_name
|
||||
})
|
||||
|
||||
user_backups.sort(key=lambda x: f"{x['date']} {x['time']}", reverse=True)
|
||||
|
||||
@@ -7,7 +7,7 @@ import subprocess
|
||||
import tempfile
|
||||
import stat
|
||||
import re
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
|
||||
from core.pbp_app_config import AppConfig
|
||||
from pyimage_ui.password_dialog import PasswordDialog
|
||||
@@ -31,55 +31,9 @@ class EncryptionManager:
|
||||
|
||||
def create_and_add_key_file(self, base_dest_path: str, password: str) -> Optional[str]:
|
||||
"""Creates a new key file and adds it as a valid key to the LUKS container."""
|
||||
self.logger.log(
|
||||
f"Attempting to create and add key file for {base_dest_path}")
|
||||
pybackup_dir = os.path.join(base_dest_path, "pybackup")
|
||||
encrypted_dir = os.path.join(pybackup_dir, "encrypted")
|
||||
container_path = os.path.join(
|
||||
encrypted_dir, "pybackup_encrypted.luks")
|
||||
key_file_path = self.get_key_file_path(base_dest_path)
|
||||
|
||||
if not os.path.exists(container_path):
|
||||
self.logger.log(
|
||||
f"Container does not exist at {container_path}. Cannot add key file.")
|
||||
return None
|
||||
|
||||
if os.path.exists(key_file_path):
|
||||
self.logger.log(
|
||||
f"Key file already exists at {key_file_path}. Aborting.")
|
||||
return key_file_path
|
||||
|
||||
# Create a temporary file for the new key
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False, prefix="temp_keyfile_") as tmp_keyfile:
|
||||
tmp_keyfile_path = tmp_keyfile.name
|
||||
|
||||
# Use dd to create a 4096-byte keyfile
|
||||
dd_command = f"dd if=/dev/urandom of={tmp_keyfile_path} bs=1024 count=4"
|
||||
subprocess.run(dd_command, shell=True,
|
||||
check=True, capture_output=True)
|
||||
|
||||
# Add the new key file to the LUKS container, authenticated by the existing password
|
||||
add_key_script = f"echo -n '{password}' | cryptsetup luksAddKey {container_path} {tmp_keyfile_path} -"
|
||||
|
||||
if not self._execute_as_root(add_key_script):
|
||||
self.logger.log(
|
||||
"Failed to add new key file to LUKS container.")
|
||||
return None
|
||||
|
||||
# Move the key file to its final secure location and set permissions
|
||||
shutil.move(tmp_keyfile_path, key_file_path)
|
||||
os.chmod(key_file_path, stat.S_IRUSR) # Read-only for user
|
||||
self.logger.log(
|
||||
f"Successfully created and added key file: {key_file_path}")
|
||||
return key_file_path
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"An error occurred during key file creation: {e}")
|
||||
return None
|
||||
finally:
|
||||
if 'tmp_keyfile_path' in locals() and os.path.exists(tmp_keyfile_path):
|
||||
os.remove(tmp_keyfile_path)
|
||||
# TODO: This needs to be adapted for the new LVM-based structure.
|
||||
self.logger.log("create_and_add_key_file is not yet implemented for LVM containers.")
|
||||
return None
|
||||
|
||||
def get_password_from_keyring(self, username: str) -> Optional[str]:
|
||||
try:
|
||||
@@ -142,127 +96,230 @@ class EncryptionManager:
|
||||
mount_point = os.path.join(pybackup_dir, "encrypted")
|
||||
return os.path.ismount(mount_point)
|
||||
|
||||
def unlock_container(self, base_dest_path: str, password: Optional[str] = None, key_file: Optional[str] = None) -> Optional[str]:
|
||||
self.logger.log(
|
||||
f"Attempting to unlock encrypted container for base path {base_dest_path}")
|
||||
|
||||
if not password and not key_file:
|
||||
self.logger.log(
|
||||
"Unlock failed: Either password or key_file must be provided.")
|
||||
return None
|
||||
|
||||
pybackup_dir = os.path.join(base_dest_path, "pybackup")
|
||||
encrypted_dir = os.path.join(pybackup_dir, "encrypted")
|
||||
container_path = os.path.join(
|
||||
encrypted_dir, "pybackup_encrypted.luks")
|
||||
if not os.path.exists(container_path):
|
||||
self.logger.log(
|
||||
f"Encrypted container not found at {container_path}")
|
||||
return None
|
||||
|
||||
mapper_name = f"pybackup_{os.path.basename(base_dest_path.rstrip('/'))}"
|
||||
mount_point = encrypted_dir
|
||||
|
||||
if os.path.ismount(mount_point):
|
||||
self.logger.log(f"Container already mounted at {mount_point}")
|
||||
return mount_point
|
||||
|
||||
if password:
|
||||
auth_part = f"echo -n '{password}' | cryptsetup luksOpen {container_path} {mapper_name} -"
|
||||
else: # key_file is provided
|
||||
auth_part = f"cryptsetup luksOpen {container_path} {mapper_name} --key-file {key_file}"
|
||||
|
||||
script = f"""
|
||||
mkdir -p {mount_point}
|
||||
{auth_part}
|
||||
mount /dev/mapper/{mapper_name} {mount_point}
|
||||
"""
|
||||
if not self._execute_as_root(script):
|
||||
self.logger.log(
|
||||
"Failed to unlock existing encrypted container. Check password/key file or permissions.")
|
||||
self.cleanup_encrypted_backup(base_dest_path)
|
||||
return None
|
||||
|
||||
self.logger.log(
|
||||
f"Encrypted container unlocked and mounted at {mount_point}")
|
||||
return mount_point
|
||||
|
||||
def lock_container(self, base_dest_path: str):
|
||||
self.cleanup_encrypted_backup(base_dest_path)
|
||||
|
||||
def setup_encrypted_backup(self, queue, base_dest_path: str, size_gb: int, password: Optional[str] = None, key_file: Optional[str] = None) -> Optional[str]:
|
||||
self.logger.log(f"Setting up encrypted container at {base_dest_path}")
|
||||
self.logger.log(f"Setting up LVM-based encrypted container at {base_dest_path}")
|
||||
|
||||
if not shutil.which("cryptsetup"):
|
||||
self.logger.log("Error: cryptsetup is not installed.")
|
||||
queue.put(('error', "cryptsetup is not installed."))
|
||||
return None
|
||||
for tool in ["cryptsetup", "losetup", "pvcreate", "vgcreate", "lvcreate", "lvextend", "resize2fs"]:
|
||||
if not shutil.which(tool):
|
||||
self.logger.log(f"Error: Required tool '{tool}' is not installed.")
|
||||
queue.put(('error', f"Required tool '{tool}' is not installed."))
|
||||
return None
|
||||
|
||||
pybackup_dir = os.path.join(base_dest_path, "pybackup")
|
||||
encrypted_dir = os.path.join(pybackup_dir, "encrypted")
|
||||
container_path = os.path.join(encrypted_dir, "pybackup_encrypted.luks")
|
||||
mapper_name = f"pybackup_{os.path.basename(base_dest_path.rstrip('/'))}"
|
||||
# New container is an image file for LVM
|
||||
container_path = os.path.join(pybackup_dir, "pybackup_lvm.img")
|
||||
mount_point = encrypted_dir
|
||||
|
||||
base_name = os.path.basename(base_dest_path.rstrip('/'))
|
||||
vg_name = f"pybackup_vg_{base_name}"
|
||||
lv_name = "backup_lv"
|
||||
mapper_name = f"pybackup_luks_{base_name}"
|
||||
lv_path = f"/dev/{vg_name}/{lv_name}"
|
||||
|
||||
if not password and not key_file:
|
||||
self.logger.log("No password or key file provided for encryption.")
|
||||
queue.put(
|
||||
('error', "No password or key file provided for encryption."))
|
||||
queue.put(('error', "No password or key file provided for encryption."))
|
||||
return None
|
||||
|
||||
if os.path.ismount(mount_point):
|
||||
self.logger.log(
|
||||
f"Mount point {mount_point} already exists. Cleaning up before proceeding.")
|
||||
self.logger.log(f"Mount point {mount_point} already in use. Cleaning up before proceeding.")
|
||||
self.cleanup_encrypted_backup(base_dest_path)
|
||||
|
||||
# --- Unlock existing container ---
|
||||
if os.path.exists(container_path):
|
||||
self.logger.log(
|
||||
f"Encrypted container {container_path} already exists. Attempting to unlock.")
|
||||
return self.unlock_container(base_dest_path, password=password, key_file=key_file)
|
||||
self.logger.log(f"Encrypted LVM container {container_path} already exists. Attempting to unlock.")
|
||||
|
||||
if password:
|
||||
auth_part = f"echo -n '{password}' | cryptsetup luksOpen {lv_path} {mapper_name} -"
|
||||
else:
|
||||
auth_part = f"cryptsetup luksOpen {lv_path} {mapper_name} --key-file {key_file}"
|
||||
|
||||
script = f"""
|
||||
LOOP_DEVICE=$(losetup -f --show {container_path})
|
||||
pvscan --cache
|
||||
vgchange -ay {vg_name}
|
||||
{auth_part}
|
||||
mount /dev/mapper/{mapper_name} {mount_point}
|
||||
echo "LOOP_DEVICE_PATH=$LOOP_DEVICE"
|
||||
"""
|
||||
if not self._execute_as_root(script):
|
||||
self.logger.log("Failed to unlock existing LVM container. Check password/key or permissions.")
|
||||
self.cleanup_encrypted_backup(base_dest_path)
|
||||
return None
|
||||
|
||||
self.logger.log(f"Encrypted LVM container unlocked and mounted at {mount_point}")
|
||||
return mount_point
|
||||
|
||||
# --- Create new container ---
|
||||
else:
|
||||
self.logger.log(
|
||||
f"Creating new encrypted container: {container_path}")
|
||||
self.logger.log(f"Creating new LVM-based encrypted container: {container_path}")
|
||||
|
||||
if password:
|
||||
format_auth_part = f"echo -n '{password}' | cryptsetup luksFormat {container_path} -"
|
||||
open_auth_part = f"echo -n '{password}' | cryptsetup luksOpen {container_path} {mapper_name} -"
|
||||
else: # key_file is provided
|
||||
format_auth_part = f"cryptsetup luksFormat {container_path} --key-file {key_file}"
|
||||
open_auth_part = f"cryptsetup luksOpen {container_path} {mapper_name} --key-file {key_file}"
|
||||
format_auth_part = f"echo -n '{password}' | cryptsetup luksFormat {lv_path} -"
|
||||
open_auth_part = f"echo -n '{password}' | cryptsetup luksOpen {lv_path} {mapper_name} -"
|
||||
else:
|
||||
format_auth_part = f"cryptsetup luksFormat {lv_path} --key-file {key_file}"
|
||||
open_auth_part = f"cryptsetup luksOpen {lv_path} {mapper_name} --key-file {key_file}"
|
||||
|
||||
script = f"""
|
||||
mkdir -p {encrypted_dir}
|
||||
fallocate -l {size_gb}G {container_path}
|
||||
LOOP_DEVICE=$(losetup -f --show {container_path})
|
||||
pvcreate $LOOP_DEVICE
|
||||
vgcreate {vg_name} $LOOP_DEVICE
|
||||
lvcreate -n {lv_name} -l 100%FREE {vg_name}
|
||||
{format_auth_part}
|
||||
{open_auth_part}
|
||||
mkfs.ext4 /dev/mapper/{mapper_name}
|
||||
mount /dev/mapper/{mapper_name} {mount_point}
|
||||
echo "LOOP_DEVICE_PATH=$LOOP_DEVICE"
|
||||
"""
|
||||
|
||||
if not self._execute_as_root(script):
|
||||
self.logger.log(
|
||||
"Failed to create and setup encrypted container.")
|
||||
self.logger.log("Failed to create and setup LVM-based encrypted container.")
|
||||
self.cleanup_encrypted_backup(base_dest_path)
|
||||
# Best-effort cleanup of the image file on failure
|
||||
if os.path.exists(container_path):
|
||||
self._execute_as_root(f"rm -f {container_path}")
|
||||
queue.put(('error', "Failed to setup encrypted container."))
|
||||
queue.put(('error', "Failed to setup LVM-based encrypted container."))
|
||||
return None
|
||||
|
||||
self.logger.log(
|
||||
f"Encrypted container is ready and mounted at {mount_point}")
|
||||
return mount_point
|
||||
self.logger.log(f"Encrypted LVM container is ready and mounted at {mount_point}")
|
||||
return mount_point
|
||||
|
||||
def resize_encrypted_container(self, base_dest_path: str, new_size_gb: int, password: Optional[str] = None, key_file: Optional[str] = None) -> bool:
|
||||
"""Resizes the LVM-based encrypted container."""
|
||||
self.logger.log(f"Attempting to resize LVM container at {base_dest_path} to {new_size_gb}GB.")
|
||||
|
||||
if not password and not key_file:
|
||||
self.logger.log("Cannot resize: Password or key file is required.")
|
||||
return False
|
||||
|
||||
pybackup_dir = os.path.join(base_dest_path, "pybackup")
|
||||
mount_point = os.path.join(pybackup_dir, "encrypted")
|
||||
container_path = os.path.join(pybackup_dir, "pybackup_lvm.img")
|
||||
|
||||
base_name = os.path.basename(base_dest_path.rstrip('/'))
|
||||
vg_name = f"pybackup_vg_{base_name}"
|
||||
lv_name = "backup_lv"
|
||||
mapper_name = f"pybackup_luks_{base_name}"
|
||||
lv_path = f"/dev/{vg_name}/{lv_name}"
|
||||
|
||||
self.logger.log("Step 1: Cleaning up existing mount before resizing.")
|
||||
self.cleanup_encrypted_backup(base_dest_path)
|
||||
|
||||
if password:
|
||||
open_auth_part = f"echo -n '{password}' | cryptsetup luksOpen {lv_path} {mapper_name} -"
|
||||
else:
|
||||
open_auth_part = f"cryptsetup luksOpen {lv_path} {mapper_name} --key-file {key_file}"
|
||||
|
||||
self.logger.log("Step 2: Resizing container and underlying volumes.")
|
||||
script = f"""
|
||||
# Step 1: Resize the backing file
|
||||
fallocate -l {new_size_gb}G {container_path}
|
||||
|
||||
# Step 2: Connect loop device and resize PV
|
||||
LOOP_DEVICE=$(losetup -f --show {container_path})
|
||||
pvresize $LOOP_DEVICE
|
||||
|
||||
# Step 3: Extend LV to fill the new space
|
||||
lvextend -l +100%FREE {lv_path}
|
||||
|
||||
# Step 4: Re-open LUKS and resize it
|
||||
{open_auth_part}
|
||||
cryptsetup resize {mapper_name}
|
||||
|
||||
# Step 5: Check and resize the filesystem
|
||||
e2fsck -f /dev/mapper/{mapper_name}
|
||||
resize2fs /dev/mapper/{mapper_name}
|
||||
|
||||
# Step 6: Mount the resized filesystem
|
||||
mount /dev/mapper/{mapper_name} {mount_point}
|
||||
"""
|
||||
|
||||
if not self._execute_as_root(script):
|
||||
self.logger.log("Failed to resize LVM-based encrypted container.")
|
||||
# Attempt a basic cleanup after failed resize
|
||||
self.cleanup_encrypted_backup(base_dest_path)
|
||||
return False
|
||||
|
||||
self.logger.log("Successfully resized and remounted the encrypted container.")
|
||||
return True
|
||||
|
||||
def cleanup_encrypted_backup(self, base_dest_path: str):
|
||||
pybackup_dir = os.path.join(base_dest_path, "pybackup")
|
||||
mount_point = os.path.join(pybackup_dir, "encrypted")
|
||||
mapper_name = f"pybackup_{os.path.basename(base_dest_path.rstrip('/'))}"
|
||||
self.logger.log(f"Cleaning up encrypted backup: {mapper_name}")
|
||||
container_path = os.path.join(pybackup_dir, "pybackup_lvm.img")
|
||||
|
||||
base_name = os.path.basename(base_dest_path.rstrip('/'))
|
||||
vg_name = f"pybackup_vg_{base_name}"
|
||||
mapper_name = f"pybackup_luks_{base_name}"
|
||||
|
||||
self.logger.log(f"Cleaning up encrypted LVM backup for {base_dest_path}")
|
||||
|
||||
# Find the loop device associated with the container file
|
||||
# This is a bit tricky as the script that creates it is ephemeral.
|
||||
# We can parse `losetup -j <file>`
|
||||
find_loop_device_cmd = f"losetup -j {container_path} | cut -d: -f1"
|
||||
|
||||
script = f"""
|
||||
LOOP_DEVICE=$({find_loop_device_cmd} | head -n 1)
|
||||
|
||||
umount {mount_point} || echo "Mount point {mount_point} not found or already unmounted."
|
||||
cryptsetup luksClose {mapper_name} || echo "Mapper {mapper_name} not found or already closed."
|
||||
|
||||
# Deactivate VG only if it exists
|
||||
if vgdisplay {vg_name} >/dev/null 2>&1; then
|
||||
vgchange -an {vg_name} || echo "Could not deactivate volume group {vg_name}."
|
||||
fi
|
||||
|
||||
# Detach loop device only if it was found
|
||||
if [ -n "$LOOP_DEVICE" ] && losetup $LOOP_DEVICE >/dev/null 2>&1; then
|
||||
losetup -d $LOOP_DEVICE || echo "Could not detach loop device $LOOP_DEVICE."
|
||||
fi
|
||||
"""
|
||||
if not self._execute_as_root(script):
|
||||
self.logger.log("Encrypted backup cleanup script failed.")
|
||||
self.logger.log("Encrypted LVM backup cleanup script failed.")
|
||||
|
||||
def inspect_container(self, base_dest_path: str, listing_callback) -> List:
|
||||
"""Temporarily mounts an encrypted container to list its contents."""
|
||||
pybackup_dir = os.path.join(base_dest_path, "pybackup")
|
||||
container_path = os.path.join(pybackup_dir, "pybackup_lvm.img")
|
||||
mount_point = os.path.join(pybackup_dir, "encrypted")
|
||||
|
||||
if not os.path.exists(container_path):
|
||||
# Not an encrypted destination, just run the callback on the base path
|
||||
return listing_callback(base_dest_path)
|
||||
|
||||
self.logger.log(f"Encrypted destination detected. Attempting to inspect {container_path}")
|
||||
password = self.get_password_from_keyring("root")
|
||||
if not password:
|
||||
self.logger.log("No password in keyring. Cannot inspect encrypted container.")
|
||||
return []
|
||||
|
||||
# Use a dummy queue as we don't want to send UI updates during inspection
|
||||
from queue import Queue
|
||||
dummy_queue = Queue()
|
||||
|
||||
# The setup function handles unlocking and mounting
|
||||
mounted_path = self.setup_encrypted_backup(
|
||||
dummy_queue, base_dest_path, size_gb=0, password=password)
|
||||
|
||||
if not mounted_path:
|
||||
self.logger.log("Failed to mount container for inspection.")
|
||||
return []
|
||||
|
||||
try:
|
||||
# Run the actual listing logic on the mounted path
|
||||
self.logger.log(f"Container mounted at {mounted_path}. Running listing callback.")
|
||||
return listing_callback(base_dest_path, mounted_path=mounted_path)
|
||||
finally:
|
||||
self.logger.log("Inspection complete. Cleaning up mount.")
|
||||
self.cleanup_encrypted_backup(base_dest_path)
|
||||
|
||||
def _execute_as_root(self, script_content: str) -> bool:
|
||||
script_path = ''
|
||||
|
||||
@@ -83,10 +83,6 @@ class BackupContentFrame(ttk.Frame):
|
||||
action_button_frame = ttk.Frame(self, padding=10)
|
||||
action_button_frame.grid(row=2, column=0, sticky="ew")
|
||||
|
||||
self.toggle_encrypted_button = ttk.Button(
|
||||
action_button_frame, text=Msg.STR["show_encrypted_backups"], command=self._toggle_encrypted_view)
|
||||
self.toggle_encrypted_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.restore_button = ttk.Button(
|
||||
action_button_frame, text=Msg.STR["restore"], command=self._restore_selected, state="disabled")
|
||||
self.restore_button.pack(side=tk.LEFT, padx=5)
|
||||
@@ -121,39 +117,6 @@ class BackupContentFrame(ttk.Frame):
|
||||
def _edit_comment(self):
|
||||
self._get_active_subframe()._edit_comment()
|
||||
|
||||
def _toggle_encrypted_view(self):
|
||||
if not self.app.destination_path:
|
||||
MessageDialog(master=self.app, message_type="info",
|
||||
title=Msg.STR["info_menu"], text=Msg.STR["err_no_dest_folder"])
|
||||
return
|
||||
|
||||
if not self.viewing_encrypted:
|
||||
username = os.path.basename(self.app.destination_path.rstrip('/'))
|
||||
password = self.app.backup_manager.encryption_manager.get_password(
|
||||
username, confirm=False)
|
||||
if not password:
|
||||
return
|
||||
|
||||
mount_point = self.app.backup_manager.encryption_manager.unlock_container(
|
||||
self.app.destination_path, password)
|
||||
if mount_point:
|
||||
self.viewing_encrypted = True
|
||||
self.toggle_encrypted_button.config(
|
||||
text=Msg.STR["show_normal_backups"])
|
||||
self.show(mount_point)
|
||||
self.app.header_frame.refresh_status()
|
||||
else:
|
||||
MessageDialog(master=self.app, message_type="error",
|
||||
title=Msg.STR["error"], text=Msg.STR["err_unlock_failed"])
|
||||
else:
|
||||
self.app.backup_manager.encryption_manager.lock_container(
|
||||
self.app.destination_path)
|
||||
self.viewing_encrypted = False
|
||||
self.toggle_encrypted_button.config(
|
||||
text=Msg.STR["show_encrypted_backups"])
|
||||
self.show(self.app.destination_path)
|
||||
self.app.header_frame.refresh_status()
|
||||
|
||||
def _switch_view(self, index):
|
||||
self.current_view_index = index
|
||||
config_key = "last_encrypted_backup_content_view" if self.viewing_encrypted else "last_backup_content_view"
|
||||
@@ -212,6 +175,6 @@ class BackupContentFrame(ttk.Frame):
|
||||
self.deletion_status_frame.pack(side=tk.LEFT, padx=15)
|
||||
|
||||
def hide_deletion_status(self):
|
||||
app_logger.log("Hiding deletion status.")
|
||||
app_logger.log("Hiding deletion status text.")
|
||||
self.deletion_status_label.config(text="")
|
||||
self.deletion_animated_icon.stop("DISABLE")
|
||||
self.deletion_status_frame.pack_forget()
|
||||
|
||||
@@ -5,6 +5,7 @@ import os
|
||||
from core.pbp_app_config import Msg
|
||||
from pyimage_ui.comment_editor_dialog import CommentEditorDialog
|
||||
|
||||
|
||||
class SystemBackupContentFrame(ttk.Frame):
|
||||
def __init__(self, master, backup_manager, actions, parent_view, **kwargs):
|
||||
super().__init__(master, **kwargs)
|
||||
@@ -22,7 +23,8 @@ class SystemBackupContentFrame(ttk.Frame):
|
||||
]
|
||||
|
||||
columns = ("date", "time", "type", "size", "comment")
|
||||
self.content_tree = ttk.Treeview(self, columns=columns, show="headings")
|
||||
self.content_tree = ttk.Treeview(
|
||||
self, columns=columns, show="headings")
|
||||
self.content_tree.heading("date", text=Msg.STR["date"])
|
||||
self.content_tree.heading("time", text=Msg.STR["time"])
|
||||
self.content_tree.heading("type", text=Msg.STR["type"])
|
||||
@@ -49,15 +51,18 @@ class SystemBackupContentFrame(ttk.Frame):
|
||||
if not self.backup_path or not os.path.isdir(self.backup_path):
|
||||
return
|
||||
|
||||
self.system_backups_list = self.backup_manager.list_system_backups(self.backup_path)
|
||||
self.system_backups_list = self.backup_manager.list_system_backups(
|
||||
self.backup_path)
|
||||
|
||||
color_index = -1
|
||||
for i, backup_info in enumerate(self.system_backups_list):
|
||||
if backup_info.get("backup_type_base") == "Full":
|
||||
color_index = (color_index + 1) % len(self.tag_colors)
|
||||
full_tag, full_color, inc_tag, inc_color = self.tag_colors[color_index]
|
||||
self.content_tree.tag_configure(full_tag, foreground=full_color)
|
||||
self.content_tree.tag_configure(inc_tag, foreground=inc_color, font=("Helvetica", 10, "bold"))
|
||||
self.content_tree.tag_configure(
|
||||
full_tag, foreground=full_color)
|
||||
self.content_tree.tag_configure(
|
||||
inc_tag, foreground=inc_color, font=("Helvetica", 10, "bold"))
|
||||
current_tag = full_tag
|
||||
else:
|
||||
_, _, inc_tag, _ = self.tag_colors[color_index]
|
||||
@@ -82,7 +87,8 @@ class SystemBackupContentFrame(ttk.Frame):
|
||||
if not selected_item_id:
|
||||
return
|
||||
|
||||
info_file_path = os.path.join(self.backup_path, "pybackup", f"{selected_item_id}.txt")
|
||||
info_file_path = os.path.join(
|
||||
self.backup_path, "pybackup", f"{selected_item_id}.txt")
|
||||
|
||||
if not os.path.exists(info_file_path):
|
||||
self.backup_manager.update_comment(info_file_path, "")
|
||||
@@ -95,13 +101,15 @@ class SystemBackupContentFrame(ttk.Frame):
|
||||
if not selected_item_id:
|
||||
return
|
||||
|
||||
selected_backup = next((b for b in self.system_backups_list if b.get("folder_name") == selected_item_id), None)
|
||||
selected_backup = next((b for b in self.system_backups_list if b.get(
|
||||
"folder_name") == selected_item_id), None)
|
||||
|
||||
if not selected_backup:
|
||||
return
|
||||
|
||||
main_app = self.winfo_toplevel()
|
||||
restore_dest_path = main_app.config_manager.get_setting("restore_destination_path", "/")
|
||||
restore_dest_path = main_app.config_manager.get_setting(
|
||||
"restore_destination_path", "/")
|
||||
|
||||
if not restore_dest_path:
|
||||
return
|
||||
@@ -117,25 +125,35 @@ class SystemBackupContentFrame(ttk.Frame):
|
||||
if not selected_item_id:
|
||||
return
|
||||
|
||||
selected_backup = next((b for b in self.system_backups_list if b.get("folder_name") == selected_item_id), None)
|
||||
selected_backup = next((b for b in self.system_backups_list if b.get(
|
||||
"folder_name") == selected_item_id), None)
|
||||
|
||||
if not selected_backup:
|
||||
return
|
||||
|
||||
folder_to_delete = selected_backup['full_path']
|
||||
info_file_to_delete = os.path.join(self.backup_path, "pybackup", f"{selected_item_id}.txt")
|
||||
is_encrypted = selected_backup['is_encrypted']
|
||||
password = None
|
||||
|
||||
dialog = MessageDialog(master=self, message_type="warning",
|
||||
title=Msg.STR["confirm_delete_title"],
|
||||
text=Msg.STR["confirm_delete_text"].format(
|
||||
folder_name=selected_item_id),
|
||||
buttons=["ok_cancel"])
|
||||
if dialog.get_result() != "ok":
|
||||
return
|
||||
if is_encrypted:
|
||||
# Get password in the UI thread before starting the background task
|
||||
password = self.backup_manager.encryption_manager.get_password("root", confirm=True)
|
||||
if not password:
|
||||
self.actions.logger.log("Password entry cancelled, aborting deletion.")
|
||||
return
|
||||
|
||||
info_file_to_delete = os.path.join(
|
||||
self.backup_path, "pybackup", f"{selected_item_id}{'_encrypted' if is_encrypted else ''}.txt")
|
||||
|
||||
self.actions._set_ui_state(False)
|
||||
# This needs to be adapted, as the deletion status is now in the parent view
|
||||
# self.master.show_deletion_status(Msg.STR["deleting_backup_in_progress"])
|
||||
self.parent_view.show_deletion_status(Msg.STR["deleting_backup_in_progress"])
|
||||
|
||||
self.backup_manager.start_delete_system_backup(
|
||||
folder_to_delete, self.winfo_toplevel().queue)
|
||||
self.backup_manager.start_delete_backup(
|
||||
path_to_delete=folder_to_delete,
|
||||
info_file_path=info_file_to_delete,
|
||||
is_encrypted=is_encrypted,
|
||||
is_system=True,
|
||||
base_dest_path=self.backup_path,
|
||||
password=password,
|
||||
queue=self.winfo_toplevel().queue
|
||||
)
|
||||
|
||||
@@ -85,29 +85,35 @@ class UserBackupContentFrame(ttk.Frame):
|
||||
if not selected_item_id:
|
||||
return
|
||||
|
||||
selected_backup = next((b for b in self.user_backups_list if b.get("folder_name") == selected_item_id), None)
|
||||
if not selected_backup:
|
||||
return
|
||||
selected_backup = next((b for b in self.user_backups_list if b.get("folder_name") == selected_item_id), None)
|
||||
selected_backup = next((b for b in self.user_backups_list if b.get(
|
||||
"folder_name") == selected_item_id), None)
|
||||
|
||||
if not selected_backup:
|
||||
return
|
||||
|
||||
folder_to_delete = selected_backup['full_path']
|
||||
info_file_to_delete = os.path.join(self.backup_path, "pybackup", f"{selected_item_id}.txt")
|
||||
is_encrypted = selected_backup['is_encrypted']
|
||||
password = None
|
||||
|
||||
dialog = MessageDialog(master=self, message_type="warning",
|
||||
title=Msg.STR["confirm_delete_title"],
|
||||
text=Msg.STR["confirm_delete_text"].format(
|
||||
folder_name=selected_item_id),
|
||||
buttons=["ok_cancel"])
|
||||
if dialog.get_result() != "ok":
|
||||
return
|
||||
if is_encrypted:
|
||||
# Get password in the UI thread before starting the background task
|
||||
password = self.backup_manager.encryption_manager.get_password("root", confirm=True)
|
||||
if not password:
|
||||
self.actions.logger.log("Password entry cancelled, aborting deletion.")
|
||||
return
|
||||
|
||||
try:
|
||||
if os.path.isdir(folder_to_delete):
|
||||
shutil.rmtree(folder_to_delete)
|
||||
if os.path.exists(info_file_to_delete):
|
||||
os.remove(info_file_to_delete)
|
||||
self._load_backup_content()
|
||||
except Exception as e:
|
||||
MessageDialog(master=self, message_type="error",
|
||||
title=Msg.STR["error"], text=str(e))
|
||||
info_file_to_delete = os.path.join(
|
||||
self.backup_path, "pybackup", f"{selected_item_id}{'_encrypted' if is_encrypted else ''}.txt")
|
||||
|
||||
self.actions._set_ui_state(False)
|
||||
self.parent_view.show_deletion_status(Msg.STR["deleting_backup_in_progress"])
|
||||
|
||||
self.backup_manager.start_delete_backup(
|
||||
path_to_delete=folder_to_delete,
|
||||
info_file_path=info_file_to_delete,
|
||||
is_encrypted=is_encrypted,
|
||||
is_system=False,
|
||||
base_dest_path=self.backup_path,
|
||||
password=password,
|
||||
queue=self.winfo_toplevel().queue
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user