add lvm for img encrypt zo automatic resize for new encrypt backup
This commit is contained in:
@@ -4,6 +4,8 @@ import threading
|
|||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
import datetime
|
import datetime
|
||||||
|
import math
|
||||||
|
import shutil
|
||||||
from typing import Optional, List, Dict, Any
|
from typing import Optional, List, Dict, Any
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from crontab import CronTab
|
from crontab import CronTab
|
||||||
@@ -33,42 +35,52 @@ class BackupManager:
|
|||||||
def _inhibit_screensaver(self):
|
def _inhibit_screensaver(self):
|
||||||
"""Prevents the screensaver and auto-suspend during a backup."""
|
"""Prevents the screensaver and auto-suspend during a backup."""
|
||||||
if not shutil.which("gdbus"):
|
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
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.logger.log("Attempting to inhibit screensaver and power management.")
|
self.logger.log(
|
||||||
|
"Attempting to inhibit screensaver and power management.")
|
||||||
command = [
|
command = [
|
||||||
"gdbus", "call", "--session", "--dest", "org.freedesktop.ScreenSaver",
|
"gdbus", "call", "--session", "--dest", "org.freedesktop.ScreenSaver",
|
||||||
"--object-path", "/org/freedesktop/ScreenSaver",
|
"--object-path", "/org/freedesktop/ScreenSaver",
|
||||||
"--method", "org.freedesktop.ScreenSaver.Inhibit",
|
"--method", "org.freedesktop.ScreenSaver.Inhibit",
|
||||||
"Py-Backup", "Backup in progress"
|
"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.
|
# Output is like "(uint32 12345,)", we need to extract the number.
|
||||||
match = re.search(r'uint32\s+(\d+)', result.stdout)
|
match = re.search(r'uint32\s+(\d+)', result.stdout)
|
||||||
if match:
|
if match:
|
||||||
self.inhibit_cookie = int(match.group(1))
|
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:
|
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:
|
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:
|
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:
|
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):
|
def _uninhibit_screensaver(self):
|
||||||
"""Releases the screensaver and auto-suspend lock."""
|
"""Releases the screensaver and auto-suspend lock."""
|
||||||
if self.inhibit_cookie is None:
|
if self.inhibit_cookie is None:
|
||||||
return
|
return
|
||||||
if not shutil.which("gdbus"):
|
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
|
return
|
||||||
|
|
||||||
try:
|
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 = [
|
command = [
|
||||||
"gdbus", "call", "--session", "--dest", "org.freedesktop.ScreenSaver",
|
"gdbus", "call", "--session", "--dest", "org.freedesktop.ScreenSaver",
|
||||||
"--object-path", "/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):
|
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.is_system_process = is_system
|
||||||
self._inhibit_screensaver()
|
self._inhibit_screensaver()
|
||||||
|
|
||||||
mount_point = None
|
mount_point = None
|
||||||
if is_encrypted:
|
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:
|
if not mount_point:
|
||||||
self.logger.log("Failed to mount encrypted destination. Aborting backup.")
|
self.logger.log(
|
||||||
queue.put(('completion', {'status': 'error', 'returncode': -1}))
|
"Failed to mount encrypted destination. Aborting backup.")
|
||||||
|
queue.put(
|
||||||
|
('completion', {'status': 'error', 'returncode': -1}))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
thread = threading.Thread(target=self._run_backup_path, args=(
|
thread = threading.Thread(target=self._run_backup_path, args=(
|
||||||
@@ -108,27 +127,60 @@ class BackupManager:
|
|||||||
user_source_name = None
|
user_source_name = None
|
||||||
if not is_system:
|
if not is_system:
|
||||||
# Extract source name from backup_name (e.g., 2025-09-06_10-00-00_user_MyDocs_full.txt -> MyDocs)
|
# 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:
|
if match:
|
||||||
user_source_name = match.group(1)
|
user_source_name = match.group(1)
|
||||||
else:
|
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
|
# Determine actual mode for user backups
|
||||||
if not is_system and not latest_backup_path:
|
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:
|
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 = []
|
command = []
|
||||||
if is_system or is_encrypted:
|
if is_system or is_encrypted:
|
||||||
command.extend(['pkexec', 'rsync', '-aAXHv'])
|
command.extend(['pkexec', 'rsync', '-aAXHvL'])
|
||||||
else:
|
else:
|
||||||
command.extend(['rsync', '-av'])
|
command.extend(['rsync', '-avL'])
|
||||||
|
|
||||||
if mode == "incremental" and latest_backup_path and not is_dry_run:
|
if mode == "incremental" and latest_backup_path and not is_dry_run:
|
||||||
command.append(f"--link-dest={latest_backup_path}")
|
command.append(f"--link-dest={latest_backup_path}")
|
||||||
@@ -137,7 +189,8 @@ class BackupManager:
|
|||||||
if exclude_files:
|
if exclude_files:
|
||||||
command.extend([f"--exclude-from={f}" for f in exclude_files])
|
command.extend([f"--exclude-from={f}" for f in exclude_files])
|
||||||
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
|
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:
|
if is_dry_run:
|
||||||
command.append('--dry-run')
|
command.append('--dry-run')
|
||||||
|
|
||||||
@@ -145,38 +198,140 @@ class BackupManager:
|
|||||||
if not is_system:
|
if not is_system:
|
||||||
trash_bin_path = os.path.join(rsync_base_dest, ".Trash")
|
trash_bin_path = os.path.join(rsync_base_dest, ".Trash")
|
||||||
if use_trash_bin:
|
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
|
# 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:
|
elif no_trash_bin:
|
||||||
command.append('--delete')
|
command.append('--delete')
|
||||||
# Exclude the trash bin itself from the backup if it exists from previous use_trash_bin
|
# 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])
|
command.extend([source_path, rsync_dest])
|
||||||
self.logger.log(f"Rsync command: {' '.join(command)}")
|
self.logger.log(f"Rsync command: {' '.join(command)}")
|
||||||
|
|
||||||
transferred_size, total_size, stderr = self._execute_rsync(queue, command)
|
max_retries = 1
|
||||||
return_code = self.process.returncode if self.process else -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:
|
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 resize container.")
|
self.logger.log(
|
||||||
# Resize logic would need to be adapted for the new mount management
|
"Rsync failed due to lack of space. Attempting to dynamically resize container.")
|
||||||
# For now, we just log the error
|
try:
|
||||||
queue.put(('error', "Container-Vergrößerung fehlgeschlagen. Backup abgebrochen."))
|
# 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:
|
if self.process:
|
||||||
status = 'error'
|
status = 'error'
|
||||||
if return_code == 0: status = 'success'
|
if return_code == 0:
|
||||||
elif return_code in [23, 24]: status = 'warning'
|
status = 'success'
|
||||||
elif return_code in [143, -15, 15, -9]: status = 'cancelled'
|
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:
|
if status in ['success', 'warning'] and not is_dry_run:
|
||||||
info_filename_base = backup_name
|
info_filename_base = backup_name
|
||||||
final_size = transferred_size if (mode == 'incremental' and latest_backup_path) else (total_size or source_size)
|
final_size = transferred_size if (
|
||||||
self._create_info_file(pybackup_dir, info_filename_base, final_size, is_encrypted)
|
mode == 'incremental' and latest_backup_path) else (total_size or source_size)
|
||||||
|
self._create_info_file(
|
||||||
queue.put(('completion', {'status': status, 'returncode': return_code}))
|
pybackup_dir, info_filename_base, final_size, is_encrypted)
|
||||||
|
|
||||||
|
queue.put(
|
||||||
|
('completion', {'status': status, 'returncode': return_code}))
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
self._uninhibit_screensaver()
|
self._uninhibit_screensaver()
|
||||||
@@ -191,12 +346,14 @@ class BackupManager:
|
|||||||
mounted_path = os.path.join(pybackup_dir, "encrypted")
|
mounted_path = os.path.join(pybackup_dir, "encrypted")
|
||||||
else:
|
else:
|
||||||
return [], []
|
return [], []
|
||||||
|
|
||||||
return self._list_all_backups_from_path(base_dest_path, mounted_path)
|
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):
|
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)
|
system_backups = self._list_system_backups_from_path(
|
||||||
user_backups = self._list_user_backups_from_path(base_dest_path, mounted_path)
|
base_dest_path, mounted_path)
|
||||||
|
user_backups = self._list_user_backups_from_path(
|
||||||
|
base_dest_path, mounted_path)
|
||||||
return system_backups, user_backups
|
return system_backups, user_backups
|
||||||
|
|
||||||
def list_system_backups(self, base_dest_path: str, mount_if_needed: bool = True) -> Optional[List[Dict[str, str]]]:
|
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 is_encrypted:
|
||||||
if not self.encryption_manager.is_mounted(base_dest_path):
|
if not self.encryption_manager.is_mounted(base_dest_path):
|
||||||
if mount_if_needed:
|
if mount_if_needed:
|
||||||
self.logger.log("Mounting needed for listing system backups.")
|
self.logger.log(
|
||||||
mounted_path = self.encryption_manager.mount(base_dest_path)
|
"Mounting needed for listing system backups.")
|
||||||
|
mounted_path = self.encryption_manager.mount(
|
||||||
|
base_dest_path)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if self.encryption_manager.is_mounted(base_dest_path):
|
if self.encryption_manager.is_mounted(base_dest_path):
|
||||||
pybackup_dir = os.path.join(base_dest_path, "pybackup")
|
pybackup_dir = os.path.join(base_dest_path, "pybackup")
|
||||||
mounted_path = os.path.join(pybackup_dir, "encrypted")
|
mounted_path = os.path.join(pybackup_dir, "encrypted")
|
||||||
@@ -239,8 +398,10 @@ class BackupManager:
|
|||||||
else:
|
else:
|
||||||
full_path = os.path.join(pybackup_dir, backup_name)
|
full_path = os.path.join(pybackup_dir, backup_name)
|
||||||
backup_type = backup_type_base.capitalize()
|
backup_type = backup_type_base.capitalize()
|
||||||
if is_compressed: backup_type += " (Compressed)"
|
if is_compressed:
|
||||||
if is_encrypted: backup_type += " (Encrypted)"
|
backup_type += " (Compressed)"
|
||||||
|
if is_encrypted:
|
||||||
|
backup_type += " (Encrypted)"
|
||||||
backup_size = "N/A"
|
backup_size = "N/A"
|
||||||
comment = ""
|
comment = ""
|
||||||
info_file_path = os.path.join(pybackup_dir, item)
|
info_file_path = os.path.join(pybackup_dir, item)
|
||||||
@@ -249,11 +410,13 @@ class BackupManager:
|
|||||||
with open(info_file_path, 'r') as f:
|
with open(info_file_path, 'r') as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
if line.strip().lower().startswith("originalgröße:"):
|
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:"):
|
elif line.strip().lower().startswith("kommentar:"):
|
||||||
comment = line.split(":", 1)[1].strip()
|
comment = line.split(":", 1)[1].strip()
|
||||||
except Exception as e:
|
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({
|
all_backups.append({
|
||||||
"date": date_str, "time": time_str, "type": backup_type,
|
"date": date_str, "time": time_str, "type": backup_type,
|
||||||
"size": backup_size, "folder_name": backup_name, "full_path": full_path,
|
"size": backup_size, "folder_name": backup_name, "full_path": full_path,
|
||||||
@@ -266,14 +429,19 @@ class BackupManager:
|
|||||||
current_group = []
|
current_group = []
|
||||||
for backup in all_backups:
|
for backup in all_backups:
|
||||||
if backup['backup_type_base'] == 'Full':
|
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]
|
current_group = [backup]
|
||||||
else:
|
else:
|
||||||
if not current_group: current_group.append(backup)
|
if not current_group:
|
||||||
else: current_group.append(backup)
|
current_group.append(backup)
|
||||||
if current_group: grouped_backups.append(current_group)
|
else:
|
||||||
|
current_group.append(backup)
|
||||||
|
if current_group:
|
||||||
|
grouped_backups.append(current_group)
|
||||||
grouped_backups.sort(key=lambda g: g[0]['datetime'], reverse=True)
|
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
|
return final_sorted_list
|
||||||
|
|
||||||
def list_user_backups(self, base_dest_path: str, mount_if_needed: bool = True) -> Optional[List[Dict[str, str]]]:
|
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 is_encrypted:
|
||||||
if not self.encryption_manager.is_mounted(base_dest_path):
|
if not self.encryption_manager.is_mounted(base_dest_path):
|
||||||
if mount_if_needed:
|
if mount_if_needed:
|
||||||
self.logger.log("Mounting needed for listing user backups.")
|
self.logger.log(
|
||||||
mounted_path = self.encryption_manager.mount(base_dest_path)
|
"Mounting needed for listing user backups.")
|
||||||
|
mounted_path = self.encryption_manager.mount(
|
||||||
|
base_dest_path)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
if self.encryption_manager.is_mounted(base_dest_path):
|
if self.encryption_manager.is_mounted(base_dest_path):
|
||||||
@@ -307,19 +477,21 @@ class BackupManager:
|
|||||||
continue
|
continue
|
||||||
date_str, time_str, source_name, backup_type_base, enc_suffix = match.groups()
|
date_str, time_str, source_name, backup_type_base, enc_suffix = match.groups()
|
||||||
is_encrypted = (enc_suffix is not None)
|
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", "")
|
backup_name = item.replace(".txt", "").replace("_encrypted", "")
|
||||||
|
|
||||||
if mounted_path:
|
if mounted_path:
|
||||||
user_backup_dir = os.path.join(mounted_path, "user_encrypt")
|
user_backup_dir = os.path.join(mounted_path, "user_encrypt")
|
||||||
full_path = os.path.join(user_backup_dir, backup_name)
|
full_path = os.path.join(user_backup_dir, backup_name)
|
||||||
else:
|
else:
|
||||||
user_backups_dir = os.path.join(pybackup_dir, "user_backups")
|
user_backups_dir = os.path.join(pybackup_dir, "user_backups")
|
||||||
full_path = os.path.join(user_backups_dir, backup_name)
|
full_path = os.path.join(user_backups_dir, backup_name)
|
||||||
|
|
||||||
backup_type = backup_type_base.capitalize()
|
backup_type = backup_type_base.capitalize()
|
||||||
if is_compressed: backup_type += " (Compressed)"
|
if is_compressed:
|
||||||
if is_encrypted: backup_type += " (Encrypted)"
|
backup_type += " (Compressed)"
|
||||||
|
if is_encrypted:
|
||||||
|
backup_type += " (Encrypted)"
|
||||||
|
|
||||||
backup_size = "N/A"
|
backup_size = "N/A"
|
||||||
comment = ""
|
comment = ""
|
||||||
@@ -329,11 +501,13 @@ class BackupManager:
|
|||||||
with open(info_file_path, 'r') as f:
|
with open(info_file_path, 'r') as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
if line.strip().lower().startswith("originalgröße:"):
|
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:"):
|
elif line.strip().lower().startswith("kommentar:"):
|
||||||
comment = line.split(":", 1)[1].strip()
|
comment = line.split(":", 1)[1].strip()
|
||||||
except Exception as e:
|
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({
|
user_backups.append({
|
||||||
"date": date_str, "time": time_str, "type": backup_type,
|
"date": date_str, "time": time_str, "type": backup_type,
|
||||||
"size": backup_size, "folder_name": backup_name, "full_path": full_path,
|
"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]:
|
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."""
|
"""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 = []
|
backup_names = []
|
||||||
if os.path.isdir(base_backup_path):
|
if os.path.isdir(base_backup_path):
|
||||||
for item in os.listdir(base_backup_path):
|
for item in os.listdir(base_backup_path):
|
||||||
@@ -362,7 +537,8 @@ class BackupManager:
|
|||||||
backup_names.append(item)
|
backup_names.append(item)
|
||||||
else:
|
else:
|
||||||
# For system backups or if no source_name is provided, include all
|
# 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)
|
backup_names.append(item)
|
||||||
|
|
||||||
# Sort by date and time (assuming format YYYY-MM-DD_HH-MM-SS or similar at the beginning)
|
# 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:
|
if not backup_names:
|
||||||
self.logger.log("No previous backups found to link against.")
|
self.logger.log("No previous backups found to link against.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
latest_backup_name = backup_names[0]
|
latest_backup_name = backup_names[0]
|
||||||
latest_backup_path = os.path.join(base_backup_path, latest_backup_name)
|
latest_backup_path = os.path.join(base_backup_path, latest_backup_name)
|
||||||
|
|
||||||
if os.path.isdir(latest_backup_path):
|
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
|
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
|
return None
|
||||||
|
|
||||||
def _create_info_file(self, pybackup_dir: str, backup_name: str, source_size: int, is_encrypted: bool):
|
def _create_info_file(self, pybackup_dir: str, backup_name: str, source_size: int, is_encrypted: bool):
|
||||||
try:
|
try:
|
||||||
info_filename = f"{backup_name}_encrypted.txt" if is_encrypted else f"{backup_name}.txt"
|
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)
|
info_file_path = os.path.join(pybackup_dir, info_filename)
|
||||||
|
|
||||||
original_bytes = source_size
|
original_bytes = source_size
|
||||||
if source_size > 0:
|
if source_size > 0:
|
||||||
power = 1024
|
power = 1024
|
||||||
@@ -474,16 +652,18 @@ class BackupManager:
|
|||||||
|
|
||||||
transferred_size = 0
|
transferred_size = 0
|
||||||
total_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")
|
total_size_regex = re.compile(r"total size is ([\d,. ]+) speedup")
|
||||||
|
|
||||||
|
|
||||||
for line in reversed(output_lines):
|
for line in reversed(output_lines):
|
||||||
match = summary_regex.search(line)
|
match = summary_regex.search(line)
|
||||||
if match and transferred_size == 0:
|
if match and transferred_size == 0:
|
||||||
try:
|
try:
|
||||||
sent_str = match.group(1).replace(',', '').replace('.', '')
|
sent_str = match.group(1).replace(
|
||||||
received_str = match.group(2).replace(',', '').replace('.', '')
|
',', '').replace('.', '')
|
||||||
|
received_str = match.group(2).replace(
|
||||||
|
',', '').replace('.', '')
|
||||||
bytes_sent = int(sent_str)
|
bytes_sent = int(sent_str)
|
||||||
bytes_received = int(received_str)
|
bytes_received = int(received_str)
|
||||||
transferred_size = bytes_sent + bytes_received
|
transferred_size = bytes_sent + bytes_received
|
||||||
@@ -492,18 +672,22 @@ class BackupManager:
|
|||||||
except (ValueError, IndexError) as e:
|
except (ValueError, IndexError) as e:
|
||||||
self.logger.log(
|
self.logger.log(
|
||||||
f"Could not parse sent/received bytes from line: '{line}'. Error: {e}")
|
f"Could not parse sent/received bytes from line: '{line}'. Error: {e}")
|
||||||
|
|
||||||
total_match = total_size_regex.search(line)
|
total_match = total_size_regex.search(line)
|
||||||
if total_match and total_size == 0:
|
if total_match and total_size == 0:
|
||||||
try:
|
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)
|
total_size = int(total_size_str)
|
||||||
self.logger.log(f"Detected total size from summary: {total_size} bytes")
|
self.logger.log(
|
||||||
except(ValueError, IndexError) as e:
|
f"Detected total size from summary: {total_size} bytes")
|
||||||
self.logger.log(f"Could not parse total size from line: '{line}'. Error: {e}")
|
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:
|
if transferred_size == 0:
|
||||||
bytes_sent = 0
|
bytes_sent = 0
|
||||||
bytes_received = 0
|
bytes_received = 0
|
||||||
@@ -511,16 +695,20 @@ class BackupManager:
|
|||||||
if line.strip().startswith('Total bytes sent:'):
|
if line.strip().startswith('Total bytes sent:'):
|
||||||
try:
|
try:
|
||||||
size_str = line.split(':')[1].strip()
|
size_str = line.split(':')[1].strip()
|
||||||
bytes_sent = int(size_str.replace(',', '').replace('.', ''))
|
bytes_sent = int(size_str.replace(
|
||||||
|
',', '').replace('.', ''))
|
||||||
except (ValueError, IndexError):
|
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:'):
|
elif line.strip().startswith('Total bytes received:'):
|
||||||
try:
|
try:
|
||||||
size_str = line.split(':')[1].strip()
|
size_str = line.split(':')[1].strip()
|
||||||
bytes_received = int(size_str.replace(',', '').replace('.', ''))
|
bytes_received = int(
|
||||||
|
size_str.replace(',', '').replace('.', ''))
|
||||||
except (ValueError, IndexError):
|
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:
|
if bytes_sent > 0 or bytes_received > 0:
|
||||||
transferred_size = bytes_sent + bytes_received
|
transferred_size = bytes_sent + bytes_received
|
||||||
self.logger.log(
|
self.logger.log(
|
||||||
@@ -536,7 +724,7 @@ class BackupManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.log(f"An unexpected error occurred: {e}")
|
self.logger.log(f"An unexpected error occurred: {e}")
|
||||||
queue.put(('error', None))
|
queue.put(('error', None))
|
||||||
|
|
||||||
return transferred_size, total_size, stderr_output
|
return transferred_size, total_size, stderr_output
|
||||||
|
|
||||||
def start_restore(self, source_path: str, dest_path: str, is_compressed: bool):
|
def start_restore(self, source_path: str, dest_path: str, is_compressed: bool):
|
||||||
@@ -544,7 +732,8 @@ class BackupManager:
|
|||||||
try:
|
try:
|
||||||
queue = self.app.queue
|
queue = self.app.queue
|
||||||
except AttributeError:
|
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
|
from queue import Queue
|
||||||
queue = Queue()
|
queue = Queue()
|
||||||
|
|
||||||
@@ -572,10 +761,12 @@ class BackupManager:
|
|||||||
status = 'error'
|
status = 'error'
|
||||||
|
|
||||||
except Exception as e:
|
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'
|
status = 'error'
|
||||||
finally:
|
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]]:
|
def get_scheduled_jobs(self) -> List[Dict[str, Any]]:
|
||||||
jobs_list = []
|
jobs_list = []
|
||||||
@@ -650,7 +841,8 @@ class BackupManager:
|
|||||||
if line.strip().lower().startswith("kommentar:"):
|
if line.strip().lower().startswith("kommentar:"):
|
||||||
return line.split(":", 1)[1].strip()
|
return line.split(":", 1)[1].strip()
|
||||||
except Exception as e:
|
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 ""
|
return ""
|
||||||
|
|
||||||
def update_comment(self, info_file_path: str, new_comment: str):
|
def update_comment(self, info_file_path: str, new_comment: str):
|
||||||
@@ -670,13 +862,14 @@ class BackupManager:
|
|||||||
comment_found = True
|
comment_found = True
|
||||||
else:
|
else:
|
||||||
new_lines.append(line)
|
new_lines.append(line)
|
||||||
|
|
||||||
if not comment_found and new_comment:
|
if not comment_found and new_comment:
|
||||||
new_lines.append(f"Kommentar: {new_comment}\n")
|
new_lines.append(f"Kommentar: {new_comment}\n")
|
||||||
|
|
||||||
with open(info_file_path, 'w') as f:
|
with open(info_file_path, 'w') as f:
|
||||||
f.writelines(new_lines)
|
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:
|
except Exception as e:
|
||||||
self.logger.log(f"Error updating comment in {info_file_path}: {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."""
|
"""Runs the deletion and puts a message on the queue when done."""
|
||||||
try:
|
try:
|
||||||
if is_encrypted:
|
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:
|
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))
|
queue.put(('deletion_complete', False))
|
||||||
return
|
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:
|
if not mount_point:
|
||||||
self.logger.log("Failed to unlock container for deletion.")
|
self.logger.log("Failed to unlock container for deletion.")
|
||||||
queue.put(('deletion_complete', False))
|
queue.put(('deletion_complete', False))
|
||||||
return
|
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"""
|
script_content = f"""
|
||||||
rm -rf '{path_to_delete}'
|
rm -rf '{path_to_delete}'
|
||||||
rm -f '{info_file_path}'
|
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:
|
if success:
|
||||||
self.logger.log("Encrypted backup deleted successfully.")
|
self.logger.log("Encrypted backup deleted successfully.")
|
||||||
queue.put(('deletion_complete', True))
|
queue.put(('deletion_complete', True))
|
||||||
else:
|
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))
|
queue.put(('deletion_complete', False))
|
||||||
|
|
||||||
elif is_system: # Unencrypted system backup
|
elif is_system: # Unencrypted system backup
|
||||||
self.logger.log(f"Starting unencrypted system deletion for {path_to_delete}")
|
self.logger.log(
|
||||||
|
f"Starting unencrypted system deletion for {path_to_delete}")
|
||||||
script_content = f"""
|
script_content = f"""
|
||||||
rm -rf '{path_to_delete}'
|
rm -rf '{path_to_delete}'
|
||||||
rm -f '{info_file_path}'
|
rm -f '{info_file_path}'
|
||||||
"""
|
"""
|
||||||
if self.encryption_manager._execute_as_root(script_content):
|
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))
|
queue.put(('deletion_complete', True))
|
||||||
else:
|
else:
|
||||||
self.logger.log(f"Failed to delete {path_to_delete}")
|
self.logger.log(f"Failed to delete {path_to_delete}")
|
||||||
queue.put(('deletion_complete', False))
|
queue.put(('deletion_complete', False))
|
||||||
|
|
||||||
else: # Unencrypted user backup
|
else: # Unencrypted user backup
|
||||||
self.logger.log(f"Starting unencrypted user deletion for {path_to_delete}")
|
self.logger.log(
|
||||||
|
f"Starting unencrypted user deletion for {path_to_delete}")
|
||||||
try:
|
try:
|
||||||
if os.path.isdir(path_to_delete):
|
if os.path.isdir(path_to_delete):
|
||||||
shutil.rmtree(path_to_delete)
|
shutil.rmtree(path_to_delete)
|
||||||
if os.path.exists(info_file_path):
|
if os.path.exists(info_file_path):
|
||||||
os.remove(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))
|
queue.put(('deletion_complete', True))
|
||||||
except Exception as e:
|
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))
|
queue.put(('deletion_complete', False))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -835,7 +1039,8 @@ set -e
|
|||||||
if is_system or is_encrypted:
|
if is_system or is_encrypted:
|
||||||
self.logger.log("Executing compression and cleanup as root.")
|
self.logger.log("Executing compression and cleanup as root.")
|
||||||
if self.encryption_manager._execute_as_root(script_content):
|
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
|
return True
|
||||||
else:
|
else:
|
||||||
self.logger.log("Compression and cleanup script failed.")
|
self.logger.log("Compression and cleanup script failed.")
|
||||||
@@ -843,20 +1048,25 @@ set -e
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
self.logger.log(f"Executing local command: {tar_command}")
|
self.logger.log(f"Executing local command: {tar_command}")
|
||||||
tar_result = subprocess.run(tar_command, shell=True, capture_output=True, text=True, check=True)
|
tar_result = subprocess.run(
|
||||||
self.logger.log(f"tar command successful. Output: {tar_result.stdout}")
|
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}")
|
self.logger.log(f"Executing local command: {rm_command}")
|
||||||
rm_result = subprocess.run(rm_command, shell=True, capture_output=True, text=True, check=True)
|
rm_result = subprocess.run(
|
||||||
self.logger.log(f"rm command successful. Output: {rm_result.stdout}")
|
rm_command, shell=True, capture_output=True, text=True, check=True)
|
||||||
|
self.logger.log(
|
||||||
|
f"rm command successful. Output: {rm_result.stdout}")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except subprocess.CalledProcessError as e:
|
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"Stdout: {e.stdout}")
|
||||||
self.logger.log(f"Stderr: {e.stderr}")
|
self.logger.log(f"Stderr: {e.stderr}")
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
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
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class EncryptionManager:
|
|||||||
self.session_password = None
|
self.session_password = None
|
||||||
self.mounted_destinations = set()
|
self.mounted_destinations = set()
|
||||||
self.auth_method = None
|
self.auth_method = None
|
||||||
|
self.is_mounting = False
|
||||||
|
|
||||||
def get_key_file_path(self, base_dest_path: str) -> str:
|
def get_key_file_path(self, base_dest_path: str) -> str:
|
||||||
"""Generates the standard path for the key file for a given destination."""
|
"""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")
|
mount_point = os.path.join(pybackup_dir, "encrypted")
|
||||||
return os.path.ismount(mount_point)
|
return os.path.ismount(mount_point)
|
||||||
|
|
||||||
def mount(self, base_dest_path: str, queue=None) -> Optional[str]:
|
def mount(self, base_dest_path: str, queue=None, size_gb: int = 0) -> Optional[str]:
|
||||||
if not self.is_encrypted(base_dest_path):
|
if self.is_mounting:
|
||||||
self.auth_method = None
|
self.logger.log("Mount process already in progress. Aborting new request.")
|
||||||
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
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
mount_point = self._setup_encrypted_backup(queue, base_dest_path, 0, password=password)
|
self.is_mounting = True
|
||||||
if mount_point:
|
try:
|
||||||
self.auth_method = "password"
|
if self.is_mounted(base_dest_path):
|
||||||
self.mounted_destinations.add(base_dest_path)
|
self.mounted_destinations.add(base_dest_path)
|
||||||
return mount_point
|
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):
|
def unmount(self, base_dest_path: str):
|
||||||
if base_dest_path in self.mounted_destinations:
|
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)
|
self.mounted_destinations.remove(base_dest_path)
|
||||||
|
|
||||||
def unmount_all(self):
|
def unmount_all(self):
|
||||||
@@ -156,6 +165,35 @@ class EncryptionManager:
|
|||||||
for path in list(self.mounted_destinations):
|
for path in list(self.mounted_destinations):
|
||||||
self.unmount(path)
|
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]:
|
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}")
|
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):
|
if not self._execute_as_root(script):
|
||||||
self.logger.log("Failed to unlock existing LVM container.")
|
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
|
return None
|
||||||
self.logger.log(f"Encrypted LVM container unlocked and mounted at {mount_point}")
|
self.logger.log(f"Encrypted LVM container unlocked and mounted at {mount_point}")
|
||||||
return mount_point
|
return mount_point
|
||||||
@@ -222,7 +260,7 @@ class EncryptionManager:
|
|||||||
"""
|
"""
|
||||||
if not self._execute_as_root(script):
|
if not self._execute_as_root(script):
|
||||||
self.logger.log("Failed to create and setup LVM-based encrypted container.")
|
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):
|
if os.path.exists(container_path):
|
||||||
self._execute_as_root(f"rm -f {container_path}")
|
self._execute_as_root(f"rm -f {container_path}")
|
||||||
queue.put(('error', "Failed to setup LVM-based encrypted container."))
|
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}")
|
self.logger.log(f"Encrypted LVM container is ready and mounted at {mount_point}")
|
||||||
return 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")
|
pybackup_dir = os.path.join(base_dest_path, "pybackup")
|
||||||
user_encrypt_dir = os.path.join(pybackup_dir, "user_encrypt")
|
user_encrypt_dir = os.path.join(pybackup_dir, "user_encrypt")
|
||||||
mount_point = os.path.join(pybackup_dir, "encrypted")
|
mount_point = os.path.join(pybackup_dir, "encrypted")
|
||||||
@@ -240,15 +278,28 @@ class EncryptionManager:
|
|||||||
mapper_name = f"pybackup_luks_{base_name}"
|
mapper_name = f"pybackup_luks_{base_name}"
|
||||||
self.logger.log(f"Cleaning up encrypted LVM backup for {base_dest_path}")
|
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"
|
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"""
|
script = f"""
|
||||||
|
set -x # Log executed commands
|
||||||
LOOP_DEVICE=$({find_loop_device_cmd} | head -n 1)
|
LOOP_DEVICE=$({find_loop_device_cmd} | head -n 1)
|
||||||
umount {mount_point} || echo "Mount point {mount_point} not found or already unmounted."
|
if mountpoint -q {mount_point}; then
|
||||||
cryptsetup luksClose {mapper_name} || echo "Mapper {mapper_name} not found or already closed."
|
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
|
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
|
fi
|
||||||
if [ -n "$LOOP_DEVICE" ] && losetup $LOOP_DEVICE >/dev/null 2>&1; then
|
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
|
fi
|
||||||
"""
|
"""
|
||||||
if not self._execute_as_root(script):
|
if not self._execute_as_root(script):
|
||||||
|
|||||||
47
main_app.py
47
main_app.py
@@ -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.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.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.header_frame.grid(row=0, column=0, sticky="nsew")
|
||||||
|
|
||||||
self.top_bar = ttk.Frame(self.content_frame)
|
self.top_bar = ttk.Frame(self.content_frame)
|
||||||
@@ -221,7 +222,7 @@ class MainApplication(tk.Tk):
|
|||||||
self._setup_scheduler_frame()
|
self._setup_scheduler_frame()
|
||||||
self._setup_settings_frame()
|
self._setup_settings_frame()
|
||||||
self._setup_backup_content_frame()
|
self._setup_backup_content_frame()
|
||||||
|
|
||||||
self._setup_task_bar()
|
self._setup_task_bar()
|
||||||
|
|
||||||
self.source_size_frame = ttk.LabelFrame(
|
self.source_size_frame = ttk.LabelFrame(
|
||||||
@@ -301,7 +302,7 @@ class MainApplication(tk.Tk):
|
|||||||
self.protocol("WM_DELETE_WINDOW", self.on_closing)
|
self.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||||
|
|
||||||
def _load_state_and_initialize(self):
|
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")
|
last_mode = self.config_manager.get_setting("last_mode", "backup")
|
||||||
|
|
||||||
backup_source_path = self.config_manager.get_setting(
|
backup_source_path = self.config_manager.get_setting(
|
||||||
@@ -338,13 +339,17 @@ class MainApplication(tk.Tk):
|
|||||||
if hasattr(self, 'header_frame'):
|
if hasattr(self, 'header_frame'):
|
||||||
self.header_frame.refresh_status()
|
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):
|
if os.path.exists(container_path):
|
||||||
username = os.path.basename(backup_dest_path.rstrip('/'))
|
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:
|
if password:
|
||||||
self.backup_manager.encryption_manager.unlock_container(backup_dest_path, password)
|
self.backup_manager.encryption_manager.unlock_container(
|
||||||
app_logger.log("Automatically unlocked encrypted container.")
|
backup_dest_path, password)
|
||||||
|
app_logger.log(
|
||||||
|
"Automatically unlocked encrypted container.")
|
||||||
if hasattr(self, 'header_frame'):
|
if hasattr(self, 'header_frame'):
|
||||||
self.header_frame.refresh_status()
|
self.header_frame.refresh_status()
|
||||||
|
|
||||||
@@ -384,7 +389,7 @@ class MainApplication(tk.Tk):
|
|||||||
self.after(100, self.actions.on_sidebar_button_click,
|
self.after(100, self.actions.on_sidebar_button_click,
|
||||||
restore_dest_folder)
|
restore_dest_folder)
|
||||||
self._process_queue()
|
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):
|
def _setup_log_window(self):
|
||||||
self.log_frame = ttk.Frame(self.content_frame)
|
self.log_frame = ttk.Frame(self.content_frame)
|
||||||
@@ -560,7 +565,7 @@ class MainApplication(tk.Tk):
|
|||||||
try:
|
try:
|
||||||
self.destroy()
|
self.destroy()
|
||||||
except tk.TclError:
|
except tk.TclError:
|
||||||
pass # App is already destroyed
|
pass # App is already destroyed
|
||||||
|
|
||||||
def _process_queue(self):
|
def _process_queue(self):
|
||||||
try:
|
try:
|
||||||
@@ -581,7 +586,8 @@ class MainApplication(tk.Tk):
|
|||||||
self.accurate_calculation_running = False
|
self.accurate_calculation_running = False
|
||||||
self.animated_icon.stop("DISABLE")
|
self.animated_icon.stop("DISABLE")
|
||||||
else:
|
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 current_folder_name == button_text:
|
||||||
if self.left_canvas_animation:
|
if self.left_canvas_animation:
|
||||||
self.left_canvas_animation.stop()
|
self.left_canvas_animation.stop()
|
||||||
@@ -622,11 +628,13 @@ class MainApplication(tk.Tk):
|
|||||||
self.drawing.update_target_projection()
|
self.drawing.update_target_projection()
|
||||||
self.animated_icon.stop("DISABLE")
|
self.animated_icon.stop("DISABLE")
|
||||||
self.task_progress.stop()
|
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.actions._set_ui_state(True)
|
||||||
self.genaue_berechnung_var.set(False)
|
self.genaue_berechnung_var.set(False)
|
||||||
self.accurate_calculation_running = 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':
|
if status == 'success':
|
||||||
self.info_label.config(
|
self.info_label.config(
|
||||||
text=Msg.STR["accurate_size_success"], foreground="#0078d7")
|
text=Msg.STR["accurate_size_success"], foreground="#0078d7")
|
||||||
@@ -753,22 +761,27 @@ class MainApplication(tk.Tk):
|
|||||||
self.encrypted_cb.config(state="disabled")
|
self.encrypted_cb.config(state="disabled")
|
||||||
|
|
||||||
self.actions._refresh_backup_options_ui()
|
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):
|
def _update_sync_mode_display(self):
|
||||||
use_trash_bin = self.config_manager.get_setting("use_trash_bin", False)
|
use_trash_bin = self.config_manager.get_setting("use_trash_bin", False)
|
||||||
no_trash_bin = self.config_manager.get_setting("no_trash_bin", False)
|
no_trash_bin = self.config_manager.get_setting("no_trash_bin", False)
|
||||||
|
|
||||||
if self.left_canvas_data.get('folder') == "Computer":
|
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
|
return
|
||||||
|
|
||||||
if no_trash_bin:
|
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:
|
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:
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class Actions:
|
|||||||
def _update_backup_type_controls(self):
|
def _update_backup_type_controls(self):
|
||||||
# Only applies to system backups in backup mode
|
# Only applies to system backups in backup mode
|
||||||
if self.app.mode != 'backup' or self.app.left_canvas_data.get('folder') != "Computer":
|
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.full_backup_cb.config(state='disabled')
|
||||||
self.app.incremental_cb.config(state='disabled')
|
self.app.incremental_cb.config(state='disabled')
|
||||||
return
|
return
|
||||||
@@ -50,11 +50,12 @@ class Actions:
|
|||||||
return
|
return
|
||||||
|
|
||||||
is_encrypted_backup = self.app.encrypted_var.get()
|
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)
|
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
|
if system_backups is None: # Encrypted, but not inspected
|
||||||
|
full_backup_exists = True # Assume one exists to be safe
|
||||||
else:
|
else:
|
||||||
for backup in system_backups:
|
for backup in system_backups:
|
||||||
# Match the encryption status and check if it's a full backup
|
# Match the encryption status and check if it's a full backup
|
||||||
@@ -79,7 +80,7 @@ class Actions:
|
|||||||
if self.app.encrypted_var.get():
|
if self.app.encrypted_var.get():
|
||||||
self.app.compressed_var.set(False)
|
self.app.compressed_var.set(False)
|
||||||
self.app.compressed_cb.config(state="disabled")
|
self.app.compressed_cb.config(state="disabled")
|
||||||
|
|
||||||
if self.app.compressed_var.get():
|
if self.app.compressed_var.get():
|
||||||
self.app.encrypted_var.set(False)
|
self.app.encrypted_var.set(False)
|
||||||
self.app.encrypted_cb.config(state="disabled")
|
self.app.encrypted_cb.config(state="disabled")
|
||||||
@@ -157,7 +158,8 @@ class Actions:
|
|||||||
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
|
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
|
||||||
exclude_file_paths.append(AppConfig.USER_EXCLUDE_LIST_PATH)
|
exclude_file_paths.append(AppConfig.USER_EXCLUDE_LIST_PATH)
|
||||||
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
|
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
|
base_dest = self.app.destination_path
|
||||||
correct_parent_dir = os.path.join(base_dest, "pybackup")
|
correct_parent_dir = os.path.join(base_dest, "pybackup")
|
||||||
@@ -195,7 +197,7 @@ class Actions:
|
|||||||
self.app.navigation.toggle_mode(
|
self.app.navigation.toggle_mode(
|
||||||
self.app.mode, trigger_calculation=False)
|
self.app.mode, trigger_calculation=False)
|
||||||
|
|
||||||
self.app.log_window.clear_log()
|
# self.app.log_window.clear_log()
|
||||||
|
|
||||||
REVERSE_FOLDER_MAP = {
|
REVERSE_FOLDER_MAP = {
|
||||||
"Computer": "Computer",
|
"Computer": "Computer",
|
||||||
@@ -245,7 +247,8 @@ class Actions:
|
|||||||
|
|
||||||
self._start_left_canvas_calculation(
|
self._start_left_canvas_calculation(
|
||||||
button_text, str(folder_path), icon_name, extra_info)
|
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):
|
def _start_left_canvas_calculation(self, button_text, folder_path, icon_name, extra_info):
|
||||||
self.app.start_pause_button.config(state="disabled")
|
self.app.start_pause_button.config(state="disabled")
|
||||||
@@ -340,7 +343,8 @@ class Actions:
|
|||||||
if self.app.mode == "backup":
|
if self.app.mode == "backup":
|
||||||
# Unmount previous destination if it was mounted
|
# Unmount previous destination if it was mounted
|
||||||
if self.app.destination_path:
|
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
|
self.app.destination_path = path
|
||||||
|
|
||||||
@@ -364,13 +368,13 @@ class Actions:
|
|||||||
size_str = f"{used / (1024**3):.2f} GB / {total / (1024**3):.2f} GB"
|
size_str = f"{used / (1024**3):.2f} GB / {total / (1024**3):.2f} GB"
|
||||||
|
|
||||||
self.app.right_canvas_data.update({
|
self.app.right_canvas_data.update({
|
||||||
'folder': os.path.basename(path.rstrip('/')),
|
'folder': os.path.basename(path.rstrip('/')),
|
||||||
'path_display': path,
|
'path_display': path,
|
||||||
'size': size_str
|
'size': size_str
|
||||||
})
|
})
|
||||||
self.app.config_manager.set_setting(
|
self.app.config_manager.set_setting(
|
||||||
"backup_destination_path", path)
|
"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.redraw_right_canvas()
|
||||||
self.app.drawing.update_target_projection()
|
self.app.drawing.update_target_projection()
|
||||||
|
|
||||||
@@ -381,7 +385,7 @@ class Actions:
|
|||||||
|
|
||||||
elif self.app.mode == "restore":
|
elif self.app.mode == "restore":
|
||||||
self.app.right_canvas_data.update({
|
self.app.right_canvas_data.update({
|
||||||
'folder': os.path.basename(path.rstrip('/')),
|
'folder': os.path.basename(path.rstrip('/')),
|
||||||
'path_display': path,
|
'path_display': path,
|
||||||
'size': ''
|
'size': ''
|
||||||
})
|
})
|
||||||
@@ -599,7 +603,7 @@ class Actions:
|
|||||||
self.app.start_pause_button["text"] = Msg.STR["cancel_backup"]
|
self.app.start_pause_button["text"] = Msg.STR["cancel_backup"]
|
||||||
self.app.update_idletasks()
|
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._set_ui_state(False, allow_log_and_backup_toggle=True)
|
||||||
|
|
||||||
self.app.animated_icon.start()
|
self.app.animated_icon.start()
|
||||||
@@ -607,9 +611,10 @@ class Actions:
|
|||||||
if self.app.mode == "backup":
|
if self.app.mode == "backup":
|
||||||
source_folder = self.app.left_canvas_data.get('folder')
|
source_folder = self.app.left_canvas_data.get('folder')
|
||||||
source_size_bytes = self.app.source_size_bytes
|
source_size_bytes = self.app.source_size_bytes
|
||||||
|
|
||||||
if not source_folder:
|
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.backup_is_running = False
|
||||||
self.app.start_pause_button["text"] = Msg.STR["start"]
|
self.app.start_pause_button["text"] = Msg.STR["start"]
|
||||||
self._set_ui_state(True)
|
self._set_ui_state(True)
|
||||||
@@ -620,7 +625,7 @@ class Actions:
|
|||||||
self._start_system_backup(mode, source_size_bytes)
|
self._start_system_backup(mode, source_size_bytes)
|
||||||
else:
|
else:
|
||||||
self._start_user_backup()
|
self._start_user_backup()
|
||||||
else: # restore mode
|
else: # restore mode
|
||||||
# Restore logic would go here
|
# Restore logic would go here
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -641,9 +646,11 @@ class Actions:
|
|||||||
password = None
|
password = None
|
||||||
if is_encrypted:
|
if is_encrypted:
|
||||||
username = os.path.basename(base_dest.rstrip('/'))
|
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:
|
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.backup_is_running = False
|
||||||
self.app.start_pause_button["text"] = Msg.STR["start"]
|
self.app.start_pause_button["text"] = Msg.STR["start"]
|
||||||
self._set_ui_state(True)
|
self._set_ui_state(True)
|
||||||
@@ -659,7 +666,8 @@ class Actions:
|
|||||||
date_str = now.strftime("%d-%m-%Y")
|
date_str = now.strftime("%d-%m-%Y")
|
||||||
time_str = now.strftime("%H:%M:%S")
|
time_str = now.strftime("%H:%M:%S")
|
||||||
folder_name = f"{date_str}_{time_str}_system_{mode}"
|
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
|
self.app.current_backup_path = final_dest
|
||||||
|
|
||||||
source_size_bytes = self.app.left_canvas_data.get('total_bytes', 0)
|
source_size_bytes = self.app.left_canvas_data.get('total_bytes', 0)
|
||||||
@@ -685,8 +693,7 @@ class Actions:
|
|||||||
source_size=source_size_bytes,
|
source_size=source_size_bytes,
|
||||||
is_compressed=is_compressed,
|
is_compressed=is_compressed,
|
||||||
is_encrypted=is_encrypted,
|
is_encrypted=is_encrypted,
|
||||||
mode=mode,
|
mode=mode)
|
||||||
password=password)
|
|
||||||
|
|
||||||
def _start_user_backup(self):
|
def _start_user_backup(self):
|
||||||
base_dest = self.app.destination_path
|
base_dest = self.app.destination_path
|
||||||
@@ -706,29 +713,33 @@ class Actions:
|
|||||||
password = None
|
password = None
|
||||||
if is_encrypted:
|
if is_encrypted:
|
||||||
username = os.path.basename(base_dest.rstrip('/'))
|
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:
|
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.backup_is_running = False
|
||||||
self.app.start_pause_button["text"] = Msg.STR["start"]
|
self.app.start_pause_button["text"] = Msg.STR["start"]
|
||||||
self._set_ui_state(True)
|
self._set_ui_state(True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Determine mode for user backup based on UI selection
|
||||||
|
mode = "full" if self.app.vollbackup_var.get() else "incremental"
|
||||||
|
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
date_str = now.strftime("%d-%m-%Y")
|
date_str = now.strftime("%d-%m-%Y")
|
||||||
time_str = now.strftime("%H:%M:%S")
|
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)
|
final_dest = os.path.join(base_dest, folder_name)
|
||||||
self.app.current_backup_path = final_dest
|
self.app.current_backup_path = final_dest
|
||||||
|
|
||||||
is_dry_run = self.app.testlauf_var.get()
|
is_dry_run = self.app.testlauf_var.get()
|
||||||
is_compressed = self.app.compressed_var.get()
|
is_compressed = self.app.compressed_var.get()
|
||||||
use_trash_bin = self.app.config_manager.get_setting("use_trash_bin", False)
|
use_trash_bin = self.app.config_manager.get_setting(
|
||||||
no_trash_bin = self.app.config_manager.get_setting("no_trash_bin", False)
|
"use_trash_bin", False)
|
||||||
|
no_trash_bin = self.app.config_manager.get_setting(
|
||||||
# Determine mode for user backup based on UI selection
|
"no_trash_bin", False)
|
||||||
mode = "full" if self.app.vollbackup_var.get() else "incremental"
|
|
||||||
|
|
||||||
self.app.backup_manager.start_backup(
|
self.app.backup_manager.start_backup(
|
||||||
queue=self.app.queue,
|
queue=self.app.queue,
|
||||||
@@ -736,11 +747,18 @@ class Actions:
|
|||||||
dest_path=final_dest,
|
dest_path=final_dest,
|
||||||
is_system=False,
|
is_system=False,
|
||||||
is_dry_run=is_dry_run,
|
is_dry_run=is_dry_run,
|
||||||
exclude_files=None,
|
exclude_files=None,
|
||||||
source_size=source_size_bytes,
|
source_size=source_size_bytes,
|
||||||
is_compressed=is_compressed,
|
is_compressed=is_compressed,
|
||||||
is_encrypted=is_encrypted,
|
is_encrypted=is_encrypted,
|
||||||
mode=mode,
|
mode=mode,
|
||||||
password=password,
|
|
||||||
use_trash_bin=use_trash_bin,
|
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
|
||||||
|
|||||||
@@ -148,6 +148,25 @@ class BackupContentFrame(ttk.Frame):
|
|||||||
|
|
||||||
self.base_backup_path = backup_path
|
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")
|
pybackup_dir = os.path.join(backup_path, "pybackup")
|
||||||
|
|
||||||
if not os.path.isdir(pybackup_dir):
|
if not os.path.isdir(pybackup_dir):
|
||||||
@@ -168,7 +187,6 @@ class BackupContentFrame(ttk.Frame):
|
|||||||
self.system_backups_frame.show(backup_path, [])
|
self.system_backups_frame.show(backup_path, [])
|
||||||
self.user_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"
|
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)
|
last_view = self.app.config_manager.get_setting(config_key, 0)
|
||||||
self._switch_view(last_view)
|
self._switch_view(last_view)
|
||||||
|
|||||||
Reference in New Issue
Block a user