add lvm for img encrypt zo automatic resize for new encrypt backup

This commit is contained in:
2025-09-07 01:54:16 +02:00
parent 4d70e0eee0
commit 4a700194c3
5 changed files with 541 additions and 231 deletions

View File

@@ -4,6 +4,8 @@ import threading
import re
import signal
import datetime
import math
import shutil
from typing import Optional, List, Dict, Any
from pathlib import Path
from crontab import CronTab
@@ -33,42 +35,52 @@ class BackupManager:
def _inhibit_screensaver(self):
"""Prevents the screensaver and auto-suspend during a backup."""
if not shutil.which("gdbus"):
self.logger.log("gdbus command not found, cannot inhibit screensaver.")
self.logger.log(
"gdbus command not found, cannot inhibit screensaver.")
return
try:
self.logger.log("Attempting to inhibit screensaver and power management.")
self.logger.log(
"Attempting to inhibit screensaver and power management.")
command = [
"gdbus", "call", "--session", "--dest", "org.freedesktop.ScreenSaver",
"--object-path", "/org/freedesktop/ScreenSaver",
"--method", "org.freedesktop.ScreenSaver.Inhibit",
"Py-Backup", "Backup in progress"
]
result = subprocess.run(command, capture_output=True, text=True, check=True)
result = subprocess.run(
command, capture_output=True, text=True, check=True)
# Output is like "(uint32 12345,)", we need to extract the number.
match = re.search(r'uint32\s+(\d+)', result.stdout)
if match:
self.inhibit_cookie = int(match.group(1))
self.logger.log(f"Successfully inhibited screensaver with cookie {self.inhibit_cookie}")
self.logger.log(
f"Successfully inhibited screensaver with cookie {self.inhibit_cookie}")
else:
self.logger.log(f"Could not parse inhibit cookie from gdbus output: {result.stdout}")
self.logger.log(
f"Could not parse inhibit cookie from gdbus output: {result.stdout}")
except FileNotFoundError:
self.logger.log("gdbus command not found, cannot inhibit screensaver.")
self.logger.log(
"gdbus command not found, cannot inhibit screensaver.")
except subprocess.CalledProcessError as e:
self.logger.log(f"Failed to inhibit screensaver. D-Bus call failed: {e.stderr}")
self.logger.log(
f"Failed to inhibit screensaver. D-Bus call failed: {e.stderr}")
except Exception as e:
self.logger.log(f"An unexpected error occurred while inhibiting screensaver: {e}")
self.logger.log(
f"An unexpected error occurred while inhibiting screensaver: {e}")
def _uninhibit_screensaver(self):
"""Releases the screensaver and auto-suspend lock."""
if self.inhibit_cookie is None:
return
if not shutil.which("gdbus"):
self.logger.log("gdbus command not found, cannot uninhibit screensaver.")
self.logger.log(
"gdbus command not found, cannot uninhibit screensaver.")
return
try:
self.logger.log(f"Attempting to uninhibit screensaver with cookie {self.inhibit_cookie}")
self.logger.log(
f"Attempting to uninhibit screensaver with cookie {self.inhibit_cookie}")
command = [
"gdbus", "call", "--session", "--dest", "org.freedesktop.ScreenSaver",
"--object-path", "/org/freedesktop/ScreenSaver",
@@ -85,13 +97,20 @@ class BackupManager:
def start_backup(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool = False, exclude_files: Optional[List[Path]] = None, source_size: int = 0, is_compressed: bool = False, is_encrypted: bool = False, mode: str = "incremental", use_trash_bin: bool = False, no_trash_bin: bool = False):
self.is_system_process = is_system
self._inhibit_screensaver()
mount_point = None
if is_encrypted:
mount_point = self.encryption_manager.mount(os.path.dirname(dest_path), queue)
# Calculate size in GB, add a 2GB buffer
size_in_gb = (source_size // (1024**3)) + 2
self.logger.log(
f"Calculated container size: {size_in_gb} GB for source size {source_size} bytes.")
mount_point = self.encryption_manager.mount(
os.path.dirname(dest_path), queue, size_gb=size_in_gb)
if not mount_point:
self.logger.log("Failed to mount encrypted destination. Aborting backup.")
queue.put(('completion', {'status': 'error', 'returncode': -1}))
self.logger.log(
"Failed to mount encrypted destination. Aborting backup.")
queue.put(
('completion', {'status': 'error', 'returncode': -1}))
return None
thread = threading.Thread(target=self._run_backup_path, args=(
@@ -108,27 +127,60 @@ class BackupManager:
user_source_name = None
if not is_system:
# Extract source name from backup_name (e.g., 2025-09-06_10-00-00_user_MyDocs_full.txt -> MyDocs)
match = re.match(r"^\d{2}-\d{2}-\d{4}_\d{2}:\d{2}:\d{2}_user_(.+?)_(full|incremental)(_encrypted)?$", backup_name)
match = re.match(
r"^\d{2}-\d{2}-\d{4}_\d{2}:\d{2}:\d{2}_user_(.+?)_(full|incremental)(_encrypted)?$", backup_name)
if match:
user_source_name = match.group(1)
else:
self.logger.log(f"Could not parse user source name from backup_name: {backup_name}")
self.logger.log(
f"Could not parse user source name from backup_name: {backup_name}")
# ... (rsync_base_dest and rsync_dest calculation) ...
if is_encrypted:
if not mount_point:
self.logger.log(
"Critical error: Encrypted backup run without a mount point.")
queue.put(
('completion', {'status': 'error', 'returncode': -1}))
return
if not is_system:
rsync_base_dest = os.path.join(mount_point, "user_encrypt")
else:
rsync_base_dest = mount_point
else:
if not is_system:
rsync_base_dest = os.path.join(
pybackup_dir, "user_backups")
else:
rsync_base_dest = pybackup_dir
latest_backup_path = self._find_latest_backup(rsync_base_dest, user_source_name)
rsync_dest = os.path.join(rsync_base_dest, backup_name)
# Ensure the base destination for rsync exists, especially for the first run
if not os.path.exists(rsync_base_dest):
if is_system or is_encrypted:
self.logger.log(
f"Creating privileged base destination: {rsync_base_dest}")
self.encryption_manager._execute_as_root(
f"mkdir -p {rsync_base_dest}")
else:
self.logger.log(
f"Creating base destination: {rsync_base_dest}")
os.makedirs(rsync_base_dest, exist_ok=True)
latest_backup_path = self._find_latest_backup(
rsync_base_dest, user_source_name)
# Determine actual mode for user backups
if not is_system and not latest_backup_path:
mode = "full" # If no previous backup, force full
mode = "full" # If no previous backup, force full
elif not is_system and latest_backup_path:
mode = "incremental" # If previous backup exists, default to incremental
mode = "incremental" # If previous backup exists, default to incremental
command = []
if is_system or is_encrypted:
command.extend(['pkexec', 'rsync', '-aAXHv'])
command.extend(['pkexec', 'rsync', '-aAXHvL'])
else:
command.extend(['rsync', '-av'])
command.extend(['rsync', '-avL'])
if mode == "incremental" and latest_backup_path and not is_dry_run:
command.append(f"--link-dest={latest_backup_path}")
@@ -137,7 +189,8 @@ class BackupManager:
if exclude_files:
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}")
command.append(
f"--exclude-from={AppConfig.MANUAL_EXCLUDE_LIST_PATH}")
if is_dry_run:
command.append('--dry-run')
@@ -145,38 +198,140 @@ class BackupManager:
if not is_system:
trash_bin_path = os.path.join(rsync_base_dest, ".Trash")
if use_trash_bin:
command.extend(['--backup', f'--backup-dir={trash_bin_path}', '--delete'])
command.extend(
['--backup', f'--backup-dir={trash_bin_path}', '--delete'])
# Exclude the trash bin itself from the backup
command.append(f"--exclude={os.path.basename(trash_bin_path)}/")
command.append(
f"--exclude={os.path.basename(trash_bin_path)}/")
elif no_trash_bin:
command.append('--delete')
# Exclude the trash bin itself from the backup if it exists from previous use_trash_bin
command.append(f"--exclude={os.path.basename(trash_bin_path)}/")
command.append(
f"--exclude={os.path.basename(trash_bin_path)}/")
command.extend([source_path, rsync_dest])
self.logger.log(f"Rsync command: {' '.join(command)}")
transferred_size, total_size, stderr = self._execute_rsync(queue, command)
return_code = self.process.returncode if self.process else -1
max_retries = 1
for i in range(max_retries + 1):
transferred_size, total_size, stderr = self._execute_rsync(
queue, command)
return_code = self.process.returncode if self.process else -1
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.")
# Resize logic would need to be adapted for the new mount management
# For now, we just log the error
queue.put(('error', "Container-Vergrößerung fehlgeschlagen. Backup abgebrochen."))
if is_encrypted and "No space left on device" in stderr and i < max_retries:
self.logger.log(
"Rsync failed due to lack of space. Attempting to dynamically resize container.")
try:
# Get necessary paths and names
container_path = os.path.join(
base_dest_path, "pybackup", "user_encrypt", "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}"
mapper_path = f"/dev/mapper/{mapper_name}"
# Calculate new required size
current_container_size_bytes = os.path.getsize(
container_path)
needed_total_bytes = source_size * 1.1 # 10% buffer
if needed_total_bytes <= current_container_size_bytes:
# If container is already big enough but full, just add a fixed amount.
gb_to_add = 5
new_total_size_bytes = current_container_size_bytes + \
(gb_to_add * (1024**3))
else:
bytes_to_add = needed_total_bytes - current_container_size_bytes
gb_to_add = math.ceil(bytes_to_add / (1024**3))
new_total_size_bytes = current_container_size_bytes + \
(gb_to_add * (1024**3))
# Check physical drive space
physical_drive_stats = shutil.disk_usage(
base_dest_path)
if physical_drive_stats.free < (gb_to_add * (1024**3)):
self.logger.log(
f"Not enough physical space on device {base_dest_path} to expand container.")
queue.put(
('error', "Nicht genügend physischer Speicherplatz für die Vergrößerung."))
break
self.logger.log(
f"Calculated expansion size: {gb_to_add} GB")
resize_script = f'''
set -e
# Find loop device associated with the container
LOOP_DEVICE=$(losetup -j {container_path} | cut -d: -f1 | head -n 1)
if [ -z "$LOOP_DEVICE" ]; then
echo "Error: Could not find loop device for {container_path}"
exit 1
fi
# 1. Extend backing file (truncate can extend)
truncate -s {int(new_total_size_bytes)} {container_path}
# 2. Resize loop device
losetup -c $LOOP_DEVICE
# 3. Resize PV
pvresize $LOOP_DEVICE
# 4. Extend LV to fill all new space in VG
lvextend -l +100%FREE {lv_path}
# 5. Resize filesystem
resize2fs {mapper_path}
'''
if self.encryption_manager._execute_as_root(resize_script):
self.logger.log(
"Container resized successfully. Unmounting and remounting to apply changes.")
# Unmount to ensure kernel recognizes the new size
self .encryption_manager._unmount_encrypted_backup(
base_dest_path)
# Remount
new_mount_point = self.encryption_manager.mount(
base_dest_path, queue)
if not new_mount_point:
self.logger.log(
"Failed to remount after resize. Aborting.")
break
mount_point = new_mount_point
self.logger.log(
"Remount successful. Retrying rsync...")
queue.put(
('status_update', f"Container um {gb_to_add} GB vergrößert. Setze Backup fort..."))
continue
else:
self.logger.log("Failed to resize container.")
queue.put(
('error', "Container-Vergrößerung fehlgeschlagen. Backup abgebrochen."))
break
except Exception as e:
self.logger.log(
f"Error during dynamic resize calculation: {e}")
break
else:
# Break the loop if rsync was successful or retries are exhausted
break
if self.process:
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
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}))
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}))
finally:
self._uninhibit_screensaver()
@@ -191,12 +346,14 @@ class BackupManager:
mounted_path = os.path.join(pybackup_dir, "encrypted")
else:
return [], []
return self._list_all_backups_from_path(base_dest_path, mounted_path)
def _list_all_backups_from_path(self, base_dest_path: str, mounted_path: Optional[str] = None):
system_backups = self._list_system_backups_from_path(base_dest_path, mounted_path)
user_backups = self._list_user_backups_from_path(base_dest_path, mounted_path)
system_backups = self._list_system_backups_from_path(
base_dest_path, mounted_path)
user_backups = self._list_user_backups_from_path(
base_dest_path, mounted_path)
return system_backups, user_backups
def list_system_backups(self, base_dest_path: str, mount_if_needed: bool = True) -> Optional[List[Dict[str, str]]]:
@@ -205,11 +362,13 @@ class BackupManager:
if is_encrypted:
if not self.encryption_manager.is_mounted(base_dest_path):
if mount_if_needed:
self.logger.log("Mounting needed for listing system backups.")
mounted_path = self.encryption_manager.mount(base_dest_path)
self.logger.log(
"Mounting needed for listing system backups.")
mounted_path = self.encryption_manager.mount(
base_dest_path)
else:
return None
if self.encryption_manager.is_mounted(base_dest_path):
pybackup_dir = os.path.join(base_dest_path, "pybackup")
mounted_path = os.path.join(pybackup_dir, "encrypted")
@@ -239,8 +398,10 @@ class BackupManager:
else:
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 = ""
info_file_path = os.path.join(pybackup_dir, item)
@@ -249,11 +410,13 @@ class BackupManager:
with open(info_file_path, 'r') as f:
for line in f:
if line.strip().lower().startswith("originalgröße:"):
backup_size = line.split(":", 1)[1].strip().split('(')[0].strip()
backup_size = line.split(
":", 1)[1].strip().split('(')[0].strip()
elif line.strip().lower().startswith("kommentar:"):
comment = line.split(":", 1)[1].strip()
except Exception as e:
self.logger.log(f"Could not read info file {info_file_path}: {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,
@@ -266,14 +429,19 @@ class BackupManager:
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:
if not current_group: current_group.append(backup)
else: current_group.append(backup)
if current_group: grouped_backups.append(current_group)
if not current_group:
current_group.append(backup)
else:
current_group.append(backup)
if current_group:
grouped_backups.append(current_group)
grouped_backups.sort(key=lambda g: g[0]['datetime'], reverse=True)
final_sorted_list = [item for group in grouped_backups for item in group]
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, mount_if_needed: bool = True) -> Optional[List[Dict[str, str]]]:
@@ -282,8 +450,10 @@ class BackupManager:
if is_encrypted:
if not self.encryption_manager.is_mounted(base_dest_path):
if mount_if_needed:
self.logger.log("Mounting needed for listing user backups.")
mounted_path = self.encryption_manager.mount(base_dest_path)
self.logger.log(
"Mounting needed for listing user backups.")
mounted_path = self.encryption_manager.mount(
base_dest_path)
else:
return None
if self.encryption_manager.is_mounted(base_dest_path):
@@ -307,19 +477,21 @@ class BackupManager:
continue
date_str, time_str, source_name, backup_type_base, enc_suffix = match.groups()
is_encrypted = (enc_suffix is not None)
is_compressed = False # User backups are not compressed in this context
is_compressed = False # User backups are not compressed in this context
backup_name = item.replace(".txt", "").replace("_encrypted", "")
if mounted_path:
user_backup_dir = os.path.join(mounted_path, "user_encrypt")
full_path = os.path.join(user_backup_dir, backup_name)
else:
user_backups_dir = os.path.join(pybackup_dir, "user_backups")
full_path = os.path.join(user_backups_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 = ""
@@ -329,11 +501,13 @@ class BackupManager:
with open(info_file_path, 'r') as f:
for line in f:
if line.strip().lower().startswith("originalgröße:"):
backup_size = line.split(":", 1)[1].strip().split('(')[0].strip()
backup_size = line.split(
":", 1)[1].strip().split('(')[0].strip()
elif line.strip().lower().startswith("kommentar:"):
comment = line.split(":", 1)[1].strip()
except Exception as e:
self.logger.log(f"Could not read info file {info_file_path}: {e}")
self.logger.log(
f"Could not read info file {info_file_path}: {e}")
user_backups.append({
"date": date_str, "time": time_str, "type": backup_type,
"size": backup_size, "folder_name": backup_name, "full_path": full_path,
@@ -349,8 +523,9 @@ class BackupManager:
def _find_latest_backup(self, base_backup_path: str, source_name: Optional[str] = None) -> Optional[str]:
"""Finds the most recent backup directory in a given path, optionally filtered by source name."""
self.logger.log(f"Searching for latest backup in: {base_backup_path} for source: {source_name or 'All'}")
self.logger.log(
f"Searching for latest backup in: {base_backup_path} for source: {source_name or 'All'}")
backup_names = []
if os.path.isdir(base_backup_path):
for item in os.listdir(base_backup_path):
@@ -362,7 +537,8 @@ class BackupManager:
backup_names.append(item)
else:
# For system backups or if no source_name is provided, include all
if "_system_" in item or "_user_" not in item: # Simple check to exclude other user backups if source_name is None
# Simple check to exclude other user backups if source_name is None
if "_system_" in item or "_user_" not in item:
backup_names.append(item)
# Sort by date and time (assuming format YYYY-MM-DD_HH-MM-SS or similar at the beginning)
@@ -372,22 +548,24 @@ class BackupManager:
if not backup_names:
self.logger.log("No previous backups found to link against.")
return None
latest_backup_name = backup_names[0]
latest_backup_path = os.path.join(base_backup_path, latest_backup_name)
if os.path.isdir(latest_backup_path):
self.logger.log(f"Found latest backup for --link-dest: {latest_backup_path}")
self.logger.log(
f"Found latest backup for --link-dest: {latest_backup_path}")
return latest_backup_path
self.logger.log(f"Latest backup entry '{latest_backup_name}' was not a directory. No link will be used.")
self.logger.log(
f"Latest backup entry '{latest_backup_name}' was not a directory. No link will be used.")
return None
def _create_info_file(self, pybackup_dir: str, backup_name: str, source_size: int, is_encrypted: bool):
try:
info_filename = f"{backup_name}_encrypted.txt" if is_encrypted else f"{backup_name}.txt"
info_file_path = os.path.join(pybackup_dir, info_filename)
original_bytes = source_size
if source_size > 0:
power = 1024
@@ -474,16 +652,18 @@ class BackupManager:
transferred_size = 0
total_size = 0
summary_regex = re.compile(r"sent ([\d,. ]+) bytes\s+received ([\d,. ]+) bytes")
summary_regex = re.compile(
r"sent ([\d,. ]+) bytes\s+received ([\d,. ]+) bytes")
total_size_regex = re.compile(r"total size is ([\d,. ]+) speedup")
for line in reversed(output_lines):
match = summary_regex.search(line)
if match and transferred_size == 0:
try:
sent_str = match.group(1).replace(',', '').replace('.', '')
received_str = match.group(2).replace(',', '').replace('.', '')
sent_str = match.group(1).replace(
',', '').replace('.', '')
received_str = match.group(2).replace(
',', '').replace('.', '')
bytes_sent = int(sent_str)
bytes_received = int(received_str)
transferred_size = bytes_sent + bytes_received
@@ -492,18 +672,22 @@ class BackupManager:
except (ValueError, IndexError) as e:
self.logger.log(
f"Could not parse sent/received bytes from line: '{line}'. Error: {e}")
total_match = total_size_regex.search(line)
if total_match and total_size == 0:
try:
total_size_str = total_match.group(1).replace(',', '').replace('.', '')
total_size_str = total_match.group(
1).replace(',', '').replace('.', '')
total_size = int(total_size_str)
self.logger.log(f"Detected total size from summary: {total_size} bytes")
except(ValueError, IndexError) as e:
self.logger.log(f"Could not parse total size from line: '{line}'. Error: {e}")
self.logger.log(
f"Detected total size from summary: {total_size} bytes")
except (ValueError, IndexError) as e:
self.logger.log(
f"Could not parse total size from line: '{line}'. Error: {e}")
self.logger.log(
f"_execute_rsync final parsed values: transferred_size={transferred_size}, total_size={total_size}")
self.logger.log(f"_execute_rsync final parsed values: transferred_size={transferred_size}, total_size={total_size}")
if transferred_size == 0:
bytes_sent = 0
bytes_received = 0
@@ -511,16 +695,20 @@ class BackupManager:
if line.strip().startswith('Total bytes sent:'):
try:
size_str = line.split(':')[1].strip()
bytes_sent = int(size_str.replace(',', '').replace('.', ''))
bytes_sent = int(size_str.replace(
',', '').replace('.', ''))
except (ValueError, IndexError):
self.logger.log(f"Could not parse bytes sent from line: {line}")
self.logger.log(
f"Could not parse bytes sent from line: {line}")
elif line.strip().startswith('Total bytes received:'):
try:
size_str = line.split(':')[1].strip()
bytes_received = int(size_str.replace(',', '').replace('.', ''))
bytes_received = int(
size_str.replace(',', '').replace('.', ''))
except (ValueError, IndexError):
self.logger.log(f"Could not parse bytes received from line: {line}")
self.logger.log(
f"Could not parse bytes received from line: {line}")
if bytes_sent > 0 or bytes_received > 0:
transferred_size = bytes_sent + bytes_received
self.logger.log(
@@ -536,7 +724,7 @@ class BackupManager:
except Exception as e:
self.logger.log(f"An unexpected error occurred: {e}")
queue.put(('error', None))
return transferred_size, total_size, stderr_output
def start_restore(self, source_path: str, dest_path: str, is_compressed: bool):
@@ -544,7 +732,8 @@ class BackupManager:
try:
queue = self.app.queue
except AttributeError:
self.logger.log("Could not get queue from app instance. Restore progress will not be reported.")
self.logger.log(
"Could not get queue from app instance. Restore progress will not be reported.")
from queue import Queue
queue = Queue()
@@ -572,10 +761,12 @@ class BackupManager:
status = 'error'
except Exception as e:
self.logger.log(f"An unexpected error occurred during restore: {e}")
self.logger.log(
f"An unexpected error occurred during restore: {e}")
status = 'error'
finally:
queue.put(('completion', {'status': status, 'returncode': 0 if status == 'success' else 1}))
queue.put(
('completion', {'status': status, 'returncode': 0 if status == 'success' else 1}))
def get_scheduled_jobs(self) -> List[Dict[str, Any]]:
jobs_list = []
@@ -650,7 +841,8 @@ class BackupManager:
if line.strip().lower().startswith("kommentar:"):
return line.split(":", 1)[1].strip()
except Exception as e:
self.logger.log(f"Error reading comment from {info_file_path}: {e}")
self.logger.log(
f"Error reading comment from {info_file_path}: {e}")
return ""
def update_comment(self, info_file_path: str, new_comment: str):
@@ -670,13 +862,14 @@ class BackupManager:
comment_found = True
else:
new_lines.append(line)
if not comment_found and new_comment:
new_lines.append(f"Kommentar: {new_comment}\n")
with open(info_file_path, 'w') as f:
f.writelines(new_lines)
self.logger.log(f"Successfully updated comment in {info_file_path}")
self.logger.log(
f"Successfully updated comment in {info_file_path}")
except Exception as e:
self.logger.log(f"Error updating comment in {info_file_path}: {e}")
@@ -755,56 +948,67 @@ class BackupManager:
"""Runs the deletion and puts a message on the queue when done."""
try:
if is_encrypted:
self.logger.log(f"Starting encrypted deletion for {path_to_delete}")
self.logger.log(
f"Starting encrypted deletion for {path_to_delete}")
if not password:
self.logger.log("Password not provided for encrypted deletion.")
self.logger.log(
"Password not provided for encrypted deletion.")
queue.put(('deletion_complete', False))
return
mount_point = self.encryption_manager.mount(base_dest_path, queue)
mount_point = self.encryption_manager.mount(
base_dest_path, queue)
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}")
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}'
"""
success = self.encryption_manager._execute_as_root(script_content)
success = self.encryption_manager._execute_as_root(
script_content)
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.")
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}")
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}")
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}")
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}")
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}")
self.logger.log(
f"Failed to delete unencrypted user backup {path_to_delete}: {e}")
queue.put(('deletion_complete', False))
except Exception as e:
@@ -835,7 +1039,8 @@ set -e
if is_system or is_encrypted:
self.logger.log("Executing compression and cleanup as root.")
if self.encryption_manager._execute_as_root(script_content):
self.logger.log("Compression and cleanup script executed successfully.")
self.logger.log(
"Compression and cleanup script executed successfully.")
return True
else:
self.logger.log("Compression and cleanup script failed.")
@@ -843,20 +1048,25 @@ set -e
else:
try:
self.logger.log(f"Executing local command: {tar_command}")
tar_result = subprocess.run(tar_command, shell=True, capture_output=True, text=True, check=True)
self.logger.log(f"tar command successful. Output: {tar_result.stdout}")
tar_result = subprocess.run(
tar_command, shell=True, capture_output=True, text=True, check=True)
self.logger.log(
f"tar command successful. Output: {tar_result.stdout}")
self.logger.log(f"Executing local command: {rm_command}")
rm_result = subprocess.run(rm_command, shell=True, capture_output=True, text=True, check=True)
self.logger.log(f"rm command successful. Output: {rm_result.stdout}")
rm_result = subprocess.run(
rm_command, shell=True, capture_output=True, text=True, check=True)
self.logger.log(
f"rm command successful. Output: {rm_result.stdout}")
return True
except subprocess.CalledProcessError as e:
self.logger.log(f"A command failed during local compression/cleanup. Return code: {e.returncode}")
self.logger.log(
f"A command failed during local compression/cleanup. Return code: {e.returncode}")
self.logger.log(f"Stdout: {e.stdout}")
self.logger.log(f"Stderr: {e.stderr}")
return False
except Exception as e:
self.logger.log(f"An unexpected error occurred during local compression/cleanup: {e}")
self.logger.log(
f"An unexpected error occurred during local compression/cleanup: {e}")
return False

View File

@@ -25,6 +25,7 @@ class EncryptionManager:
self.session_password = None
self.mounted_destinations = set()
self.auth_method = None
self.is_mounting = False
def get_key_file_path(self, base_dest_path: str) -> str:
"""Generates the standard path for the key file for a given destination."""
@@ -94,60 +95,68 @@ class EncryptionManager:
mount_point = os.path.join(pybackup_dir, "encrypted")
return os.path.ismount(mount_point)
def mount(self, base_dest_path: str, queue=None) -> Optional[str]:
if not self.is_encrypted(base_dest_path):
self.auth_method = None
return None
if self.is_mounted(base_dest_path):
self.mounted_destinations.add(base_dest_path)
pybackup_dir = os.path.join(base_dest_path, "pybackup")
return os.path.join(pybackup_dir, "encrypted")
username = os.path.basename(base_dest_path.rstrip('/'))
# Use a dummy queue if none is provided
if queue is None:
from queue import Queue
queue = Queue()
# 1. Try keyring
password = self.get_password_from_keyring(username)
if password:
self.logger.log("Found password in keyring. Attempting to mount.")
mount_point = self._setup_encrypted_backup(queue, base_dest_path, 0, password=password)
if mount_point:
self.auth_method = "keyring"
self.mounted_destinations.add(base_dest_path)
return mount_point
# 2. Try key file
key_file_path = self.get_key_file_path(base_dest_path)
if os.path.exists(key_file_path):
self.logger.log(f"Found key file at {key_file_path}. Attempting to mount.")
mount_point = self._setup_encrypted_backup(queue, base_dest_path, 0, key_file=key_file_path)
if mount_point:
self.auth_method = "keyfile"
self.mounted_destinations.add(base_dest_path)
return mount_point
# 3. Prompt for password
self.logger.log("No password in keyring or key file found. Prompting user.")
password = self.get_password(username, confirm=False)
if not password:
self.logger.log("No password provided, cannot mount container.")
self.auth_method = None
def mount(self, base_dest_path: str, queue=None, size_gb: int = 0) -> Optional[str]:
if self.is_mounting:
self.logger.log("Mount process already in progress. Aborting new request.")
return None
mount_point = self._setup_encrypted_backup(queue, base_dest_path, 0, password=password)
if mount_point:
self.auth_method = "password"
self.mounted_destinations.add(base_dest_path)
return mount_point
self.is_mounting = True
try:
if self.is_mounted(base_dest_path):
self.mounted_destinations.add(base_dest_path)
pybackup_dir = os.path.join(base_dest_path, "pybackup")
return os.path.join(pybackup_dir, "encrypted")
username = os.path.basename(base_dest_path.rstrip('/'))
# Use a dummy queue if none is provided
if queue is None:
from queue import Queue
queue = Queue()
# 1. Try keyring
password = self.get_password_from_keyring(username)
if password:
self.logger.log("Found password in keyring. Attempting to mount.")
mount_point = self._setup_encrypted_backup(queue, base_dest_path, size_gb, password=password)
if mount_point:
self.auth_method = "keyring"
self.mounted_destinations.add(base_dest_path)
return mount_point
else:
# If mounting with keyring key fails, stop here and report error.
self.logger.log("Mounting with keyring password failed. Aborting mount attempt.")
return None
# 2. Try key file
key_file_path = self.get_key_file_path(base_dest_path)
if os.path.exists(key_file_path):
self.logger.log(f"Found key file at {key_file_path}. Attempting to mount.")
mount_point = self._setup_encrypted_backup(queue, base_dest_path, size_gb, key_file=key_file_path)
if mount_point:
self.auth_method = "keyfile"
self.mounted_destinations.add(base_dest_path)
return mount_point
# 3. Prompt for password
self.logger.log("No password in keyring or key file found. Prompting user.")
password = self.get_password(username, confirm=False)
if not password:
self.logger.log("No password provided, cannot mount container.")
self.auth_method = None
return None
mount_point = self._setup_encrypted_backup(queue, base_dest_path, size_gb, password=password)
if mount_point:
self.auth_method = "password"
self.mounted_destinations.add(base_dest_path)
return mount_point
finally:
self.is_mounting = False
def unmount(self, base_dest_path: str):
if base_dest_path in self.mounted_destinations:
self._cleanup_encrypted_backup(base_dest_path)
self._unmount_encrypted_backup(base_dest_path)
self.mounted_destinations.remove(base_dest_path)
def unmount_all(self):
@@ -156,6 +165,35 @@ class EncryptionManager:
for path in list(self.mounted_destinations):
self.unmount(path)
def _unmount_encrypted_backup(self, base_dest_path: str):
""" Gently unmounts the container without destroying LVM structures. """
pybackup_dir = os.path.join(base_dest_path, "pybackup")
user_encrypt_dir = os.path.join(pybackup_dir, "user_encrypt")
mount_point = os.path.join(pybackup_dir, "encrypted")
container_path = os.path.join(user_encrypt_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"Unmounting encrypted LVM backup for {base_dest_path}")
find_loop_device_cmd = f"losetup -j {container_path} | cut -d: -f1"
script = f"""
LOOP_DEVICE=$({find_loop_device_cmd} | head -n 1)
if mountpoint -q {mount_point}; then
umount {mount_point} || echo "Umount failed, continuing..."
fi
if [ -e /dev/mapper/{mapper_name} ]; then
cryptsetup luksClose {mapper_name} || echo "luksClose failed, continuing..."
fi
if vgdisplay {vg_name} >/dev/null 2>&1; then
vgchange -an {vg_name} || echo "Could not deactivate volume group {vg_name}."
fi
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 LVM backup unmount script failed.")
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 LVM-based encrypted container at {base_dest_path}")
@@ -199,7 +237,7 @@ class EncryptionManager:
"""
if not self._execute_as_root(script):
self.logger.log("Failed to unlock existing LVM container.")
self._cleanup_encrypted_backup(base_dest_path)
self._destroy_encrypted_structures(base_dest_path)
return None
self.logger.log(f"Encrypted LVM container unlocked and mounted at {mount_point}")
return mount_point
@@ -222,7 +260,7 @@ class EncryptionManager:
"""
if not self._execute_as_root(script):
self.logger.log("Failed to create and setup LVM-based encrypted container.")
self._cleanup_encrypted_backup(base_dest_path)
self._destroy_encrypted_structures(base_dest_path)
if os.path.exists(container_path):
self._execute_as_root(f"rm -f {container_path}")
queue.put(('error', "Failed to setup LVM-based encrypted container."))
@@ -230,7 +268,7 @@ class EncryptionManager:
self.logger.log(f"Encrypted LVM container is ready and mounted at {mount_point}")
return mount_point
def _cleanup_encrypted_backup(self, base_dest_path: str):
def _destroy_encrypted_structures(self, base_dest_path: str):
pybackup_dir = os.path.join(base_dest_path, "pybackup")
user_encrypt_dir = os.path.join(pybackup_dir, "user_encrypt")
mount_point = os.path.join(pybackup_dir, "encrypted")
@@ -240,15 +278,28 @@ class EncryptionManager:
mapper_name = f"pybackup_luks_{base_name}"
self.logger.log(f"Cleaning up encrypted LVM backup for {base_dest_path}")
find_loop_device_cmd = f"losetup -j {container_path} | cut -d: -f1"
lv_name = "backup_lv"
lv_path = f"/dev/{vg_name}/{lv_name}"
script = f"""
set -x # Log executed commands
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."
if mountpoint -q {mount_point}; then
umount {mount_point} || echo "Umount failed, continuing cleanup..."
fi
if [ -e /dev/mapper/{mapper_name} ]; then
cryptsetup luksClose {mapper_name} || echo "luksClose failed, continuing cleanup..."
fi
# Deactivate and remove all LVM structures associated with the VG
if vgdisplay {vg_name} >/dev/null 2>&1; then
vgchange -an {vg_name} || echo "Could not deactivate volume group {vg_name}."
lvchange -an {lv_path} >/dev/null 2>&1 || echo "lvchange failed, continuing..."
vgchange -an {vg_name} || echo "vgchange -an failed, continuing cleanup..."
lvremove -f {vg_name} || echo "lvremove failed, continuing cleanup..."
vgremove -f {vg_name} || echo "vgremove failed, continuing cleanup..."
fi
if [ -n "$LOOP_DEVICE" ] && losetup $LOOP_DEVICE >/dev/null 2>&1; then
losetup -d $LOOP_DEVICE || echo "Could not detach loop device $LOOP_DEVICE."
pvremove -f $LOOP_DEVICE || echo "pvremove failed, continuing cleanup..."
losetup -d $LOOP_DEVICE || echo "losetup -d failed, continuing cleanup..."
fi
"""
if not self._execute_as_root(script):

View File

@@ -147,7 +147,8 @@ class MainApplication(tk.Tk):
self.sidebar_buttons_frame, text=Msg.STR["settings"], command=lambda: self.navigation.toggle_settings_frame(4), style="Sidebar.TButton")
self.settings_button.pack(fill=tk.X, pady=10)
self.header_frame = HeaderFrame(self.content_frame, self.image_manager, self.backup_manager.encryption_manager, self)
self.header_frame = HeaderFrame(
self.content_frame, self.image_manager, self.backup_manager.encryption_manager, self)
self.header_frame.grid(row=0, column=0, sticky="nsew")
self.top_bar = ttk.Frame(self.content_frame)
@@ -221,7 +222,7 @@ class MainApplication(tk.Tk):
self._setup_scheduler_frame()
self._setup_settings_frame()
self._setup_backup_content_frame()
self._setup_task_bar()
self.source_size_frame = ttk.LabelFrame(
@@ -301,7 +302,7 @@ class MainApplication(tk.Tk):
self.protocol("WM_DELETE_WINDOW", self.on_closing)
def _load_state_and_initialize(self):
self.log_window.clear_log()
# self.log_window.clear_log()
last_mode = self.config_manager.get_setting("last_mode", "backup")
backup_source_path = self.config_manager.get_setting(
@@ -338,13 +339,17 @@ class MainApplication(tk.Tk):
if hasattr(self, 'header_frame'):
self.header_frame.refresh_status()
container_path = os.path.join(backup_dest_path, "pybackup_encrypted.luks")
container_path = os.path.join(
backup_dest_path, "pybackup_encrypted.luks")
if os.path.exists(container_path):
username = os.path.basename(backup_dest_path.rstrip('/'))
password = self.backup_manager.encryption_manager.get_password_from_keyring(username)
password = self.backup_manager.encryption_manager.get_password_from_keyring(
username)
if password:
self.backup_manager.encryption_manager.unlock_container(backup_dest_path, password)
app_logger.log("Automatically unlocked encrypted container.")
self.backup_manager.encryption_manager.unlock_container(
backup_dest_path, password)
app_logger.log(
"Automatically unlocked encrypted container.")
if hasattr(self, 'header_frame'):
self.header_frame.refresh_status()
@@ -384,7 +389,7 @@ class MainApplication(tk.Tk):
self.after(100, self.actions.on_sidebar_button_click,
restore_dest_folder)
self._process_queue()
self._update_sync_mode_display() # Call after loading state
self._update_sync_mode_display() # Call after loading state
def _setup_log_window(self):
self.log_frame = ttk.Frame(self.content_frame)
@@ -560,7 +565,7 @@ class MainApplication(tk.Tk):
try:
self.destroy()
except tk.TclError:
pass # App is already destroyed
pass # App is already destroyed
def _process_queue(self):
try:
@@ -581,7 +586,8 @@ class MainApplication(tk.Tk):
self.accurate_calculation_running = False
self.animated_icon.stop("DISABLE")
else:
current_folder_name = self.left_canvas_data.get('folder')
current_folder_name = self.left_canvas_data.get(
'folder')
if current_folder_name == button_text:
if self.left_canvas_animation:
self.left_canvas_animation.stop()
@@ -622,11 +628,13 @@ class MainApplication(tk.Tk):
self.drawing.update_target_projection()
self.animated_icon.stop("DISABLE")
self.task_progress.stop()
self.task_progress.config(mode="determinate", value=0)
self.task_progress.config(
mode="determinate", value=0)
self.actions._set_ui_state(True)
self.genaue_berechnung_var.set(False)
self.accurate_calculation_running = False
self.start_pause_button.config(text=Msg.STR["start"])
self.start_pause_button.config(
text=Msg.STR["start"])
if status == 'success':
self.info_label.config(
text=Msg.STR["accurate_size_success"], foreground="#0078d7")
@@ -753,22 +761,27 @@ class MainApplication(tk.Tk):
self.encrypted_cb.config(state="disabled")
self.actions._refresh_backup_options_ui()
self._update_sync_mode_display() # Update sync mode display after options are loaded
# Update sync mode display after options are loaded
self._update_sync_mode_display()
def _update_sync_mode_display(self):
use_trash_bin = self.config_manager.get_setting("use_trash_bin", False)
no_trash_bin = self.config_manager.get_setting("no_trash_bin", False)
if self.left_canvas_data.get('folder') == "Computer":
self.sync_mode_label.config(text="") # Not applicable for system backups
# Not applicable for system backups
self.sync_mode_label.config(text="")
return
if no_trash_bin:
self.sync_mode_label.config(text=Msg.STR["sync_mode_pure_sync"], foreground="red")
self.sync_mode_label.config(
text=Msg.STR["sync_mode_pure_sync"], foreground="red")
elif use_trash_bin:
self.sync_mode_label.config(text=Msg.STR["sync_mode_trash_bin"], foreground="orange")
self.sync_mode_label.config(
text=Msg.STR["sync_mode_trash_bin"], foreground="orange")
else:
self.sync_mode_label.config(text=Msg.STR["sync_mode_no_delete"], foreground="green")
self.sync_mode_label.config(
text=Msg.STR["sync_mode_no_delete"], foreground="green")
if __name__ == "__main__":

View File

@@ -29,7 +29,7 @@ class Actions:
def _update_backup_type_controls(self):
# Only applies to system backups in backup mode
if self.app.mode != 'backup' or self.app.left_canvas_data.get('folder') != "Computer":
self._set_backup_type("full") # Default for user backups
self._set_backup_type("full") # Default for user backups
self.app.full_backup_cb.config(state='disabled')
self.app.incremental_cb.config(state='disabled')
return
@@ -50,11 +50,12 @@ class Actions:
return
is_encrypted_backup = self.app.encrypted_var.get()
system_backups = self.app.backup_manager.list_system_backups(self.app.destination_path, mount_if_needed=False)
if system_backups is None: # Encrypted, but not inspected
full_backup_exists = True # Assume one exists to be safe
system_backups = self.app.backup_manager.list_system_backups(
self.app.destination_path, mount_if_needed=False)
if system_backups is None: # Encrypted, but not inspected
full_backup_exists = True # Assume one exists to be safe
else:
for backup in system_backups:
# Match the encryption status and check if it's a full backup
@@ -79,7 +80,7 @@ class Actions:
if self.app.encrypted_var.get():
self.app.compressed_var.set(False)
self.app.compressed_cb.config(state="disabled")
if self.app.compressed_var.get():
self.app.encrypted_var.set(False)
self.app.encrypted_cb.config(state="disabled")
@@ -157,7 +158,8 @@ class Actions:
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
exclude_file_paths.append(AppConfig.USER_EXCLUDE_LIST_PATH)
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
exclude_file_paths.append(AppConfig.MANUAL_EXCLUDE_LIST_PATH)
exclude_file_paths.append(
AppConfig.MANUAL_EXCLUDE_LIST_PATH)
base_dest = self.app.destination_path
correct_parent_dir = os.path.join(base_dest, "pybackup")
@@ -195,7 +197,7 @@ class Actions:
self.app.navigation.toggle_mode(
self.app.mode, trigger_calculation=False)
self.app.log_window.clear_log()
# self.app.log_window.clear_log()
REVERSE_FOLDER_MAP = {
"Computer": "Computer",
@@ -245,7 +247,8 @@ class Actions:
self._start_left_canvas_calculation(
button_text, str(folder_path), icon_name, extra_info)
self.app._update_sync_mode_display() # Update sync mode display when source changes
# Update sync mode display when source changes
self.app._update_sync_mode_display()
def _start_left_canvas_calculation(self, button_text, folder_path, icon_name, extra_info):
self.app.start_pause_button.config(state="disabled")
@@ -340,7 +343,8 @@ class Actions:
if self.app.mode == "backup":
# Unmount previous destination if it was mounted
if self.app.destination_path:
self.app.backup_manager.encryption_manager.unmount(self.app.destination_path)
self.app.backup_manager.encryption_manager.unmount(
self.app.destination_path)
self.app.destination_path = path
@@ -364,13 +368,13 @@ class Actions:
size_str = f"{used / (1024**3):.2f} GB / {total / (1024**3):.2f} GB"
self.app.right_canvas_data.update({
'folder': os.path.basename(path.rstrip('/')),
'folder': os.path.basename(path.rstrip('/')),
'path_display': path,
'size': size_str
})
self.app.config_manager.set_setting(
"backup_destination_path", path)
self.app.header_frame.refresh_status() # Refresh keyring status
self.app.header_frame.refresh_status() # Refresh keyring status
self.app.drawing.redraw_right_canvas()
self.app.drawing.update_target_projection()
@@ -381,7 +385,7 @@ class Actions:
elif self.app.mode == "restore":
self.app.right_canvas_data.update({
'folder': os.path.basename(path.rstrip('/')),
'folder': os.path.basename(path.rstrip('/')),
'path_display': path,
'size': ''
})
@@ -599,7 +603,7 @@ class Actions:
self.app.start_pause_button["text"] = Msg.STR["cancel_backup"]
self.app.update_idletasks()
self.app.log_window.clear_log()
# self.app.log_window.clear_log()
self._set_ui_state(False, allow_log_and_backup_toggle=True)
self.app.animated_icon.start()
@@ -607,9 +611,10 @@ class Actions:
if self.app.mode == "backup":
source_folder = self.app.left_canvas_data.get('folder')
source_size_bytes = self.app.source_size_bytes
if not source_folder:
app_logger.log("No source folder selected, aborting backup.")
app_logger.log(
"No source folder selected, aborting backup.")
self.app.backup_is_running = False
self.app.start_pause_button["text"] = Msg.STR["start"]
self._set_ui_state(True)
@@ -620,7 +625,7 @@ class Actions:
self._start_system_backup(mode, source_size_bytes)
else:
self._start_user_backup()
else: # restore mode
else: # restore mode
# Restore logic would go here
pass
@@ -641,9 +646,11 @@ class Actions:
password = None
if is_encrypted:
username = os.path.basename(base_dest.rstrip('/'))
password = self.app.backup_manager.encryption_manager.get_password(username, confirm=True)
password = self.app.backup_manager.encryption_manager.get_password(
username, confirm=True)
if not password:
app_logger.log("Encryption enabled, but no password provided. Aborting backup.")
app_logger.log(
"Encryption enabled, but no password provided. Aborting backup.")
self.app.backup_is_running = False
self.app.start_pause_button["text"] = Msg.STR["start"]
self._set_ui_state(True)
@@ -659,7 +666,8 @@ class Actions:
date_str = now.strftime("%d-%m-%Y")
time_str = now.strftime("%H:%M:%S")
folder_name = f"{date_str}_{time_str}_system_{mode}"
final_dest = os.path.join(base_dest, folder_name) # The backup_manager will add /pybackup/
# The backup_manager will add /pybackup/
final_dest = os.path.join(base_dest, folder_name)
self.app.current_backup_path = final_dest
source_size_bytes = self.app.left_canvas_data.get('total_bytes', 0)
@@ -685,8 +693,7 @@ class Actions:
source_size=source_size_bytes,
is_compressed=is_compressed,
is_encrypted=is_encrypted,
mode=mode,
password=password)
mode=mode)
def _start_user_backup(self):
base_dest = self.app.destination_path
@@ -706,29 +713,33 @@ class Actions:
password = None
if is_encrypted:
username = os.path.basename(base_dest.rstrip('/'))
password = self.app.backup_manager.encryption_manager.get_password(username, confirm=True)
password = self.app.backup_manager.encryption_manager.get_password(
username, confirm=True)
if not password:
app_logger.log("Encryption enabled, but no password provided. Aborting backup.")
app_logger.log(
"Encryption enabled, but no password provided. Aborting backup.")
self.app.backup_is_running = False
self.app.start_pause_button["text"] = Msg.STR["start"]
self._set_ui_state(True)
return
# Determine mode for user backup based on UI selection
mode = "full" if self.app.vollbackup_var.get() else "incremental"
now = datetime.datetime.now()
date_str = now.strftime("%d-%m-%Y")
time_str = now.strftime("%H:%M:%S")
folder_name = f"{date_str}_{time_str}_user_{source_name}"
folder_name = f"{date_str}_{time_str}_user_{source_name}_{mode}"
final_dest = os.path.join(base_dest, folder_name)
self.app.current_backup_path = final_dest
is_dry_run = self.app.testlauf_var.get()
is_compressed = self.app.compressed_var.get()
use_trash_bin = self.app.config_manager.get_setting("use_trash_bin", False)
no_trash_bin = self.app.config_manager.get_setting("no_trash_bin", False)
# Determine mode for user backup based on UI selection
mode = "full" if self.app.vollbackup_var.get() else "incremental"
use_trash_bin = self.app.config_manager.get_setting(
"use_trash_bin", False)
no_trash_bin = self.app.config_manager.get_setting(
"no_trash_bin", False)
self.app.backup_manager.start_backup(
queue=self.app.queue,
@@ -736,11 +747,18 @@ class Actions:
dest_path=final_dest,
is_system=False,
is_dry_run=is_dry_run,
exclude_files=None,
exclude_files=None,
source_size=source_size_bytes,
is_compressed=is_compressed,
is_encrypted=is_encrypted,
mode=mode,
password=password,
use_trash_bin=use_trash_bin,
no_trash_bin=no_trash_bin)
no_trash_bin=no_trash_bin)
now = datetime.datetime.now()
date_str = now.strftime("%d-%m-%Y")
time_str = now.strftime("%H:%M:%S")
folder_name = f"{date_str}_{time_str}_user_{source_name}_{mode}"
final_dest = os.path.join(base_dest, folder_name)
self.app.current_backup_path = final_dest

View File

@@ -148,6 +148,25 @@ class BackupContentFrame(ttk.Frame):
self.base_backup_path = backup_path
# Check if the destination is encrypted and trigger mount if necessary
is_encrypted = self.backup_manager.encryption_manager.is_encrypted(
backup_path)
if is_encrypted and not self.backup_manager.encryption_manager.is_mounted(backup_path):
app_logger.log(
"Encrypted destination is not mounted. Attempting to mount.")
mount_point = self.backup_manager.encryption_manager.mount(
backup_path)
if not mount_point:
app_logger.log("Mount failed. Cannot display backup content.")
MessageDialog(
message_type="error", title=Msg.STR["error"], text=Msg.STR["err_unlock_failed"])
# Clear views and return if mount fails
self.system_backups_frame.show(backup_path, [])
self.user_backups_frame.show(backup_path, [])
return
# Refresh header status after successful mount
self.app.header_frame.refresh_status()
pybackup_dir = os.path.join(backup_path, "pybackup")
if not os.path.isdir(pybackup_dir):
@@ -168,7 +187,6 @@ class BackupContentFrame(ttk.Frame):
self.system_backups_frame.show(backup_path, [])
self.user_backups_frame.show(backup_path, [])
config_key = "last_encrypted_backup_content_view" if self.viewing_encrypted else "last_backup_content_view"
last_view = self.app.config_manager.get_setting(config_key, 0)
self._switch_view(last_view)