Compare commits

...

24 Commits

Author SHA1 Message Date
4a96fd1547 Checkboxes on main window works now correct 2025-09-12 01:08:55 +02:00
eff7569d71 In the entry window, the buttons were set up and a frameless style was applied. Between the button was added still separators as a horizontal to make the look better 2025-09-11 23:39:02 +02:00
d6ead1694c add methode for font and color on infolabel part one 2025-09-11 15:37:35 +02:00
3b57df2ffa renema accurate to Incrementel 2025-09-11 13:46:56 +02:00
d7dd4215c0 fix MessageDialog, rename variables 2025-09-11 13:11:14 +02:00
22144859d8 feat: Implement Hard Reset and Refactor PasswordDialog
This commit introduces a new "Hard Reset" functionality in the settings, allowing users to reset the application to its initial state by deleting the configuration directory.

Key changes include:
- Added a "Hard Reset" button and a dedicated confirmation frame in `settings_frame.py`.
- Implemented the logic to delete the `.config/py_backup` directory and restart the application.
- Enhanced the hard reset process to unmount encrypted drives if they are mounted, prompting for a password if necessary.

To improve modularity and maintainability, the PasswordDialog class has been refactored:
- Moved PasswordDialog from `pyimage_ui/password_dialog.py` to `shared_libs/message.py`.
- Updated all references and imports to the new location.
- Externalized all user-facing strings in PasswordDialog for translation support.

Additionally, several bug fixes and improvements were made:
- Corrected object access hierarchy in `settings_frame.py` and `advanced_settings_frame.py` by passing manager instances directly.
- Handled `FileNotFoundError` in `actions.py` when selecting remote backup destinations, preventing crashes and displaying "N/A" for disk usage.
- Replaced incorrect `calculating_animation` reference with `animated_icon` in `actions.py`.
- Added missing translation keys in `pbp_app_config.py`.
2025-09-11 01:08:38 +02:00
8a70fe2320 backup view corrected 2025-09-10 18:56:19 +02:00
9fd032e9b4 add new button for refresh log disable 2025-09-10 14:38:24 +02:00
444650f9f0 fix(ui): Testlauf-Button nach inkrementeller Berechnung freigeben
Der Button für den Testlauf wurde nicht freigegeben, wenn die inkrementelle Berechnung abgeschlossen war. Dies wurde behoben, indem der Status des Buttons in der UI-Refresh-Funktion korrekt gesetzt wird.

fix(logging): Logger früher initialisieren

Der Logger wurde zu spät initialisiert, was dazu führte, dass einige Log-Meldungen auf der Konsole statt im Log-Fenster ausgegeben wurden. Die Initialisierung wurde vorgezogen, um alle Meldungen abzufangen.

refactor: Variablen umbenennen

Einige Variablen wurden für eine bessere Lesbarkeit und Konsistenz im gesamten Code umbenannt (z.B. `testlauf_var` zu `test_run_var`).
2025-09-10 12:04:33 +02:00
b6a0bb82f1 feat(ui): Ersetze Checkboxen und Radio-Buttons durch Switches
Dieses Commit ersetzt die meisten ttk.Checkbutton- und ttk.Radiobutton-Widgets in der gesamten Anwendung durch einen benutzerdefinierten "Switch"-Stil, um ein moderneres Erscheinungsbild zu erzielen.

Die Änderungen umfassen:
- **Hauptfenster**:
  - Umwandlung der Backup-Optionen (Voll, Inkrementell, Komprimiert, Verschlüsselt) in Switches.
  - Ersetzung der "Genaue Grössenberechnung"-Checkbox durch einen normalen Button.
  - Verschiebung der "Testlauf"- und "Sicherheit umgehen"-Switches in die Seitenleiste unter "Einstellungen".
- **Scheduler**:
  - Ersetzung aller Radio-Buttons und Checkboxen durch Switches, mit implementierter Logik zur Gewährleistung der exklusiven Auswahl für Backup-Typ und Frequenz.
- **Erweiterte Einstellungen**:
  - Umwandlung aller Checkboxen im Abschnitt "Backup-Standards" in Switches.
- **Styling**:
  - Hinzufügen eines neuen Stils `Switch2.TCheckbutton` für die Switches in der Seitenleiste, um sie an das dunkle Thema der Seitenleiste anzupassen. Die Konfiguration erfolgt direkt in `main_app.py`.
- **Fehlerbehebungen**:
  - Behebung eines `AttributeError`-Absturzes, der durch die Verschiebung der Switches vor der Deklaration ihrer `tk.BooleanVar`-Variablen verursacht wurde.
  - Anpassung der zugehörigen Logik in `pyimage_ui/actions.py` an den neuen Button.
2025-09-10 01:04:10 +02:00
fd6bb6cc1b feat: Implement backup option logic and UI improvements
This commit introduces the following changes:

- Enhanced Backup Option Logic:
  - Implemented mutual exclusivity between 'Compressed' and 'Incremental' backups:
    - If 'Compressed' is selected, 'Incremental' is deselected and disabled, and 'Full' is automatically selected.
  - Implemented mutual exclusivity between 'Compressed' and 'Encrypted' backups:
    - If 'Compressed' is selected, 'Encrypted' is deselected and disabled.
    - If 'Encrypted' is selected, 'Compressed' is deselected and disabled.
  - If 'Incremental' is selected, 'Compressed' is deselected and disabled.
  - These rules are applied consistently across the main backup window, advanced settings, and scheduler.

- UI Improvements:
  - Added 'Full', 'Incremental', 'Compressed', and 'Encrypted' checkboxes to the scheduler view.
  - Adjusted the layout in the scheduler view to place 'Backup Options' next to 'Folders to back up'.
  - Added missing string definitions for new UI elements in core/pbp_app_config.py.

- Refactoring:
  - Updated _refresh_backup_options_ui in pyimage_ui/actions.py to handle the new logic.
  - Modified _on_compression_toggle in pyimage_ui/advanced_settings_frame.py and _on_compression_toggle_scheduler in pyimage_ui/scheduler_frame.py to reflect the updated exclusivity rules.
  - Adjusted _save_job and _load_scheduled_jobs in pyimage_ui/scheduler_frame.py to include and parse the new backup options.
  - Updated _parse_job_comment in core/backup_manager.py to correctly parse new backup options from cron job comments.
2025-09-09 19:04:45 +02:00
e932dff8a6 Fix: Adjust spacing in backup type display 2025-09-09 16:49:30 +02:00
94a44881e6 feat(ui): Refine "Backup Content" view display logic
This commit implements several UI/UX improvements for the "Backup Content" list view based on user feedback.

- feat(ui): User backups are now grouped by their full/incremental chains, similar to system backups, for a more logical and organized view.
- feat(ui): The color scheme for backup chains has been simplified. Each chain (a full backup and its incrementals) now shares a single color to improve visual grouping.
- feat(ui): Incremental backups are now denoted by a ▲ icon in the Type column instead of a different color or font style, providing a clear and clean indicator.
- fix(ui): Adjusted all column widths in the backup lists to ensure all data (especially Date and Time) is fully visible without truncation.
2025-09-09 14:22:37 +02:00
94afeb5d45 fix: Correct incremental size calculations and rsync handling
This commit refactors the backup size calculation logic and fixes several bugs related to rsync argument passing and UI actions.

- refactor(core): Centralized all incremental backup size calculation logic within the BackupManager class. This removes duplicated and buggy code from the DataProcessing class and ensures both post-backup reporting and pre-backup estimation use a single, robust implementation.

- fix(backup): The pre-backup "accurate size calculation" now works correctly for all backup types. It uses an `rsync --dry-run` with a proper temporary directory and correctly handles root permissions for system backups.

- fix(backup): The post-backup size reporting for incremental system backups is now accurate. It uses a root-privileged command to inspect file links and calculate the true disk usage, avoiding permission errors.

- fix(backup): Corrected multiple quoting issues with rsync arguments (`--link-dest`, `--exclude-from`) that caused backups to fail or misbehave.

- fix(ui): Fixed a TypeError that occurred when deleting a system backup from the "Backup Content" view.

- feat(backup): Added the `-v` (verbose) flag to rsync for user backups to provide better feedback in the UI.
2025-09-09 13:07:56 +02:00
474930e6d0 feat(ui): Improve "Backup Content" view default behavior
Implements a more intuitive default view for the "Backup Content" screen based on user context.

- After a backup is completed, the "Backup Content" view will now automatically open the tab corresponding to the type of backup just performed (System or User).
- On a fresh application start, the "Backup Content" view will now always default to showing the "System" backups tab.
- This is achieved by adding state variables to the main app and modifying the navigation logic, while removing the previous behavior of saving the last-viewed tab to the config.
2025-09-09 02:54:34 +02:00
a646b9d13a fix(settings): Improve settings logic and fix UI bugs
This commit addresses several bugs and improves the logic of the settings panels.

- fix(settings): The "Reset to default settings" action no longer deletes the user-defined file/folder exclusion list.
- fix(settings): Corrected a bug in the "Add to exclude list" function where new entries were not being written correctly. The logic is now more robust and prevents duplicate entries.
- fix(ui): Fixed a TclError crash in Advanced Settings caused by mixing `grid` and `pack` geometry managers.
- feat(settings): Implemented mutual exclusivity for the "trash bin" and "compression/encryption" checkboxes to prevent invalid configurations.
- i18n: Improved and clarified the English text for the trash bin feature descriptions to enable better translation.
2025-09-09 02:39:16 +02:00
f11f30ba74 refactor(core): Implement new backup directory structure
Refactor the core backup and encryption logic to use a new, consistent directory structure. This new structure separates encrypted and unencrypted backups and centralizes metadata, making the system more robust and easier to manage.

Key changes:
- Implemented a new directory scheme:
  /pybackup/
  ├── unencrypted/{system,user}/<source>/
  ├── encrypted/{system,user}/<source>/  (mount point)
  ├── metadata/
  └── pybackup_encrypted.luks
- Reworked path generation logic in BackupManager and EncryptionManager to support the new structure.
- All backup, restore, and listing operations now correctly resolve paths based on the new scheme.

This also includes several bug fixes identified during the refactoring:
- fix(backup): Correctly quote rsync paths for user backups to prevent "No such file or directory" errors.
- fix(encryption): Change key lookup order to Keyring -> Keyfile -> Password Prompt, as requested.
- fix(ui): Remove eager auto-mount on startup to prevent unexpected password prompts. The app now only mounts when required by a user action.
2025-09-09 01:27:29 +02:00
798134dd20 new method to detect backup 2025-09-08 21:02:03 +02:00
95a55a7d4c remove double rows 2025-09-08 09:51:00 +02:00
a8cbfcb380 secure unmount on close the app 2025-09-08 00:59:24 +02:00
4aa38ab33d feat(ui): Improve backup content view and settings
This commit introduces several UI enhancements and bug fixes based on user feedback.

- **Backup Content View:**
  - The "Folder" column is now displayed before the "Comment" column for user backups.
  - The "Folder" column now shows the simple source name (e.g., "Dokumente") instead of the full backup name.
  - Column alignment and widths have been adjusted in both system and user backup views for better readability.
  - A timing issue causing progress bars to be missing after unlocking an encrypted volume has been resolved.

- **Advanced Settings:**
  - New options have been added to control file deletion behavior (use trash or delete directly).
  - The layout of the advanced settings panel has been refined.

- **Encryption:**
  - Introduces a keyfile-based authentication mechanism for LUKS containers, reducing the need for password prompts.
  - Replaces temporary script files with a dedicated runner script (`privileged_script_runner.sh`) for executing root commands, improving security and robustness.
2025-09-07 23:37:12 +02:00
8c6256e94c refactor(encryption): Improve resize logic and password handling
This commit refactors the encrypted container management to significantly improve usability and robustness.

- **Reduced Password Prompts:** The entire resize operation (unmount, truncate, check, resize, mount) is now consolidated into a single script. This reduces the maximum number of password prompts during a backup with resize from four down to two.
- **Fix "No Space Left" Error:** The resize script is now more robust, including a filesystem check (`e2fsck`) before resizing to prevent the filesystem from failing to expand. This resolves the critical "No space left on device" error.
- **Session-wide Password Cache:** A simple in-memory cache for the LUKS password has been introduced. This prevents further password prompts when unmounting containers, for example, when the application is closed.
- **Improved Logging:** Privileged script execution now logs stderr output even on success, aiding future diagnostics.
2025-09-07 20:02:20 +02:00
dbaa623b17 fix(backup): Resolve multiple issues in encrypted backup handling
This commit addresses several bugs related to the mounting, unmounting, and deletion of encrypted backups, as well as a crash when listing backups.

The key changes are:
- **Fix Double Mount on View:** Removed redundant mount operation when viewing encrypted backup contents. The mount is now handled by a single, centralized function.
- **Fix Deletion of Encrypted Backups:**
    - The container is no longer re-mounted if already open, preventing a second password prompt.
    - Deletion of encrypted *user* backups is now performed with user-level permissions, removing the need for a third password prompt via pkexec.
- **Fix UI Refresh after Deletion:** The backup list now correctly refreshes after a backup is deleted.
- **Fix Crash on Empty Backup List:** Resolved an `UnboundLocalError` that occurred when listing backups from an empty or non-existent backup directory.
- **Improve Mount Detection:** The `is_mounted` check is now more robust to prevent race conditions or other OS-level inconsistencies.
2025-09-07 19:02:39 +02:00
73e6e42485 Refactor: Encrypted backups to use direct LUKS
Replaced the LVM-on-a-file implementation with a more robust, industry-standard LUKS-on-a-file approach.

This change was motivated by persistent and hard-to-debug errors related to LVM state management and duplicate loop device detection during repeated mount/unmount cycles.

The new implementation provides several key benefits:
- **Robustness:** Eliminates the entire LVM layer, which was the root cause of the mount/unmount failures.
- **Improved UX:** Drastically reduces the number of password prompts for encrypted user backups. By changing ownership of the mountpoint, rsync can run with user privileges.
- **Enhanced Security:** The file transfer process (rsync) for user backups no longer runs with root privileges.
- **Better Usability:** Encrypted containers are now left mounted during the application's lifecycle and are only unmounted on exit, improving workflow for consecutive operations.
2025-09-07 15:58:28 +02:00
21 changed files with 1835 additions and 1907 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,8 @@
# pyimage/core/data_processing.py
import os
import fnmatch
import shutil
import re
import subprocess
from queue import Empty
from core.pbp_app_config import AppConfig, Msg
from core.pbp_app_config import AppConfig
from shared_libs.logger import app_logger
@@ -66,7 +63,6 @@ class DataProcessing:
if exclude_patterns is None:
exclude_patterns = []
# Compile exclude patterns into a single regex for performance
if exclude_patterns:
exclude_regex = re.compile(
'|'.join(fnmatch.translate(p) for p in exclude_patterns))
@@ -75,7 +71,7 @@ class DataProcessing:
for dirpath, dirnames, filenames in os.walk(path, topdown=True):
if stop_event.is_set():
return # Stop the calculation
return
if exclude_regex:
dirnames[:] = [d for d in dirnames if not exclude_regex.match(
@@ -83,7 +79,7 @@ class DataProcessing:
for f in filenames:
if stop_event.is_set():
return # Stop the calculation
return
fp = os.path.join(dirpath, f)
if not exclude_regex or not exclude_regex.match(fp):
@@ -101,11 +97,11 @@ class DataProcessing:
total_size = 0
for dirpath, dirnames, filenames in os.walk(path, topdown=True):
if stop_event.is_set():
return # Stop the calculation
return
for f in filenames:
if stop_event.is_set():
return # Stop the calculation
return
fp = os.path.join(dirpath, f)
if not os.path.islink(fp):
@@ -116,91 +112,3 @@ class DataProcessing:
if not stop_event.is_set():
self.app.queue.put((button_text, total_size, mode))
def get_incremental_backup_size(self, source_path: str, dest_path: str, is_system: bool, exclude_files: list = None) -> int:
"""
Calculates the approximate size of an incremental backup using rsync's dry-run feature.
This is much faster than walking the entire file tree.
"""
app_logger.log(f"Calculating incremental backup size for source: {source_path}")
parent_dest = os.path.dirname(dest_path)
if not os.path.exists(parent_dest):
# If the parent destination doesn't exist, there are no previous backups to link to.
# In this case, the incremental size is the full size of the source.
# We can use the existing full-size calculation method.
# This is a simplified approach for the estimation.
# A more accurate one would run rsync without --link-dest.
app_logger.log("Destination parent does not exist, cannot calculate incremental size. Returning 0.")
return 0
# Find the latest backup to link against
try:
backups = sorted([d for d in os.listdir(parent_dest) if os.path.isdir(os.path.join(parent_dest, d))], reverse=True)
if not backups:
app_logger.log("No previous backups found. Incremental size is full size.")
return 0 # Or trigger a full size calculation
latest_backup_path = os.path.join(parent_dest, backups[0])
except FileNotFoundError:
app_logger.log("Could not list backups, assuming no prior backups exist.")
return 0
command = []
if is_system:
command.extend(['pkexec', 'rsync', '-aAXHvn', '--stats'])
else:
command.extend(['rsync', '-avn', '--stats'])
command.append(f"--link-dest={latest_backup_path}")
if exclude_files:
for exclude_file in exclude_files:
command.append(f"--exclude-from={exclude_file}")
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
command.append(f"--exclude-from={AppConfig.MANUAL_EXCLUDE_LIST_PATH}")
# The destination for a dry run can be a dummy path, but it must exist.
# Let's use a temporary directory.
dummy_dest = os.path.join(parent_dest, "dry_run_dest")
os.makedirs(dummy_dest, exist_ok=True)
command.extend([source_path, dummy_dest])
app_logger.log(f"Executing rsync dry-run command: {' '.join(command)}")
try:
result = subprocess.run(command, capture_output=True, text=True, check=False)
# Clean up the dummy directory
shutil.rmtree(dummy_dest)
if result.returncode != 0:
app_logger.log(f"Rsync dry-run failed with code {result.returncode}: {result.stderr}")
return 0
output = result.stdout + "\n" + result.stderr
# The regex now accepts dots as thousands separators (e.g., 1.234.567).
match = re.search(r"Total transferred file size: ([\d,.]+) bytes", output)
if match:
# Remove both dots and commas before converting to an integer.
size_str = match.group(1).replace(',', '').replace('.', '')
size_bytes = int(size_str)
app_logger.log(f"Estimated incremental backup size: {size_bytes} bytes")
return size_bytes
else:
app_logger.log("Could not find 'Total transferred file size' in rsync output.")
# Log the output just in case something changes in the future
app_logger.log(f"Full rsync output for debugging:\n{output}")
return 0
except FileNotFoundError:
app_logger.log("Error: 'rsync' or 'pkexec' command not found.")
return 0
except Exception as e:
app_logger.log(f"An unexpected error occurred during incremental size calculation: {e}")
return 0
# The queue processing logic has been moved to main_app.py
# to fix a race condition and ensure all queue messages are handled correctly.

View File

@@ -1,16 +1,16 @@
import keyring
import keyring.errors
from keyring.backends import SecretService
import os
import shutil
import subprocess
import tempfile
import stat
import re
from typing import Optional, List
import math
from typing import Optional, Tuple
from core.pbp_app_config import AppConfig
from pyimage_ui.password_dialog import PasswordDialog
from core.pbp_app_config import AppConfig, Msg
from shared_libs.message import PasswordDialog
import json
class EncryptionManager:
@@ -22,32 +22,44 @@ class EncryptionManager:
self.logger = logger
self.app = app
self.service_id = "py-backup-encryption"
self.session_password = None
self.mounted_destinations = set()
self.auth_method = None
self.is_mounting = False
self.password_cache = {}
self.lock_file = AppConfig.LOCK_FILE_PATH
def get_key_file_path(self, base_dest_path: str) -> str:
"""Generates the standard path for the key file for a given destination."""
key_filename = f"keyfile_{os.path.basename(base_dest_path.rstrip('/'))}.key"
return os.path.join(AppConfig.CONFIG_DIR, key_filename)
def _write_lock_file(self, data):
with open(self.lock_file, 'w') as f:
json.dump(data, f)
def _read_lock_file(self):
if not self.lock_file.exists():
return []
with open(self.lock_file, 'r') as f:
try:
return json.load(f)
except json.JSONDecodeError:
return []
def add_to_lock_file(self, base_path, mapper_name):
locks = self._read_lock_file()
if not any(lock['base_path'] == base_path for lock in locks):
locks.append({"base_path": base_path, "mapper_name": mapper_name})
self._write_lock_file(locks)
def remove_from_lock_file(self, base_path):
locks = self._read_lock_file()
updated_locks = [
lock for lock in locks if lock['base_path'] != base_path]
self._write_lock_file(updated_locks)
def get_password_from_keyring(self, username: str) -> Optional[str]:
try:
return keyring.get_password(self.service_id, username)
except keyring.errors.InitError as e:
self.logger.log(f"Could not initialize keyring: {e}")
return None
except Exception as e:
self.logger.log(f"Could not get password from keyring: {e}")
return None
def is_key_in_keyring(self, username: str) -> bool:
try:
return self.get_password_from_keyring(username) is not None
except Exception as e:
self.logger.log(f"Could not check password in keyring: {e}")
return False
return self.get_password_from_keyring(username) is not None
def set_password_in_keyring(self, username: str, password: str) -> bool:
try:
@@ -59,279 +71,315 @@ class EncryptionManager:
return False
def get_password(self, username: str, confirm: bool = True) -> Optional[str]:
if self.session_password:
return self.session_password
if username in self.password_cache:
return self.password_cache[username]
password = self.get_password_from_keyring(username)
if password:
self.session_password = password
self.password_cache[username] = password
return password
dialog = PasswordDialog(
self.app, title=f"Enter password for {username}", confirm=confirm)
self.app, title=f"Enter password for {username}", confirm=confirm, translations=Msg.STR)
password, save_to_keyring = dialog.get_password()
if password and save_to_keyring:
self.set_password_in_keyring(username, password)
if password:
self.session_password = password
self.password_cache[username] = password
if save_to_keyring:
self.set_password_in_keyring(username, password)
return password
def get_container_path(self, base_dest_path: str) -> str:
"""Returns the path for the LUKS container file itself."""
pybackup_dir = os.path.join(base_dest_path, "pybackup")
return os.path.join(pybackup_dir, "pybackup_encrypted.luks")
def get_key_file_path(self, base_dest_path: str) -> str:
pybackup_dir = os.path.join(base_dest_path, "pybackup")
return os.path.join(pybackup_dir, "luks.keyfile")
def create_and_add_key_file(self, base_dest_path: str, password: str) -> Optional[str]:
key_file_path = self.get_key_file_path(base_dest_path)
container_path = self.get_container_path(base_dest_path)
try:
with open(key_file_path, 'wb') as f:
f.write(os.urandom(4096))
os.chmod(key_file_path, 0o400)
self.logger.log(f"Generated new key file at {key_file_path}")
script = f'cryptsetup luksAddKey "{container_path}" "{key_file_path}"'
if self._execute_as_root(script, password):
self.logger.log(
"Successfully added key file to LUKS container.")
return key_file_path
else:
self.logger.log("Failed to add key file to LUKS container.")
os.remove(key_file_path)
return None
except Exception as e:
self.logger.log(f"Error creating key file: {e}")
if os.path.exists(key_file_path):
os.remove(key_file_path)
return None
def _get_password_or_key_cmd(self, base_dest_path: str, username: str) -> Tuple[str, Optional[str]]:
# 1. Check cache and keyring (without triggering dialog)
password = self.password_cache.get(
username) or self.get_password_from_keyring(username)
if password:
self.logger.log(
"Using password from cache or keyring for LUKS operation.")
self.password_cache[username] = password # ensure it's cached
return "-", password
# 2. Check for key file
key_file_path = self.get_key_file_path(base_dest_path)
if os.path.exists(key_file_path):
self.logger.log(
f"Using key file for LUKS operation: {key_file_path}")
return f'--key-file "{key_file_path}"'
# 3. If nothing found, prompt for password
self.logger.log(
"No password in keyring and no keyfile found. Prompting user.")
# This will now definitely open the dialog
password = self.get_password(username, confirm=False)
if not password:
return "", None
return "-", password
def is_encrypted(self, base_dest_path: str) -> bool:
if os.path.basename(base_dest_path) == "pybackup":
pybackup_dir = base_dest_path
else:
pybackup_dir = os.path.join(base_dest_path, "pybackup")
user_encrypt_dir = os.path.join(pybackup_dir, "user_encrypt")
container_path = os.path.join(user_encrypt_dir, "pybackup_lvm.img")
return os.path.exists(container_path)
return os.path.exists(self.get_container_path(base_dest_path))
def get_mount_point(self, base_dest_path: str) -> str:
"""Constructs the unique, static mount point path for a given destination."""
return os.path.join(base_dest_path, "pybackup", "encrypted")
def is_mounted(self, base_dest_path: str) -> bool:
if os.path.basename(base_dest_path) == "pybackup":
pybackup_dir = base_dest_path
mount_point = self.get_mount_point(base_dest_path)
return os.path.ismount(mount_point) or base_dest_path in self.mounted_destinations
def mount_for_deletion(self, base_dest_path: str, is_system: bool, password: str) -> Optional[str]:
self.logger.log("Mounting container for deletion operation.")
if self._open_and_mount(base_dest_path, is_system, password):
mount_point = self.get_mount_point(base_dest_path)
self.mounted_destinations.add(base_dest_path)
return mount_point
self.logger.log("Failed to mount container for deletion.")
return None
def prepare_encrypted_destination(self, base_dest_path: str, is_system: bool, source_size: int, queue) -> Optional[str]:
container_path = self.get_container_path(base_dest_path)
if os.path.exists(container_path):
return self._handle_existing_container(base_dest_path, is_system, source_size, queue)
else:
pybackup_dir = os.path.join(base_dest_path, "pybackup")
mount_point = os.path.join(pybackup_dir, "encrypted")
return os.path.ismount(mount_point)
return self._handle_new_container(base_dest_path, is_system, source_size, queue)
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
def _handle_existing_container(self, base_dest_path: str, is_system: bool, source_size: int, queue) -> Optional[str]:
self.logger.log("Handling existing container.")
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('/'))
mount_point = self.get_mount_point(base_dest_path)
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
if not self.is_mounted(base_dest_path):
if not self._open_and_mount(base_dest_path, is_system):
self.logger.log("Failed to mount container for size check.")
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
free_space = shutil.disk_usage(mount_point).free
required_space = int(source_size * 1.15)
if required_space > free_space:
self.logger.log(
f"Resize needed. Free: {free_space}, Required: {required_space}")
queue.put(('status_update', "Container zu klein. Vergrößere..."))
current_total = shutil.disk_usage(mount_point).total
needed_additional = required_space - free_space
new_total_size = current_total + needed_additional
container_path = self.get_container_path(base_dest_path)
mapper_name = f"pybackup_luks_{username}"
chown_cmd = self._get_chown_command(mount_point, is_system)
key_or_pass_arg, password = self._get_password_or_key_cmd(
base_dest_path, username)
if not key_or_pass_arg:
return None
luks_open_cmd = f'echo -n "$LUKSPASS" | cryptsetup luksOpen "{container_path}" {mapper_name} {key_or_pass_arg}' if password else f'cryptsetup luksOpen "{container_path}" {mapper_name} {key_or_pass_arg}'
resize_script = f"""
# Unmount cleanly first
umount -l \"{mount_point}\" || true
cryptsetup luksClose {mapper_name} || true
# Resize container file
truncate -s {int(new_total_size)} \"{container_path}\"
# Re-open, check, and resize filesystem
{luks_open_cmd}
e2fsck -fy \"/dev/mapper/{mapper_name}\"
resize2fs \"/dev/mapper/{mapper_name}\"
# Now mount it
mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\"
{chown_cmd}
"""
if not self._execute_as_root(resize_script, password):
self.logger.log("Failed to execute resize and remount script.")
return None
if not self.is_mounted(base_dest_path):
self.logger.log(
"CRITICAL: Mount failed after resize script, but script reported success. Aborting.")
return None
self.mounted_destinations.add(base_dest_path)
return mount_point
def _handle_new_container(self, base_dest_path: str, is_system: bool, source_size: int, queue) -> Optional[str]:
self.logger.log("Handling new container creation.")
size_gb = math.ceil((source_size * 1.2) / (1024**3)) + 5
username = os.path.basename(base_dest_path.rstrip('/'))
password = self.get_password(username, confirm=True)
if not password:
return None
container_path = self.get_container_path(base_dest_path)
mount_point = self.get_mount_point(base_dest_path)
mapper_name = f"pybackup_luks_{username}"
chown_cmd = self._get_chown_command(mount_point, is_system)
script = f"""
mkdir -p \"{os.path.dirname(container_path)}\"\n mkdir -p \"{mount_point}\"\n truncate -s {int(size_gb)}G \"{container_path}\"\n echo -n \"$LUKSPASS\" | cryptsetup luksFormat \"{container_path}\" -
echo -n \"$LUKSPASS\" | cryptsetup luksOpen \"{container_path}\" {mapper_name} -
mkfs.ext4 \"/dev/mapper/{mapper_name}\"
mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\"\n {chown_cmd}\n """
if not self._execute_as_root(script, password):
return None
self.mounted_destinations.add(base_dest_path)
return mount_point
def _open_and_mount(self, base_dest_path: str, is_system: bool, password_override: Optional[str] = None) -> bool:
username = os.path.basename(base_dest_path.rstrip('/'))
key_or_pass_arg, password = "", None
if password_override:
password = password_override
key_or_pass_arg = "-"
else:
key_or_pass_arg, password = self._get_password_or_key_cmd(
base_dest_path, username)
if not key_or_pass_arg:
return False
container_path = self.get_container_path(base_dest_path)
mount_point = self.get_mount_point(base_dest_path)
mapper_name = f"pybackup_luks_{username}"
chown_cmd = self._get_chown_command(mount_point, is_system)
luks_open_cmd = f'echo -n \"$LUKSPASS\" | cryptsetup luksOpen \"{container_path}\" {mapper_name} {key_or_pass_arg}' if password else f'cryptsetup luksOpen \"{container_path}\" {mapper_name} {key_or_pass_arg}'
script = f"""
umount -l \"{mount_point}\" || true
cryptsetup luksClose {mapper_name} || true
mkdir -p \"{mount_point}\"
{luks_open_cmd}
mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\"
{chown_cmd}
"""
if self._execute_as_root(script, password):
self.add_to_lock_file(base_dest_path, mapper_name)
return True
return False
def unmount_and_reset_owner(self, base_dest_path: str, force_unmap=False):
username = os.path.basename(base_dest_path.rstrip('/'))
mapper_name = f"pybackup_luks_{username}"
if not os.path.exists(f"/dev/mapper/{mapper_name}") and not self.is_mounted(base_dest_path):
if not force_unmap:
return
self.logger.log(f"Unmounting and resetting owner for {base_dest_path}")
mount_point = self.get_mount_point(base_dest_path)
script = f"""
chown root:root \"{mount_point}\" || true
umount -l \"{mount_point}\"
cryptsetup luksClose {mapper_name}
"""
password = self.password_cache.get(username)
self._execute_as_root(script, password)
self.remove_from_lock_file(base_dest_path)
def unmount(self, base_dest_path: str):
if base_dest_path in self.mounted_destinations:
self._unmount_encrypted_backup(base_dest_path)
self.mounted_destinations.remove(base_dest_path)
if username in self.password_cache:
del self.password_cache[username]
def unmount_all(self):
self.logger.log(f"Unmounting all mounted backup destinations: {self.mounted_destinations}")
# Create a copy for safe iteration
self.logger.log(f"Unmounting all: {self.mounted_destinations}")
for path in list(self.mounted_destinations):
self.unmount(path)
self.unmount_and_reset_owner(path, force_unmap=True)
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 unmount_all_encrypted_drives(self, password: str) -> Tuple[bool, str]:
for path in list(self.mounted_destinations):
self.unmount_and_reset_owner(path, force_unmap=True)
return True, "Successfully unmounted all drives."
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}")
def _get_chown_command(self, mount_point: str, is_system: bool) -> str:
if not is_system:
try:
uid = os.getuid()
gid = os.getgid()
return f"chown {uid}:{gid} \"{mount_point}\""
except Exception as e:
self.logger.log(
f"Could not get current user UID/GID for chown: {e}")
return ""
for tool in ["cryptsetup", "losetup", "pvcreate", "vgcreate", "lvcreate", "lvextend", "resize2fs"]:
if not shutil.which(tool):
self.logger.log(f"Error: Required tool '{tool}' is not installed.")
queue.put(('error', f"Required tool '{tool}' is not installed."))
return None
pybackup_dir = os.path.join(base_dest_path, "pybackup")
user_encrypt_dir = os.path.join(pybackup_dir, "user_encrypt")
encrypted_dir = os.path.join(pybackup_dir, "encrypted")
container_path = os.path.join(user_encrypt_dir, "pybackup_lvm.img")
mount_point = encrypted_dir
base_name = os.path.basename(base_dest_path.rstrip('/'))
vg_name = f"pybackup_vg_{base_name}"
lv_name = "backup_lv"
mapper_name = f"pybackup_luks_{base_name}"
lv_path = f"/dev/{vg_name}/{lv_name}"
if not password and not key_file:
self.logger.log("No password or key file provided for encryption.")
queue.put(('error', "No password or key file provided for encryption."))
return None
if os.path.ismount(mount_point):
self.logger.log(f"Mount point {mount_point} already in use. Assuming it's correctly mounted.")
return mount_point
if os.path.exists(container_path):
auth_part = f"echo -n '{password}' | cryptsetup luksOpen {lv_path} {mapper_name} -" if password else f"cryptsetup luksOpen {lv_path} {mapper_name} --key-file {key_file}"
script = f"""
mkdir -p {user_encrypt_dir}
LOOP_DEVICE=$(losetup -f --show {container_path})
pvscan --cache
vgchange -ay {vg_name}
{auth_part}
mount /dev/mapper/{mapper_name} {mount_point}
echo "LOOP_DEVICE_PATH=$LOOP_DEVICE"
"""
if not self._execute_as_root(script):
self.logger.log("Failed to unlock existing LVM container.")
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
else:
format_auth_part = f"echo -n '{password}' | cryptsetup luksFormat {lv_path} -" if password else f"cryptsetup luksFormat {lv_path} --key-file {key_file}"
open_auth_part = f"echo -n '{password}' | cryptsetup luksOpen {lv_path} {mapper_name} -" if password else f"cryptsetup luksOpen {lv_path} {mapper_name} --key-file {key_file}"
script = f"""
mkdir -p {encrypted_dir}
mkdir -p {user_encrypt_dir}
fallocate -l {size_gb}G {container_path}
LOOP_DEVICE=$(losetup -f --show {container_path})
pvcreate $LOOP_DEVICE
vgcreate {vg_name} $LOOP_DEVICE
lvcreate -n {lv_name} -l 100%FREE {vg_name}
{format_auth_part}
{open_auth_part}
mkfs.ext4 /dev/mapper/{mapper_name}
mount /dev/mapper/{mapper_name} {mount_point}
echo "LOOP_DEVICE_PATH=$LOOP_DEVICE"
"""
if not self._execute_as_root(script):
self.logger.log("Failed to create and setup LVM-based encrypted container.")
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."))
return None
self.logger.log(f"Encrypted LVM container is ready and mounted at {mount_point}")
return mount_point
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")
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"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)
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
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
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):
self.logger.log("Encrypted LVM backup cleanup script failed.")
def _execute_as_root(self, script_content: str) -> bool:
script_path = ''
def _execute_as_root(self, script_content: str, password_for_stdin: Optional[str] = None) -> bool:
try:
with tempfile.NamedTemporaryFile(mode='w', delete=False, prefix="pybackup_script_", suffix=".sh", dir="/tmp") as tmp_script:
tmp_script.write("#!/bin/bash\n\n")
tmp_script.write("set -e\n\n")
tmp_script.write(script_content)
script_path = tmp_script.name
if password_for_stdin:
escaped_password = password_for_stdin.replace("'", "'\\\''")
script_content = f"LUKSPASS='{escaped_password}'\n{script_content}"
os.chmod(script_path, stat.S_IRWXU | stat.S_IRGRP |
stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
project_root = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..'))
runner_script_path = os.path.join(
project_root, 'core', 'privileged_script_runner.sh')
command = ['pkexec', script_path]
if not os.path.exists(runner_script_path):
self.logger.log(
f"CRITICAL: Privileged script runner not found at {runner_script_path}")
return False
command = ['pkexec', runner_script_path]
log_lines = []
for line in script_content.split('\n'):
if "LUKSPASS=" in line:
log_lines.append("LUKSPASS='[REDACTED]'")
else:
log_lines.append(line)
sanitized_script_content = "\n".join(log_lines)
sanitized_script_content = re.sub(
r"echo -n \'.*?\'", "echo -n \'[REDACTED]\'", script_content)
self.logger.log(
f"Executing privileged command via script: {script_path}")
f"Executing privileged command via runner: {runner_script_path}")
self.logger.log(
f"Script content:\n---\n{sanitized_script_content}\n---")
f"Script content to be piped:\n---\n{sanitized_script_content}\n---")
result = subprocess.run(
command, capture_output=True, text=True, check=False)
command, input=script_content, capture_output=True, text=True, check=False)
if result.returncode == 0:
self.logger.log(
f"Privileged script executed successfully. Output:\n{result.stdout}")
log_output = f"Privileged script executed successfully."
if result.stdout:
log_output += f"\nStdout:\n{result.stdout}"
if result.stderr:
log_output += f"\nStderr:\n{result.stderr}"
self.logger.log(log_output)
return True
else:
self.logger.log(
@@ -341,6 +389,3 @@ class EncryptionManager:
self.logger.log(
f"Failed to set up or execute privileged command: {e}")
return False
finally:
if script_path and os.path.exists(script_path):
os.remove(script_path)

View File

@@ -10,13 +10,13 @@ class AppConfig:
# --- Core Paths ---
BASE_DIR: Path = Path.home()
CONFIG_DIR: Path = BASE_DIR / ".config/lx_pyimage"
SETTINGS_FILE: Path = CONFIG_DIR / "settings.json"
GENERATED_EXCLUDE_LIST_PATH: Path = CONFIG_DIR / "rsync-generated-excludes.conf"
USER_EXCLUDE_LIST_PATH: Path = CONFIG_DIR / \
"rsync-user-excludes.conf" # Single file
MANUAL_EXCLUDE_LIST_PATH: Path = CONFIG_DIR / \
"rsync-manual-excludes.conf" # Single file
APP_DIR: Path = BASE_DIR / ".config/py_backup"
SETTINGS_FILE: Path = APP_DIR / "pbp_settings.json"
GENERATED_EXCLUDE_LIST_PATH: Path = APP_DIR / "rsync-generated-excludes.conf"
USER_EXCLUDE_LIST_PATH: Path = APP_DIR / "user_excludes.txt"
MANUAL_EXCLUDE_LIST_PATH: Path = APP_DIR / "manual_excludes.txt"
LOG_FILE_PATH: Path = APP_DIR / "py-backup.log"
LOCK_FILE_PATH: Path = APP_DIR / "pybackup.lock"
APP_ICONS_DIR: Path = Path(__file__).parent / "lx-icons"
# --- Application Info ---
@@ -125,8 +125,8 @@ class AppConfig:
@classmethod
def ensure_directories(cls) -> None:
"""Ensures that all required application directories exist."""
if not cls.CONFIG_DIR.exists():
cls.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
if not cls.APP_DIR.exists():
cls.APP_DIR.mkdir(parents=True, exist_ok=True)
# In the future, we can create a default settings file here
# Generate/update the final exclude list on every start
@@ -227,13 +227,14 @@ class Msg:
"warning_not_enough_space": _("WARNING: Not enough space for the backup.\nPlease free up space or choose another location."),
"warning_space_over_90_percent": _("WARNING: The storage space will be over 90% full. Backup at your own risk!"),
"ready_for_first_backup": _("Everything is ready for your first backup."),
"backup_mode": _("Backup Mode"),
"backup_mode_info": _("Backup Mode: You can start a backup here."),
"restore_mode_info": _("Restore Mode: You can start a restore here."),
"advanced_settings_title": _("Advanced Settings"),
"animation_settings_title": _("Animation Settings"),
"backup_animation_label": _("Backup/Restore Animation:"),
"calc_animation_label": _("Size Calculation Animation:"),
"advanced_settings_warning": _("WARNING: Changing these settings is recommended for experienced users only. Incorrect configurations can lead to an unreliable backup.\n\nThe backup destination is always excluded for security reasons and cannot be changed here."),
"advanced_settings_warning": _("WARNING: Changing these settings is recommended for experienced users only. Incorrect configurations can lead to an unreliable backup.\nThe backup destination is always excluded for security reasons and cannot be changed here."),
"exclude_system_folders": _("Exclude system folders"),
"in_backup": _("In Backup"),
"name": _("Name"),
@@ -255,6 +256,7 @@ class Msg:
"log": _("Log"),
"full_backup": _("Full backup"),
"incremental": _("Incremental"),
"incremental_backup": _("Incremental backup"), # New
"test_run": _("Test run"),
"start": _("Start"),
"cancel_backup": _("Cancel"),
@@ -264,12 +266,12 @@ class Msg:
"backup_finished_with_warnings": _("Backup finished with warnings. See log for details."),
"backup_failed": _("Backup failed. See log for details."),
"backup_cancelled_by_user": _("Backup was cancelled by the user."),
"accurate_size_cb_label": _("Accurate inkrem. size"),
"accurate_size_info_label": _("(Calculation may take longer)"),
"accurate_size_success": _("Accurate size calculated successfully."),
"accurate_size_failed": _("Failed to calculate size. See log for details."),
"incremental_size_cb_label": _("Inkrem. size"),
"incremental_size_info_label": _("(Calculation may take longer)"),
"incremental_size_success": _("Incremental size calculated successfully."),
"incremental_size_failed": _("Failed to calculate size. See log for details."),
"please_wait": _("Please wait, calculating..."),
"accurate_calc_cancelled": _("Calculate size cancelled."),
"incremental_calc_cancelled": _("Calculate size cancelled."),
"add_to_exclude_list": _("Add to exclude list"),
"exclude_dialog_text": _("Do you want to add a folder or a file?"),
"add_folder_button": _("Folder"),
@@ -277,6 +279,9 @@ class Msg:
"system_excludes": _("System Excludes"),
"manual_excludes": _("Manual Excludes"),
"manual_excludes_info": _("Here, manually add files or folders to be excluded from the backup. Each entry should be on a new line."),
"sudoers_info_text": _(f"To run automated backups, an administrator must create a file in /etc/sudoers.d/\n"
f"with the following content (replace 'user' with the correct username):\n"
f"user ALL=(ALL) NOPASSWD: /path/to/pybackup-cli.py"),
# Menus
"file_menu": _("File"),
@@ -345,22 +350,39 @@ class Msg:
"header_subtitle": _("Simple GUI for rsync"),
"encrypted_backup_content": _("Encrypted Backups"),
"compressed": _("Compressed"),
"compression": _("Compression"), # New
"encrypted": _("Encrypted"),
"encryption": _("Encryption"), # New
"bypass_security": _("Bypass security"),
"refresh_log": _("Refresh log"),
"comment": _("Kommentar"),
"force_full_backup": _("Always force full backup"),
"force_incremental_backup": _("Always force incremental backup (except first)"),
"force_compression": _("Always compress backup"),
"force_encryption": _("Always encrypt backup"),
"use_trash_bin": _("Papierkorb verwenden"),
"no_trash_bin": _("Kein Papierkorb (reine Synchronisierung)"),
"sync_mode_pure_sync": _("Synchronisierungsmodus: Reine Synchronisierung (Dateien werden gelöscht)"),
"sync_mode_trash_bin": _("Synchronisierungsmodus: Papierkorb verwenden (Gelöschte Dateien werden verschoben)"),
"sync_mode_no_delete": _("Synchronisierungsmodus: Keine Löschung (Dateien bleiben im Ziel)"),
"use_trash_bin": _("Archive outdated files in a trash bin"),
"no_trash_bin": _("Permanently delete outdated files (true sync)"),
"trash_bin_explanation": _("This setting only applies to User Backups, not System Backups. It controls how files that are deleted from your source (e.g., your Documents folder) are handled in the destination."),
"sync_mode_pure_sync": _("Sync Mode: Mirror. Files deleted from the source will also be permanently deleted from the backup."),
"sync_mode_trash_bin": _("Sync Mode: Archive. Files deleted from the source will be moved to a trash folder in the backup."),
"sync_mode_no_delete": _("Sync Mode: Additive. Files deleted from the source will be kept in the backup."),
"encryption_note_system_backup": _("Note: For system backups, encryption only applies to files directly within the /home directory. Folders are not automatically encrypted unless explicitly included in the backup."),
"keyfile_settings": _("Keyfile Settings"), # New
"backup_defaults_title": _("Backup Defaults"), # New
"automation_settings_title": _("Automation Settings"), # New
"create_add_key_file": _("Create/Add Key File"), # New
"key_file_not_created": _("Key file not created."), # New
"create_add_key_file": _("Create/Add Key File"), # New
"key_file_not_created": _("Key file not created."), # New
"backup_options": _("Backup Options"), # New
"hard_reset": _("Hard reset"),
"hard_reset_warning": _("This will reset the application to its initial state, as if it were opened for the first time. This can be useful if you are experiencing problems with the app. Clicking 'Delete now' will delete the '.config/py_backup' folder in your home directory without an additional dialog. The application will then automatically restart."),
"delete_now": _("Delete now"),
"full_delete_config_settings": _("Full delete config settings"),
"password_required": _("Password Required"),
"enter_password_prompt": _("Please enter the password for the encrypted backup:"),
"confirm_password_prompt": _("Confirm password:"),
"save_to_keyring": _("Save password to system keyring"),
"password_empty_error": _("Password cannot be empty."),
"passwords_do_not_match_error": _("Passwords do not match."),
"ok": _("OK"),
"unlock_backup": _("Unlock Backup"),
}

View File

@@ -0,0 +1,5 @@
#!/bin/bash
# This script executes commands passed to its standard input.
# The 'set -e' command ensures that the script will exit immediately if any command fails.
set -e
/bin/bash

View File

@@ -5,6 +5,7 @@ import os
import datetime
from queue import Queue, Empty
import shutil
import signal
from shared_libs.log_window import LogWindow
from shared_libs.logger import app_logger
@@ -51,6 +52,13 @@ class MainApplication(tk.Tk):
self.style.configure("Green.Sidebar.TButton", foreground="green")
self.style.configure("Switch2.TCheckbutton",
background="#2b3e4f", foreground="white")
self.style.map("Switch2.TCheckbutton",
background=[("active", "#2b3e4f"), ("selected",
"#2b3e4f"), ("disabled", "#2b3e4f")],
foreground=[("active", "white"), ("selected", "white"), ("disabled", "#737373")])
main_frame = ttk.Frame(self)
main_frame.grid(row=0, column=0, sticky="nsew")
self.grid_rowconfigure(0, weight=1)
@@ -73,8 +81,14 @@ class MainApplication(tk.Tk):
self.content_frame.grid_rowconfigure(6, weight=0)
self.content_frame.grid_columnconfigure(0, weight=1)
self._setup_log_window()
self.backup_manager = BackupManager(app_logger, self)
self.queue = Queue()
self._check_for_stale_mounts()
signal.signal(signal.SIGTERM, self._handle_signal)
signal.signal(signal.SIGINT, self._handle_signal)
self.image_manager = IconManager()
self.config_manager = ConfigManager(AppConfig.SETTINGS_FILE)
@@ -83,14 +97,25 @@ class MainApplication(tk.Tk):
self.navigation = Navigation(self)
self.actions = Actions(self)
self.full_backup_var = tk.BooleanVar()
self.incremental_var = tk.BooleanVar()
self.genaue_berechnung_var = tk.BooleanVar()
self.test_run_var = tk.BooleanVar()
self.compressed_var = tk.BooleanVar()
self.encrypted_var = tk.BooleanVar()
self.bypass_security_var = tk.BooleanVar()
self.refresh_log_var = tk.BooleanVar(value=True)
self.mode = "backup" # Default mode
self.backup_is_running = False
self.start_time = None
self.last_backup_was_system = True
self.next_backup_content_view = 'system'
self.calculation_thread = None
self.calculation_stop_event = None
self.source_larger_than_partition = False
self.accurate_calculation_running = False
self.incremental_calculation_running = False
self.is_first_backup = False
self.left_canvas_animation = None
@@ -147,6 +172,16 @@ 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.test_run_cb = ttk.Checkbutton(self.sidebar_buttons_frame, text=Msg.STR["test_run"],
variable=self.test_run_var, style="Switch2.TCheckbutton")
self.test_run_cb.pack(fill=tk.X, pady=(100, 10))
self.bypass_security_cb = ttk.Checkbutton(self.sidebar_buttons_frame, text=Msg.STR["bypass_security"],
variable=self.bypass_security_var, style="Switch2.TCheckbutton")
self.bypass_security_cb.pack(fill=tk.X, pady=10)
self.refresh_log_cb = ttk.Checkbutton(self.sidebar_buttons_frame, text=Msg.STR["refresh_log"],
variable=self.refresh_log_var, style="Switch2.TCheckbutton")
self.refresh_log_cb.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.grid(row=0, column=0, sticky="nsew")
@@ -218,7 +253,6 @@ class MainApplication(tk.Tk):
self.right_canvas.bind(
"<Button-1>", self.actions.on_right_canvas_click)
self._setup_log_window()
self._setup_scheduler_frame()
self._setup_settings_frame()
self._setup_backup_content_frame()
@@ -298,12 +332,13 @@ class MainApplication(tk.Tk):
self.restore_size_frame_after.grid_remove()
self._load_state_and_initialize()
self.update_backup_options_from_config()
self.protocol("WM_DELETE_WINDOW", self.on_closing)
def _load_state_and_initialize(self):
# self.log_window.clear_log()
last_mode = self.config_manager.get_setting("last_mode", "backup")
refresh_log = self.config_manager.get_setting("refresh_log", True)
self.refresh_log_var.set(refresh_log)
backup_source_path = self.config_manager.get_setting(
"backup_source_path")
@@ -336,23 +371,17 @@ class MainApplication(tk.Tk):
self.destination_total_bytes = total
self.destination_used_bytes = used
# If the destination is already mounted from a previous session,
# adopt it into the current session's state so it can be cleaned up properly.
if self.backup_manager.encryption_manager.is_mounted(backup_dest_path):
app_logger.log(
f"Adopting pre-existing mount for {backup_dest_path} into session.")
self.backup_manager.encryption_manager.mounted_destinations.add(
backup_dest_path)
if hasattr(self, 'header_frame'):
self.header_frame.refresh_status()
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)
if password:
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()
restore_src_path = self.config_manager.get_setting(
"restore_source_path")
if restore_src_path and os.path.isdir(restore_src_path):
@@ -407,7 +436,7 @@ class MainApplication(tk.Tk):
def _setup_settings_frame(self):
self.settings_frame = SettingsFrame(
self.content_frame, self.navigation, self.actions, padding=10)
self.content_frame, self.navigation, self.actions, self.backup_manager.encryption_manager, self.image_manager, self.config_manager, padding=(0, 10))
self.settings_frame.grid(row=2, column=0, sticky="nsew")
self.settings_frame.grid_remove()
@@ -418,19 +447,11 @@ class MainApplication(tk.Tk):
self.backup_content_frame.grid_remove()
def _setup_task_bar(self):
self.vollbackup_var = tk.BooleanVar()
self.inkrementell_var = tk.BooleanVar()
self.genaue_berechnung_var = tk.BooleanVar()
self.testlauf_var = tk.BooleanVar()
self.compressed_var = tk.BooleanVar()
self.encrypted_var = tk.BooleanVar()
self.bypass_security_var = tk.BooleanVar()
self.info_checkbox_frame = ttk.Frame(self.content_frame, padding=10)
self.info_checkbox_frame.grid(row=3, column=0, sticky="ew")
self.info_label = ttk.Label(
self.info_checkbox_frame, text=Msg.STR["info_text_placeholder"])
self.info_label = ttk.Label(self.info_checkbox_frame)
self._update_info_label(Msg.STR["backup_mode"]) # Set initial text
self.info_label.pack(anchor=tk.W, fill=tk.X, pady=5)
self.sync_mode_label = ttk.Label(
@@ -452,39 +473,33 @@ class MainApplication(tk.Tk):
self.time_info_frame, text="Ende: --:--:--")
self.end_time_label.pack(side=tk.LEFT, padx=5)
accurate_size_frame = ttk.Frame(self.time_info_frame)
accurate_size_frame.pack(side=tk.LEFT, padx=20)
incremental_size_frame = ttk.Frame(self.time_info_frame)
incremental_size_frame.pack(side=tk.LEFT, padx=20)
self.accurate_size_cb = ttk.Checkbutton(accurate_size_frame, text=Msg.STR["accurate_size_cb_label"],
variable=self.genaue_berechnung_var, command=self.actions.on_toggle_accurate_size_calc)
self.accurate_size_cb.pack(side=tk.LEFT, padx=5)
self.incremental_size_btn = ttk.Button(incremental_size_frame, text=Msg.STR["incremental_size_cb_label"],
command=self.actions.on_incremental_size_calc)
self.incremental_size_btn.pack(side=tk.LEFT, padx=5)
accurate_size_info_label = ttk.Label(
accurate_size_frame, text=Msg.STR["accurate_size_info_label"], foreground="gray")
accurate_size_info_label.pack(side=tk.LEFT)
incremental_size_info_label = ttk.Label(
incremental_size_frame, text=Msg.STR["incremental_size_info_label"], foreground="gray")
incremental_size_info_label.pack(side=tk.LEFT)
checkbox_frame = ttk.Frame(self.info_checkbox_frame)
checkbox_frame.pack(fill=tk.X, pady=5)
self.full_backup_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["full_backup"],
variable=self.vollbackup_var, command=lambda: self.actions.handle_backup_type_change('voll'))
variable=self.full_backup_var, command=lambda: self.actions.handle_backup_type_change('full'), style="Switch.TCheckbutton")
self.full_backup_cb.pack(side=tk.LEFT, padx=5)
self.incremental_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["incremental"],
variable=self.inkrementell_var, command=lambda: self.actions.handle_backup_type_change('inkrementell'))
variable=self.incremental_var, command=lambda: self.actions.handle_backup_type_change('incremental'), style="Switch.TCheckbutton")
self.incremental_cb.pack(side=tk.LEFT, padx=5)
self.compressed_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["compressed"],
variable=self.compressed_var, command=self.actions.handle_compression_change)
variable=self.compressed_var, command=self.actions.handle_compression_change, style="Switch.TCheckbutton")
self.compressed_cb.pack(side=tk.LEFT, padx=5)
self.encrypted_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["encrypted"],
variable=self.encrypted_var, command=self.actions.handle_encryption_change)
variable=self.encrypted_var, command=self.actions.handle_encryption_change, style="Switch.TCheckbutton")
self.encrypted_cb.pack(side=tk.LEFT, padx=5)
self.test_run_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["test_run"],
variable=self.testlauf_var)
self.test_run_cb.pack(side=tk.LEFT, padx=5)
self.bypass_security_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["bypass_security"],
variable=self.bypass_security_var)
self.bypass_security_cb.pack(side=tk.LEFT, padx=5)
self.action_frame = ttk.Frame(self.content_frame, padding=10)
self.action_frame.grid(row=6, column=0, sticky="ew")
@@ -515,11 +530,13 @@ class MainApplication(tk.Tk):
progress_container, orient="horizontal", length=100, mode="determinate")
self.task_progress.grid(row=1, column=0, sticky="ew", pady=(5, 0))
self.start_pause_button = ttk.Button(
self.start_cancel_button = ttk.Button(
self.action_frame, text=Msg.STR["start"], command=self.actions.toggle_start_cancel, state="disabled")
self.start_pause_button.grid(row=0, column=2, rowspan=2, padx=5)
self.start_cancel_button.grid(row=0, column=2, rowspan=2, padx=5)
def on_closing(self):
self.config_manager.set_setting(
"refresh_log", self.refresh_log_var.get())
self.backup_manager.encryption_manager.unmount_all()
self.config_manager.set_setting("last_mode", self.mode)
@@ -567,6 +584,10 @@ class MainApplication(tk.Tk):
except tk.TclError:
pass # App is already destroyed
def _update_info_label(self, text, color="black"):
self.info_label.config(
text=text, foreground=color, font=("Helvetica", 14))
def _process_queue(self):
try:
for _ in range(100):
@@ -580,10 +601,9 @@ class MainApplication(tk.Tk):
button_text, folder_size, mode_when_started = message
if mode_when_started != self.mode:
if calc_type == 'accurate_incremental':
if calc_type == 'incremental_incremental':
self.actions._set_ui_state(True)
self.genaue_berechnung_var.set(False)
self.accurate_calculation_running = False
self.incremental_calculation_running = False
self.animated_icon.stop("DISABLE")
else:
current_folder_name = self.left_canvas_data.get(
@@ -623,7 +643,7 @@ class MainApplication(tk.Tk):
self.drawing.update_target_projection()
if calc_type == 'accurate_incremental':
if calc_type == 'incremental_incremental':
self.source_size_bytes = folder_size
self.drawing.update_target_projection()
self.animated_icon.stop("DISABLE")
@@ -631,17 +651,16 @@ class MainApplication(tk.Tk):
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(
self.incremental_calculation_running = False
self.start_cancel_button.config(
text=Msg.STR["start"])
if status == 'success':
self.info_label.config(
text=Msg.STR["accurate_size_success"], foreground="#0078d7")
self._update_info_label(
Msg.STR["incremental_size_success"], color="#0078d7")
self.current_file_label.config(text="")
else:
self.info_label.config(
text=Msg.STR["accurate_size_failed"], foreground="#D32F2F")
self._update_info_label(
Msg.STR["incremental_size_failed"], color="#D32F2F")
self.current_file_label.config(text="")
elif isinstance(message, tuple) and len(message) == 2:
@@ -649,14 +668,14 @@ class MainApplication(tk.Tk):
if message_type == 'progress':
self.task_progress["value"] = value
self.info_label.config(text=f"Fortschritt: {value}%")
self._update_info_label(f"Fortschritt: {value}%")
elif message_type == 'file_update':
max_len = 120
if len(value) > max_len:
value = "..." + value[-max_len:]
self.current_file_label.config(text=value)
elif message_type == 'status_update':
self.info_label.config(text=value)
self._update_info_label(value)
elif message_type == 'progress_mode':
self.task_progress.config(mode=value)
if value == 'indeterminate':
@@ -664,15 +683,20 @@ class MainApplication(tk.Tk):
else:
self.task_progress.stop()
elif message_type == 'cancel_button_state':
self.start_pause_button.config(state=value)
self.start_cancel_button.config(state=value)
elif message_type == 'current_path':
self.current_backup_path = value
app_logger.log(f"Set current backup path to: {value}")
elif message_type == 'deletion_complete':
self.actions._set_ui_state(True)
self.backup_content_frame.hide_deletion_status()
self.backup_content_frame.system_backups_frame._load_backup_content()
self.backup_content_frame.user_backups_frame._load_backup_content()
if self.destination_path:
active_tab_index = self.backup_content_frame.current_view_index
self.backup_content_frame.show(
self.destination_path, initial_tab_index=active_tab_index)
elif message_type == 'error':
self.animated_icon.stop("DISABLE")
self.start_pause_button["text"] = "Start"
self.start_cancel_button["text"] = "Start"
self.backup_is_running = False
elif message_type == 'completion':
status_info = value
@@ -682,20 +706,25 @@ class MainApplication(tk.Tk):
elif status_info is None:
status = 'success'
if status in ['success', 'warning']:
if self.last_backup_was_system:
self.next_backup_content_view = 'system'
else:
self.next_backup_content_view = 'user'
if status == 'success':
self.info_label.config(
text=Msg.STR["backup_finished_successfully"])
self._update_info_label(
Msg.STR["backup_finished_successfully"])
elif status == 'warning':
self.info_label.config(
text=Msg.STR["backup_finished_with_warnings"])
self._update_info_label(
Msg.STR["backup_finished_with_warnings"])
elif status == 'error':
self.info_label.config(
text=Msg.STR["backup_failed"])
self._update_info_label(Msg.STR["backup_failed"])
elif status == 'cancelled':
pass
self._update_info_label(Msg.STR["backup_mode"])
self.animated_icon.stop("DISABLE")
self.start_pause_button["text"] = "Start"
self.start_cancel_button["text"] = "Start"
self.task_progress["value"] = 0
self.current_file_label.config(text="")
@@ -731,6 +760,37 @@ class MainApplication(tk.Tk):
def quit(self):
self.on_closing()
def _check_for_stale_mounts(self):
app_logger.log("Checking for stale mounts from previous sessions...")
try:
locks = self.backup_manager.encryption_manager._read_lock_file()
if not locks:
app_logger.log(
"No lock file found or lock file is empty. Clean state.")
return
stale_mounts_found = False
for lock in locks:
mapper_path = f"/dev/mapper/{lock['mapper_name']}"
if os.path.exists(mapper_path):
stale_mounts_found = True
app_logger.log(
f"Found stale mount: {lock['mapper_name']} for path {lock['base_path']}. Attempting to close.")
self.backup_manager.encryption_manager.unmount_and_reset_owner(
lock['base_path'], force_unmap=True)
if not stale_mounts_found:
app_logger.log("No stale mounts detected.")
if locks:
self.backup_manager.encryption_manager._write_lock_file([])
except Exception as e:
app_logger.log(f"Error during stale mount check: {e}")
def _handle_signal(self, signum, frame):
app_logger.log(f"Received signal {signum}. Cleaning up and exiting.")
self.on_closing()
def update_backup_options_from_config(self):
force_full = self.config_manager.get_setting(
"force_full_backup", False)
@@ -738,13 +798,13 @@ class MainApplication(tk.Tk):
"force_incremental_backup", False)
if force_full:
self.vollbackup_var.set(True)
self.inkrementell_var.set(False)
self.full_backup_var.set(True)
self.incremental_var.set(False)
self.full_backup_cb.config(state="disabled")
self.incremental_cb.config(state="disabled")
elif force_incremental:
self.vollbackup_var.set(False)
self.inkrementell_var.set(True)
self.full_backup_var.set(False)
self.incremental_var.set(True)
self.full_backup_cb.config(state="disabled")
self.incremental_cb.config(state="disabled")

View File

@@ -10,23 +10,35 @@ from shared_libs.logger import app_logger
from core.pbp_app_config import AppConfig
# A simple logger for the CLI that just prints to the console
class CliLogger:
def log(self, message):
print(f"[CLI] {message}")
def init_logger(self, log_method):
pass # Not needed for CLI
pass # Not needed for CLI
def main():
parser = argparse.ArgumentParser(description="Py-Backup Command-Line Interface.")
parser.add_argument("--backup-type", choices=['user', 'system'], required=True, help="Type of backup to perform.")
parser.add_argument("--destination", required=True, help="Destination directory for the backup.")
parser.add_argument("--source", help="Source directory for user backup. Required for --backup-type user.")
parser.add_argument("--mode", choices=['full', 'incremental'], default='incremental', help="Mode for system backup.")
parser.add_argument("--encrypted", action='store_true', help="Flag to indicate the backup should be encrypted.")
parser.add_argument("--key-file", help="Path to the key file for unlocking an encrypted container.")
parser.add_argument("--password", help="Password for the encrypted container (use with caution). If --key-file is not provided, this will be used.")
parser.add_argument("--compressed", action='store_true', help="Flag to indicate the backup should be compressed.")
parser = argparse.ArgumentParser(
description="Py-Backup Command-Line Interface.")
parser.add_argument(
"--backup-type", choices=['user', 'system'], required=True, help="Type of backup to perform.")
parser.add_argument("--destination", required=True,
help="Destination directory for the backup.")
parser.add_argument(
"--source", help="Source directory for user backup. Required for --backup-type user.")
parser.add_argument("--mode", choices=['full', 'incremental'],
default='incremental', help="Mode for system backup.")
parser.add_argument("--encrypted", action='store_true',
help="Flag to indicate the backup should be encrypted.")
parser.add_argument(
"--key-file", help="Path to the key file for unlocking an encrypted container.")
parser.add_argument(
"--password", help="Password for the encrypted container (use with caution). If --key-file is not provided, this will be used.")
parser.add_argument("--compressed", action='store_true',
help="Flag to indicate the backup should be compressed.")
args = parser.parse_args()
@@ -34,17 +46,19 @@ def main():
parser.error("--source is required for --backup-type 'user'.")
if args.encrypted and not (args.key_file or args.password):
parser.error("For encrypted backups, either --key-file or --password must be provided.")
parser.error(
"For encrypted backups, either --key-file or --password must be provided.")
cli_logger = CliLogger()
backup_manager = BackupManager(cli_logger)
queue = Queue() # Dummy queue for now, might be used for progress later
queue = Queue() # Dummy queue for now, might be used for progress later
source_path = "/" # Default for system backup
source_path = "/" # Default for system backup
if args.backup_type == 'user':
source_path = args.source
if not os.path.isdir(source_path):
cli_logger.log(f"Error: Source path '{source_path}' does not exist or is not a directory.")
cli_logger.log(
f"Error: Source path '{source_path}' does not exist or is not a directory.")
sys.exit(1)
# Determine password or key_file to pass
@@ -54,7 +68,8 @@ def main():
if args.key_file:
auth_key_file = args.key_file
if not os.path.exists(auth_key_file):
cli_logger.log(f"Error: Key file '{auth_key_file}' does not exist.")
cli_logger.log(
f"Error: Key file '{auth_key_file}' does not exist.")
sys.exit(1)
elif args.password:
auth_password = args.password
@@ -78,8 +93,8 @@ def main():
dest_path=args.destination,
is_system=(args.backup_type == 'system'),
is_dry_run=False,
exclude_files=None, # Excludes are handled by AppConfig.MANUAL_EXCLUDE_LIST_PATH
source_size=0, # Not accurately calculable in CLI without scanning, set to 0
exclude_files=None, # Excludes are handled by AppConfig.MANUAL_EXCLUDE_LIST_PATH
source_size=0, # Not accurately calculable in CLI without scanning, set to 0
is_compressed=args.compressed,
is_encrypted=args.encrypted,
mode=args.mode,
@@ -92,5 +107,6 @@ def main():
cli_logger.log("CLI backup process finished.")
if __name__ == "__main__":
main()
main()

View File

@@ -20,48 +20,47 @@ class Actions:
def _set_backup_type(self, backup_type: str):
if backup_type == "full":
self.app.vollbackup_var.set(True)
self.app.inkrementell_var.set(False)
self.app.full_backup_var.set(True)
self.app.incremental_var.set(False)
elif backup_type == "incremental":
self.app.vollbackup_var.set(False)
self.app.inkrementell_var.set(True)
self.app.full_backup_var.set(False)
self.app.incremental_var.set(True)
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.app.full_backup_cb.config(state='disabled')
self.app.incremental_cb.config(state='disabled')
return
else:
# Re-enable if we switch back to system backup
source_name = self.app.left_canvas_data.get('folder')
is_system_backup = (source_name == "Computer")
if not is_system_backup:
self.app.full_backup_cb.config(state='normal')
self.app.incremental_cb.config(state='normal')
else: # System backup
self.app.full_backup_cb.config(state='normal')
self.app.incremental_cb.config(state='normal')
# If controls are forced by advanced settings, do nothing
if self.app.full_backup_cb.cget('state') == 'disabled' and self.app.incremental_cb.cget('state') == 'disabled':
if self.app.config_manager.get_setting("force_full_backup", False):
self._set_backup_type("full")
self.app.full_backup_cb.config(state='disabled')
self.app.incremental_cb.config(state='disabled')
return
if self.app.config_manager.get_setting("force_incremental_backup", False):
self._set_backup_type("incremental")
self.app.full_backup_cb.config(state='disabled')
self.app.incremental_cb.config(state='disabled')
return
full_backup_exists = False
if self.app.destination_path and os.path.isdir(self.app.destination_path):
pybackup_dir = os.path.join(self.app.destination_path, "pybackup")
if not os.path.isdir(pybackup_dir):
self._set_backup_type("full")
return
if not self.app.destination_path or not os.path.isdir(self.app.destination_path):
self._set_backup_type("full")
return
is_encrypted_backup = self.app.encrypted_var.get()
is_encrypted = self.app.encrypted_var.get()
system_backups = self.app.backup_manager.list_system_backups(
self.app.destination_path, mount_if_needed=False)
profile_name = "system" if is_system_backup else source_name
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
if backup.get('is_encrypted') == is_encrypted_backup and backup.get('backup_type_base') == 'Full':
full_backup_exists = True
break
full_backup_exists = self.app.backup_manager.check_for_full_backup(
dest_path=self.app.destination_path,
source_name=profile_name,
is_encrypted=is_encrypted
)
if full_backup_exists:
self._set_backup_type("incremental")
@@ -69,39 +68,42 @@ class Actions:
self._set_backup_type("full")
def _refresh_backup_options_ui(self):
# Reset enabled/disabled state for all potentially affected controls
self.app.full_backup_cb.config(state="normal")
self.app.incremental_cb.config(state="normal")
# Reset enabled/disabled state first, but respect forced states from config
if self.app.full_backup_cb.cget('state') != 'disabled':
self.app.full_backup_cb.config(state="normal")
if self.app.incremental_cb.cget('state') != 'disabled':
self.app.incremental_cb.config(state="normal")
self.app.compressed_cb.config(state="normal")
self.app.encrypted_cb.config(state="normal")
self.app.accurate_size_cb.config(state="normal")
# Apply logic: Encryption and Compression are mutually exclusive
if self.app.encrypted_var.get():
self.app.test_run_cb.config(state="normal")
# Apply mutual exclusion rules for Option A
if self.app.compressed_var.get():
self.app.incremental_var.set(False)
self.app.full_backup_var.set(True)
self.app.incremental_cb.config(state="disabled")
self.app.encrypted_var.set(False)
self.app.encrypted_cb.config(state="disabled")
if self.app.incremental_var.get() or 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")
# Compression forces full backup
self.app.vollbackup_var.set(True)
self.app.inkrementell_var.set(False)
self.app.full_backup_cb.config(state="disabled")
self.app.incremental_cb.config(state="disabled")
self.app.accurate_size_cb.config(state="disabled")
self.app.genaue_berechnung_var.set(False)
# After setting the states, determine the final full/incremental choice
self._update_backup_type_controls()
# Set incremental_size_btn state
if self.app.mode == "backup" and self.app.incremental_var.get():
self.app.incremental_size_btn.config(state="normal")
else:
self.app.incremental_size_btn.config(state="disabled")
def handle_backup_type_change(self, changed_var_name):
if changed_var_name == 'voll':
if self.app.vollbackup_var.get():
if changed_var_name == 'full':
if self.app.full_backup_var.get():
self._set_backup_type("full")
elif changed_var_name == 'inkrementell':
if self.app.inkrementell_var.get():
elif changed_var_name == 'incremental':
if self.app.incremental_var.get():
self._set_backup_type("incremental")
self._refresh_backup_options_ui()
def handle_compression_change(self):
self._refresh_backup_options_ui()
@@ -109,23 +111,20 @@ class Actions:
def handle_encryption_change(self):
self._refresh_backup_options_ui()
def on_toggle_accurate_size_calc(self):
if not self.app.genaue_berechnung_var.get():
return
def on_incremental_size_calc(self):
if self.app.calculation_thread and self.app.calculation_thread.is_alive():
self.app.calculation_stop_event.set()
app_logger.log("Stopping previous size calculation.")
app_logger.log("Accurate incremental size calculation requested.")
self.app.accurate_calculation_running = True
app_logger.log("Incremental size calculation requested.")
self.app.incremental_calculation_running = True
self._set_ui_state(False, keep_cancel_enabled=True)
self.app.start_pause_button.config(text=Msg.STR["cancel_backup"])
self.app.start_cancel_button.config(text=Msg.STR["cancel_backup"])
self.app.drawing.reset_projection_canvases()
self.app.info_label.config(
text=Msg.STR["please_wait"], foreground="#0078d7")
self.app._update_info_label(Msg.STR["please_wait"], color="#0078d7")
self.app.task_progress.config(mode="indeterminate")
self.app.task_progress.start()
self.app.left_canvas_data.update({
@@ -140,10 +139,9 @@ class Actions:
if not folder_path or not button_text:
app_logger.log(
"Cannot start accurate calculation, source folder info missing.")
"Cannot start incremental calculation, source folder info missing.")
self._set_ui_state(True)
self.app.genaue_berechnung_var.set(False)
self.app.accurate_calculation_running = False
self.app.incremental_calculation_running = False
self.app.animated_icon.stop("DISABLE")
return
@@ -151,35 +149,37 @@ class Actions:
status = 'failure'
size = 0
try:
exclude_file_paths = []
if AppConfig.GENERATED_EXCLUDE_LIST_PATH.exists():
exclude_file_paths.append(
AppConfig.GENERATED_EXCLUDE_LIST_PATH)
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)
is_system = (button_text == "Computer")
source_name = "system" if is_system else button_text
is_encrypted = self.app.encrypted_var.get()
base_dest = self.app.destination_path
correct_parent_dir = os.path.join(base_dest, "pybackup")
dummy_dest_for_calc = os.path.join(
correct_parent_dir, "dummy_name")
exclude_files = []
if is_system:
if AppConfig.GENERATED_EXCLUDE_LIST_PATH.exists():
exclude_files.append(
AppConfig.GENERATED_EXCLUDE_LIST_PATH)
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
exclude_files.append(AppConfig.USER_EXCLUDE_LIST_PATH)
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
exclude_files.append(
AppConfig.MANUAL_EXCLUDE_LIST_PATH)
size = self.app.data_processing.get_incremental_backup_size(
size = self.app.backup_manager.estimate_incremental_size(
source_path=folder_path,
dest_path=dummy_dest_for_calc,
is_system=True,
exclude_files=exclude_file_paths
is_system=is_system,
source_name=source_name,
base_dest_path=self.app.destination_path,
is_encrypted=is_encrypted,
exclude_files=exclude_files
)
status = 'success' if size > 0 else 'failure'
status = 'success'
except Exception as e:
app_logger.log(f"Error during threaded_incremental_calc: {e}")
status = 'failure'
finally:
if self.app.accurate_calculation_running:
if self.app.incremental_calculation_running:
self.app.queue.put(
(button_text, size, self.app.mode, 'accurate_incremental', status))
(button_text, size, self.app.mode, 'incremental_incremental', status))
self.app.calculation_thread = threading.Thread(
target=threaded_incremental_calc)
@@ -187,9 +187,9 @@ class Actions:
self.app.calculation_thread.start()
def on_sidebar_button_click(self, button_text):
if self.app.backup_is_running or self.app.accurate_calculation_running:
if self.app.backup_is_running or self.app.incremental_calculation_running:
app_logger.log(
"Action blocked: Backup or accurate calculation is in progress.")
"Action blocked: Backup or incremental calculation is in progress.")
return
self.app.drawing.reset_projection_canvases()
@@ -197,8 +197,6 @@ class Actions:
self.app.navigation.toggle_mode(
self.app.mode, trigger_calculation=False)
# self.app.log_window.clear_log()
REVERSE_FOLDER_MAP = {
"Computer": "Computer",
Msg.STR["cat_documents"]: "Documents",
@@ -213,7 +211,7 @@ class Actions:
if not folder_path or not folder_path.exists():
print(
f"Folder not found for {canonical_key} (Path: {folder_path})")
self.app.start_pause_button.config(state="disabled")
self.app.start_cancel_button.config(state="disabled")
return
if self.app.mode == 'restore':
@@ -239,19 +237,18 @@ class Actions:
else:
extra_info = Msg.STR["user_restore_info"]
# Update the info label based on the current mode
if self.app.mode == 'backup':
self._update_backup_type_controls()
else:
self.app.config_manager.set_setting(
"restore_destination_path", folder_path)
self.app._update_info_label(Msg.STR["backup_mode"])
elif self.app.mode == 'restore':
self.app._update_info_label(Msg.STR["restore"])
self._start_left_canvas_calculation(
button_text, str(folder_path), icon_name, extra_info)
# 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")
self.app.start_cancel_button.config(state="disabled")
if self.app.mode == 'backup' and not self.app.destination_path:
self.app.left_canvas_data.update({
'icon': icon_name,
@@ -312,6 +309,7 @@ class Actions:
if self.app.mode == 'backup':
self._update_backup_type_controls()
self._refresh_backup_options_ui()
else:
self.app.config_manager.set_setting(
"restore_destination_path", folder_path)
@@ -341,9 +339,8 @@ class Actions:
def _update_right_canvas_info(self, path):
try:
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.backup_manager.encryption_manager.unmount_and_reset_owner(
self.app.destination_path)
self.app.destination_path = path
@@ -361,49 +358,46 @@ class Actions:
f.write(f"{backup_root_to_exclude}\n")
except IOError as e:
app_logger.log(f"Error updating exclusion list: {e}")
total, used, free = shutil.disk_usage(path)
self.app.destination_total_bytes = total
self.app.destination_used_bytes = used
size_str = f"{used / (1024**3):.2f} GB / {total / (1024**3):.2f} GB"
try:
total, used, free = shutil.disk_usage(path)
self.app.destination_total_bytes = total
self.app.destination_used_bytes = used
size_str = f"{used / (1024**3):.2f} GB / {total / (1024**3):.2f} GB"
except FileNotFoundError:
size_str = "N/A"
self.app.right_canvas_data.update({
'folder': os.path.basename(path.rstrip('/')),
'folder': os.path.basename(path.rstrip('/')) if not path.startswith(('ssh:', 'sftp:')) else path,
'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()
self.app.drawing.redraw_right_canvas()
self.app.drawing.update_target_projection()
current_source = self.app.left_canvas_data.get('folder')
if current_source:
self.on_sidebar_button_click(current_source)
self._update_backup_type_controls()
elif self.app.mode == "restore":
self.app.right_canvas_data.update({
'folder': os.path.basename(path.rstrip('/')),
'folder': os.path.basename(path.rstrip('/')) if not path.startswith(('ssh:', 'sftp:')) else path,
'path_display': path,
'size': ''
})
self.app.config_manager.set_setting(
"restore_source_path", path)
self.app.drawing.calculate_restore_folder_size()
self.app.start_pause_button.config(state="normal")
self.app.start_cancel_button.config(state="normal")
except FileNotFoundError:
with message_box_animation(self.app.calculating_animation):
MessageDialog(master=self.app, message_type="error",
with message_box_animation(self.app.animated_icon):
MessageDialog(message_type="error",
title=Msg.STR["error"], text=Msg.STR["path_not_found"].format(path=path)).show()
def reset_to_default_settings(self):
try:
AppConfig.create_default_user_excludes()
except OSError as e:
app_logger.log(f"Error creating default user exclude list: {e}")
self.app.config_manager.set_setting("backup_destination_path", None)
self.app.config_manager.set_setting("restore_source_path", None)
@@ -426,7 +420,7 @@ class Actions:
settings_frame.load_and_display_excludes()
settings_frame._load_hidden_files()
self.app.destination_path = None
self.app.start_pause_button.config(state="disabled")
self.app.start_cancel_button.config(state="disabled")
self.app.backup_left_canvas_data.clear()
self.app.backup_right_canvas_data.clear()
@@ -441,7 +435,7 @@ class Actions:
self.app.backup_content_frame.user_backups_frame._load_backup_content()
with message_box_animation(self.app.animated_icon):
MessageDialog(master=self.app, message_type="info",
MessageDialog(message_type="info",
title=Msg.STR["settings_reset_title"], text=Msg.STR["settings_reset_text"])
def _parse_size_string_to_bytes(self, size_str: str) -> int:
@@ -499,29 +493,28 @@ class Actions:
self.app.right_canvas.config(cursor="")
if enable:
self._update_backup_type_controls()
self.app.update_backup_options_from_config()
self.app.actions._update_backup_type_controls()
else:
checkboxes = [
self.app.full_backup_cb,
self.app.incremental_cb,
self.app.accurate_size_cb,
self.app.compressed_cb,
self.app.encrypted_cb,
self.app.test_run_cb,
self.app.bypass_security_cb
]
self.app.incremental_size_btn.config(state="disabled")
for cb in checkboxes:
cb.config(state="disabled")
if keep_cancel_enabled:
self.app.start_pause_button.config(state="normal")
self.app.start_cancel_button.config(state="normal")
def toggle_start_cancel(self):
if self.app.accurate_calculation_running:
app_logger.log("Accurate size calculation cancelled by user.")
self.app.accurate_calculation_running = False
self.app.genaue_berechnung_var.set(False)
if self.app.incremental_calculation_running:
app_logger.log("Incremental size calculation cancelled by user.")
self.app.incremental_calculation_running = False
self.app.animated_icon.stop("DISABLE")
if self.app.left_canvas_animation:
@@ -531,9 +524,8 @@ class Actions:
self.app.task_progress.stop()
self.app.task_progress.config(mode="determinate", value=0)
self.app.info_label.config(
text=Msg.STR["accurate_calc_cancelled"], foreground="#E8740C")
self.app.start_pause_button.config(text=Msg.STR["start"])
self.app._update_info_label(Msg.STR["incremental_calc_cancelled"], color="#E8740C")
self.app.start_cancel_button.config(text=Msg.STR["start"])
self._set_ui_state(True)
return
@@ -549,14 +541,12 @@ class Actions:
delete_path)
app_logger.log(
Msg.STR["backup_cancelled_and_deleted_msg"])
self.app.info_label.config(
text=Msg.STR["backup_cancelled_and_deleted_msg"])
self.app._update_info_label(Msg.STR["backup_cancelled_and_deleted_msg"])
else:
self.app.backup_manager.cancel_backup()
app_logger.log(
"Backup cancelled, but directory could not be deleted (path unknown).")
self.app.info_label.config(
text="Backup cancelled, but directory could not be deleted (path unknown).")
self.app._update_info_label("Backup cancelled, but directory could not be deleted (path unknown).")
else:
self.app.backup_manager.cancel_backup()
if delete_path:
@@ -567,27 +557,24 @@ class Actions:
shutil.rmtree(delete_path)
app_logger.log(
Msg.STR["backup_cancelled_and_deleted_msg"])
self.app.info_label.config(
text=Msg.STR["backup_cancelled_and_deleted_msg"])
self.app._update_info_label(Msg.STR["backup_cancelled_and_deleted_msg"])
except Exception as e:
app_logger.log(f"Error deleting backup directory: {e}")
self.app.info_label.config(
text=f"Error deleting backup directory: {e}")
self.app._update_info_label(f"Error deleting backup directory: {e}")
else:
app_logger.log(
"Backup cancelled, but no path found to delete.")
self.app.info_label.config(
text="Backup cancelled, but no path found to delete.")
self.app._update_info_label("Backup cancelled, but no path found to delete.")
if hasattr(self.app, 'current_backup_path'):
self.app.current_backup_path = None
self.app.backup_is_running = False
self.app.start_pause_button["text"] = Msg.STR["start"]
self.app.start_cancel_button["text"] = Msg.STR["start"]
self._set_ui_state(True)
else:
if self.app.start_pause_button['state'] == 'disabled':
if self.app.start_cancel_button['state'] == 'disabled':
return
self.app.backup_is_running = True
@@ -597,13 +584,12 @@ class Actions:
self.app.start_time_label.config(text=f"Start: {start_str}")
self.app.end_time_label.config(text="Ende: --:--:--")
self.app.duration_label.config(text="Dauer: --:--:--")
self.app.info_label.config(text="Backup wird vorbereitet...")
self.app._update_info_label("Backup wird vorbereitet...")
self.app._update_duration()
self.app.start_pause_button["text"] = Msg.STR["cancel_backup"]
self.app.start_cancel_button["text"] = Msg.STR["cancel_backup"]
self.app.update_idletasks()
# self.app.log_window.clear_log()
self._set_ui_state(False, allow_log_and_backup_toggle=True)
self.app.animated_icon.start()
@@ -616,59 +602,34 @@ class Actions:
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.app.start_cancel_button["text"] = Msg.STR["start"]
self._set_ui_state(True)
return
if source_folder == "Computer":
mode = "full" if self.app.vollbackup_var.get() else "incremental"
self.app.last_backup_was_system = True
mode = "full" if self.app.full_backup_var.get() else "incremental"
self._start_system_backup(mode, source_size_bytes)
else:
self.app.last_backup_was_system = False
self._start_user_backup()
else: # restore mode
# Restore logic would go here
pass
def _start_system_backup(self, mode, source_size_bytes):
base_dest = self.app.destination_path
if not base_dest:
MessageDialog(master=self.app, message_type="error",
MessageDialog(message_type="error",
title=Msg.STR["error"], text=Msg.STR["err_no_dest_folder"])
return
if base_dest.startswith("/home"):
with message_box_animation(self.app.animated_icon):
MessageDialog(master=self.app, message_type="error",
MessageDialog(message_type="error",
title=Msg.STR["error"], text=Msg.STR["system_backup_in_home_error"])
return
is_encrypted = self.app.encrypted_var.get()
password = None
if is_encrypted:
username = os.path.basename(base_dest.rstrip('/'))
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.")
self.app.backup_is_running = False
self.app.start_pause_button["text"] = Msg.STR["start"]
self._set_ui_state(True)
return
try:
locale.setlocale(locale.LC_TIME, 'de_DE.UTF-8')
except locale.Error:
app_logger.log(
"Could not set locale to de_DE.UTF-8. Using default.")
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}_system_{mode}"
# 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)
@@ -680,14 +641,15 @@ class Actions:
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
exclude_file_paths.append(AppConfig.MANUAL_EXCLUDE_LIST_PATH)
is_dry_run = self.app.testlauf_var.get()
is_dry_run = self.app.test_run_var.get()
is_compressed = self.app.compressed_var.get()
self.app.backup_manager.start_backup(
queue=self.app.queue,
source_path="/",
dest_path=final_dest,
dest_path=base_dest,
is_system=True,
source_name="system",
is_dry_run=is_dry_run,
exclude_files=exclude_file_paths,
source_size=source_size_bytes,
@@ -702,39 +664,18 @@ class Actions:
source_size_bytes = self.app.left_canvas_data.get('total_bytes', 0)
if not base_dest or not source_path:
MessageDialog(master=self.app, message_type="error",
MessageDialog(message_type="error",
title=Msg.STR["error"], text=Msg.STR["err_no_dest_folder"])
self.app.backup_is_running = False
self.app.start_pause_button["text"] = Msg.STR["start"]
self.app.start_cancel_button["text"] = Msg.STR["start"]
self._set_ui_state(True)
return
is_encrypted = self.app.encrypted_var.get()
password = None
if is_encrypted:
username = os.path.basename(base_dest.rstrip('/'))
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.")
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"
mode = "full" if self.app.full_backup_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}_{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_dry_run = self.app.test_run_var.get()
is_compressed = self.app.compressed_var.get()
use_trash_bin = self.app.config_manager.get_setting(
"use_trash_bin", False)
@@ -744,8 +685,9 @@ class Actions:
self.app.backup_manager.start_backup(
queue=self.app.queue,
source_path=source_path,
dest_path=final_dest,
dest_path=base_dest,
is_system=False,
source_name=source_name,
is_dry_run=is_dry_run,
exclude_files=None,
source_size=source_size_bytes,
@@ -754,11 +696,3 @@ class Actions:
mode=mode,
use_trash_bin=use_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

@@ -6,8 +6,7 @@ from pathlib import Path
from core.pbp_app_config import AppConfig, Msg
from shared_libs.animated_icon import AnimatedIcon
from pyimage_ui.shared_logic import enforce_backup_type_exclusivity
from shared_libs.message import MessageDialog
from pyimage_ui.password_dialog import PasswordDialog
from shared_libs.message import MessageDialog, PasswordDialog
class AdvancedSettingsFrame(ttk.Frame):
@@ -19,25 +18,21 @@ class AdvancedSettingsFrame(ttk.Frame):
self.app_instance = app_instance
self.current_view_index = 0
self.info_label = ttk.Label(
self, text=Msg.STR["advanced_settings_warning"], wraplength=780, justify="left")
self.info_label.pack(pady=10, fill=tk.X, padx=10)
nav_frame = ttk.Frame(self)
nav_frame.pack(fill=tk.X, padx=10, pady=5)
nav_frame.pack(fill=tk.X, padx=10, pady=(0, 5))
ttk.Separator(nav_frame, orient=tk.HORIZONTAL).pack(
fill=tk.X, pady=(0, 15))
top_nav_frame = ttk.Frame(nav_frame)
top_nav_frame.pack(side=tk.LEFT)
self.nav_buttons_defs = [
(Msg.STR["system_excludes"], lambda: self._switch_view(0)),
(Msg.STR["manual_excludes"], lambda: self._switch_view(1)),
# New button for Keyfile/Automation
(Msg.STR["keyfile_settings"], lambda: self._switch_view(2)),
(Msg.STR["animation_settings_title"],
lambda: self._switch_view(3)), # Animation settings
lambda: self._switch_view(3)),
(Msg.STR["backup_defaults_title"],
lambda: self._switch_view(4)), # Backup Defaults
lambda: self._switch_view(4)),
]
self.nav_buttons = []
@@ -78,6 +73,10 @@ class AdvancedSettingsFrame(ttk.Frame):
self.tree.tag_configure("backup_dest_exclude", foreground="gray")
self.tree.bind("<Button-1>", self._toggle_include_status)
self.info_label = ttk.Label(
self, text=Msg.STR["advanced_settings_warning"], wraplength=780, justify="left")
self.info_label.pack(pady=10, fill=tk.X, padx=10)
self.manual_excludes_frame = ttk.LabelFrame(
view_container, text=Msg.STR["manual_excludes"], padding=10)
@@ -122,24 +121,44 @@ class AdvancedSettingsFrame(ttk.Frame):
self.force_incremental_var = tk.BooleanVar()
self.force_compression_var = tk.BooleanVar()
self.force_encryption_var = tk.BooleanVar()
self.use_trash_bin_var = tk.BooleanVar()
self.no_trash_bin_var = tk.BooleanVar()
ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_full_backup"], variable=self.force_full_var, command=lambda: enforce_backup_type_exclusivity(
self.force_full_var, self.force_incremental_var, self.force_full_var.get())).pack(anchor=tk.W)
ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_incremental_backup"], variable=self.force_incremental_var, command=lambda: enforce_backup_type_exclusivity(
self.force_incremental_var, self.force_full_var, self.force_incremental_var.get())).pack(anchor=tk.W)
ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_compression"],
variable=self.force_compression_var).pack(anchor=tk.W)
ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_encryption"],
variable=self.force_encryption_var).pack(anchor=tk.W)
self.full_backup_checkbutton = ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_full_backup"], variable=self.force_full_var, command=lambda: enforce_backup_type_exclusivity(
self.force_full_var, self.force_incremental_var, self.force_full_var.get()), style="Switch.TCheckbutton")
self.full_backup_checkbutton.pack(anchor=tk.W)
ttk.Separator(self.backup_defaults_frame, orient=tk.HORIZONTAL).pack(
fill=tk.X, pady=5)
self.incremental_checkbutton = ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_incremental_backup"], variable=self.force_incremental_var, command=lambda: enforce_backup_type_exclusivity(
self.force_incremental_var, self.force_full_var, self.force_incremental_var.get()), style="Switch.TCheckbutton")
self.incremental_checkbutton.pack(anchor=tk.W)
self.compression_checkbutton = ttk.Checkbutton(
self.backup_defaults_frame, text=Msg.STR["force_compression"], variable=self.force_compression_var, command=self._on_compression_toggle, style="Switch.TCheckbutton")
self.compression_checkbutton.pack(anchor=tk.W)
self.encryption_checkbutton = ttk.Checkbutton(
self.backup_defaults_frame, text=Msg.STR["force_encryption"], variable=self.force_encryption_var, style="Switch.TCheckbutton")
self.encryption_checkbutton.pack(anchor=tk.W)
ttk.Separator(self.backup_defaults_frame,
orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
trash_info_label = ttk.Label(
self.backup_defaults_frame, text=Msg.STR["trash_bin_explanation"], wraplength=750, justify="left")
trash_info_label.pack(anchor=tk.W, pady=5)
ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["use_trash_bin"], variable=self.use_trash_bin_var, command=lambda: self._handle_trash_checkbox_click(
self.use_trash_bin_var, self.no_trash_bin_var), style="Switch.TCheckbutton").pack(anchor=tk.W)
ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["no_trash_bin"], variable=self.no_trash_bin_var, command=lambda: self._handle_trash_checkbox_click(
self.no_trash_bin_var, self.use_trash_bin_var), style="Switch.TCheckbutton").pack(anchor=tk.W)
ttk.Separator(self.backup_defaults_frame,
orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
encryption_note = ttk.Label(
self.backup_defaults_frame, text=Msg.STR["encryption_note_system_backup"], wraplength=750, justify="left")
encryption_note.pack(anchor=tk.W, pady=5)
# --- Keyfile/Automation Settings ---
self.keyfile_settings_frame = ttk.LabelFrame(
view_container, text=Msg.STR["automation_settings_title"], padding=10)
@@ -153,10 +172,7 @@ class AdvancedSettingsFrame(ttk.Frame):
self.keyfile_settings_frame, textvariable=self.key_file_status_var, foreground="gray")
key_file_status_label.grid(row=0, column=1, padx=5, pady=5, sticky="w")
sudoers_info_text = (f"To run automated backups, an administrator must create a file in /etc/sudoers.d/\n"
f"with the following content (replace 'punix' with the correct username):\n"
# Path needs to be updated
f"punix ALL=(ALL) NOPASSWD: /path/to/pybackup-cli.py")
sudoers_info_text = Msg.STR["sudoers_info_text"]
sudoers_info_label = ttk.Label(
self.keyfile_settings_frame, text=sudoers_info_text, justify="left")
@@ -165,7 +181,6 @@ class AdvancedSettingsFrame(ttk.Frame):
self.keyfile_settings_frame.columnconfigure(1, weight=1)
# --- Action Buttons ---
button_frame = ttk.Frame(self)
button_frame.pack(pady=10)
@@ -174,16 +189,10 @@ class AdvancedSettingsFrame(ttk.Frame):
ttk.Button(button_frame, text=Msg.STR["cancel"], command=self._cancel_changes).pack(
side=tk.LEFT, padx=5)
# Initial packing of frames (all hidden except the first one by _switch_view)
# Initially packed, then hidden by _switch_view
self.tree_frame.pack(fill=tk.BOTH, expand=True)
# Initially packed, then hidden by _switch_view
self.manual_excludes_frame.pack(fill=tk.BOTH, expand=True)
# Initially packed, then hidden by _switch_view
self.keyfile_settings_frame.pack(fill=tk.BOTH, expand=True)
# Initially packed, then hidden by _switch_view
self.animation_settings_frame.pack(fill=tk.BOTH, expand=True)
# Initially packed, then hidden by _switch_view
self.backup_defaults_frame.pack(fill=tk.BOTH, expand=True)
self._load_system_folders()
@@ -194,9 +203,42 @@ class AdvancedSettingsFrame(ttk.Frame):
self._switch_view(self.current_view_index)
def _on_compression_toggle(self):
if self.force_compression_var.get():
self.force_incremental_var.set(False)
self.incremental_checkbutton.config(state="disabled")
self.force_encryption_var.set(False)
self.encryption_checkbutton.config(state="disabled")
else:
self.incremental_checkbutton.config(state="normal")
self.encryption_checkbutton.config(state="normal")
def _handle_trash_checkbox_click(self, changed_var, other_var):
enforce_backup_type_exclusivity(
changed_var, other_var, changed_var.get())
self._on_trash_setting_change()
def _on_trash_setting_change(self):
use_trash = self.use_trash_bin_var.get()
no_trash = self.no_trash_bin_var.get()
if no_trash:
self.app_instance.sync_mode_label.config(
text=Msg.STR["sync_mode_pure_sync"], foreground="red")
elif use_trash:
self.app_instance.sync_mode_label.config(
text=Msg.STR["sync_mode_trash_bin"], foreground="orange")
else:
self.app_instance.sync_mode_label.config(
text=Msg.STR["sync_mode_no_delete"], foreground="green")
self.config_manager.set_setting("use_trash_bin", use_trash)
self.config_manager.set_setting("no_trash_bin", no_trash)
def _create_key_file(self):
if not self.app_instance.destination_path:
MessageDialog(self, message_type="error", title="Error",
MessageDialog(message_type="error", title="Error",
text="Please select a backup destination first.")
return
@@ -204,26 +246,25 @@ class AdvancedSettingsFrame(ttk.Frame):
self.app_instance.destination_path, "pybackup")
container_path = os.path.join(pybackup_dir, "pybackup_encrypted.luks")
if not os.path.exists(container_path):
MessageDialog(self, message_type="error", title="Error",
MessageDialog(message_type="error", title="Error",
text="No encrypted container found at the destination.")
return
# Prompt for the existing password to authorize adding a new key
password_dialog = PasswordDialog(
self, title="Enter Existing Password", confirm=False)
self, title="Enter Existing Password", confirm=False, translations=Msg.STR)
password, _ = password_dialog.get_password()
if not password:
return # User cancelled
return
key_file_path = self.app_instance.backup_manager.encryption_manager.create_and_add_key_file(
self.app_instance.destination_path, password)
if key_file_path:
MessageDialog(self, message_type="info", title="Success",
MessageDialog(message_type="info", title="Success",
text=f"Key file created and added successfully!\nPath: {key_file_path}")
else:
MessageDialog(self, message_type="error", title="Error",
MessageDialog(message_type="error", title="Error",
text="Failed to create or add key file. See log for details.")
self._update_key_file_status()
@@ -246,14 +287,12 @@ class AdvancedSettingsFrame(ttk.Frame):
self.current_view_index = index
self.update_nav_buttons(index)
# Hide all frames first
self.tree_frame.pack_forget()
self.manual_excludes_frame.pack_forget()
self.keyfile_settings_frame.pack_forget()
self.animation_settings_frame.pack_forget()
self.backup_defaults_frame.pack_forget()
# Show the selected frame and update info label
if index == 0:
self.tree_frame.pack(fill=tk.BOTH, expand=True)
self.info_label.config(text=Msg.STR["advanced_settings_warning"])
@@ -262,14 +301,13 @@ class AdvancedSettingsFrame(ttk.Frame):
self.info_label.config(text=Msg.STR["manual_excludes_info"])
elif index == 2:
self.keyfile_settings_frame.pack(fill=tk.BOTH, expand=True)
# Use automation title for info
self.info_label.config(text=Msg.STR["automation_settings_title"])
self.info_label.config(text="")
elif index == 3:
self.animation_settings_frame.pack(fill=tk.BOTH, expand=True)
self.info_label.config(text=Msg.STR["animation_settings_title"])
self.info_label.config(text="")
elif index == 4:
self.backup_defaults_frame.pack(fill=tk.BOTH, expand=True)
self.info_label.config(text=Msg.STR["backup_defaults_title"])
self.info_label.config(text="")
def update_nav_buttons(self, active_index):
for i, button in enumerate(self.nav_buttons):
@@ -318,6 +356,11 @@ class AdvancedSettingsFrame(ttk.Frame):
self.config_manager.get_setting("force_compression", False))
self.force_encryption_var.set(
self.config_manager.get_setting("force_encryption", False))
self.use_trash_bin_var.set(
self.config_manager.get_setting("use_trash_bin", False))
self.no_trash_bin_var.set(
self.config_manager.get_setting("no_trash_bin", False))
self._on_compression_toggle()
def _load_animation_settings(self):
backup_anim = self.config_manager.get_setting(
@@ -409,6 +452,10 @@ class AdvancedSettingsFrame(ttk.Frame):
"force_compression", self.force_compression_var.get())
self.config_manager.set_setting(
"force_encryption", self.force_encryption_var.get())
self.config_manager.set_setting(
"use_trash_bin", self.use_trash_bin_var.get())
self.config_manager.set_setting(
"no_trash_bin", self.no_trash_bin_var.get())
if self.app_instance:
self.app_instance.update_backup_options_from_config()
@@ -424,8 +471,8 @@ class AdvancedSettingsFrame(ttk.Frame):
self.app_instance.animated_icon = AnimatedIcon(
self.app_instance.action_frame, width=20, height=20, use_pillow=True, bg=bg_color, animation_type=initial_animation_type)
self.app_instance.animated_icon.pack(
side=tk.LEFT, padx=5, before=self.app_instance.task_progress)
self.app_instance.animated_icon.grid(
row=0, column=0, rowspan=2, padx=5)
self.app_instance.animated_icon.stop("DISABLE")
self.app_instance.animated_icon.animation_type = backup_animation_type
@@ -472,7 +519,6 @@ class AdvancedSettingsFrame(ttk.Frame):
for item in self.manual_excludes_listbox.get(0, tk.END):
f.write(f"{item}\n")
# Instead of destroying the Toplevel, hide this frame and show main settings
self.pack_forget()
if self.show_main_settings_callback:
self.show_main_settings_callback()
@@ -484,7 +530,6 @@ class AdvancedSettingsFrame(ttk.Frame):
current_source)
def _cancel_changes(self):
# Hide this frame and show main settings without applying changes
self.pack_forget()
if self.show_main_settings_callback:
self.show_main_settings_callback()

View File

@@ -6,7 +6,6 @@ from core.pbp_app_config import Msg
from pyimage_ui.system_backup_content_frame import SystemBackupContentFrame
from pyimage_ui.user_backup_content_frame import UserBackupContentFrame
from shared_libs.logger import app_logger
from shared_libs.message import MessageDialog
class BackupContentFrame(ttk.Frame):
@@ -119,8 +118,6 @@ class BackupContentFrame(ttk.Frame):
def _switch_view(self, index):
self.current_view_index = index
config_key = "last_encrypted_backup_content_view" if self.viewing_encrypted else "last_backup_content_view"
self.app.config_manager.set_setting(config_key, index)
self.update_nav_buttons(index)
if index == 0:
@@ -141,7 +138,7 @@ class BackupContentFrame(ttk.Frame):
button.configure(style="Gray.Toolbutton")
self.nav_progress_bars[i].pack_forget()
def show(self, backup_path):
def show(self, backup_path, initial_tab_index=0):
app_logger.log(
f"BackupContentFrame: show called with path {backup_path}")
self.grid(row=2, column=0, sticky="nsew")
@@ -151,21 +148,7 @@ class BackupContentFrame(ttk.Frame):
# 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()
self.viewing_encrypted = is_encrypted # Set this flag for remembering the view
pybackup_dir = os.path.join(backup_path, "pybackup")
@@ -177,7 +160,8 @@ class BackupContentFrame(ttk.Frame):
self.user_backups_frame.show(backup_path, [])
return
all_backups = self.backup_manager.list_all_backups(backup_path)
all_backups = self.backup_manager.list_all_backups(
backup_path, mount_if_needed=True)
if all_backups:
system_backups, user_backups = all_backups
self.system_backups_frame.show(backup_path, system_backups)
@@ -187,9 +171,8 @@ 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)
# Use the passed index to switch to the correct view
self.after(10, lambda: self._switch_view(initial_tab_index))
def hide(self):
self.grid_remove()

View File

@@ -1,6 +1,7 @@
import tkinter as tk
from tkinter import ttk
class CommentEditorDialog(tk.Toplevel):
def __init__(self, master, info_file_path, backup_manager):
super().__init__(master)
@@ -13,17 +14,20 @@ class CommentEditorDialog(tk.Toplevel):
main_frame = ttk.Frame(self, padding=10)
main_frame.pack(fill=tk.BOTH, expand=True)
self.text_widget = tk.Text(main_frame, wrap="word", height=10, width=40)
self.text_widget = tk.Text(
main_frame, wrap="word", height=10, width=40)
self.text_widget.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X)
ttk.Button(button_frame, text="Speichern & Schließen", command=self._save_and_close).pack(side=tk.RIGHT)
ttk.Button(button_frame, text="Abbrechen", command=self.destroy).pack(side=tk.RIGHT, padx=5)
ttk.Button(button_frame, text="Speichern & Schließen",
command=self._save_and_close).pack(side=tk.RIGHT)
ttk.Button(button_frame, text="Abbrechen",
command=self.destroy).pack(side=tk.RIGHT, padx=5)
self._load_comment()
self.transient(master)
self.grab_set()
self.wait_window(self)

View File

@@ -166,7 +166,7 @@ class Drawing:
if self.app.right_calculation_thread and self.app.right_calculation_thread.is_alive():
self.app.right_calculation_stop_event.set()
self.app.start_pause_button.config(state="disabled")
self.app.start_cancel_button.config(state="disabled")
path_to_calculate = self.app.right_canvas_data.get('path_display')
if path_to_calculate and os.path.isdir(path_to_calculate):
self.app.right_canvas_data['calculating'] = True
@@ -213,7 +213,7 @@ class Drawing:
self.redraw_right_canvas_restore()
if not self.app.left_canvas_data.get('calculating', False):
self.app.start_pause_button.config(state="normal")
self.app.start_cancel_button.config(state="normal")
if not stop_event.is_set():
self.app.after(0, update_ui)
@@ -240,46 +240,53 @@ class Drawing:
required_space *= 2 # Double the space for compression process
projected_total_used = self.app.destination_used_bytes + required_space
if self.app.destination_total_bytes > 0:
projected_total_percentage = projected_total_used / self.app.destination_total_bytes
projected_total_percentage = projected_total_used / \
self.app.destination_total_bytes
else:
projected_total_percentage = 0
info_font = (AppConfig.UI_CONFIG["font_family"], 10, "bold")
info_messages = []
# First, check for critical space issues
if projected_total_used > self.app.destination_total_bytes or projected_total_percentage >= 0.95:
self.app.start_pause_button.config(state="disabled")
canvas.create_rectangle(0, 0, canvas_width, canvas.winfo_height(), fill="#D32F2F", outline="") # Red bar
self.app.start_cancel_button.config(state="disabled")
canvas.create_rectangle(0, 0, canvas_width, canvas.winfo_height(
), fill="#D32F2F", outline="") # Red bar
info_messages.append(Msg.STR["warning_not_enough_space"])
elif projected_total_percentage >= 0.90:
self.app.start_pause_button.config(state="normal")
canvas.create_rectangle(0, 0, canvas_width, canvas.winfo_height(), fill="#E8740C", outline="") # Orange bar
self.app.start_cancel_button.config(state="normal")
canvas.create_rectangle(0, 0, canvas_width, canvas.winfo_height(
), fill="#E8740C", outline="") # Orange bar
info_messages.append(Msg.STR["warning_space_over_90_percent"])
else:
# Only enable the button if the source is not larger than the partition itself
if not self.app.source_larger_than_partition:
self.app.start_pause_button.config(state="normal")
self.app.start_cancel_button.config(state="normal")
else:
self.app.start_pause_button.config(state="disabled")
self.app.start_cancel_button.config(state="disabled")
used_percentage = self.app.destination_used_bytes / self.app.destination_total_bytes
used_percentage = self.app.destination_used_bytes / \
self.app.destination_total_bytes
used_width = canvas_width * used_percentage
canvas.create_rectangle(0, 0, used_width, canvas.winfo_height(), fill="#0078d7", outline="")
canvas.create_rectangle(
0, 0, used_width, canvas.winfo_height(), fill="#0078d7", outline="")
# Draw the projected part only if there is space
projected_percentage = self.app.source_size_bytes / self.app.destination_total_bytes
projected_percentage = self.app.source_size_bytes / \
self.app.destination_total_bytes
projected_width = canvas_width * projected_percentage
canvas.create_rectangle(used_width, 0, used_width + projected_width, canvas.winfo_height(), fill="#50E6FF", outline="")
canvas.create_rectangle(used_width, 0, used_width + projected_width,
canvas.winfo_height(), fill="#50E6FF", outline="")
# Add other informational messages if no critical warnings are present
if not info_messages:
if self.app.source_larger_than_partition:
info_messages.append(Msg.STR["warning_source_larger_than_partition"])
info_messages.append(
Msg.STR["warning_source_larger_than_partition"])
elif self.app.is_first_backup:
info_messages.append(Msg.STR["ready_for_first_backup"])
elif self.app.mode == "backup":
@@ -287,7 +294,8 @@ class Drawing:
else:
info_messages.append(Msg.STR["restore_mode_info"])
self.app.info_label.config(text="\n".join(info_messages), font=info_font)
self.app.info_label.config(
text="\n".join(info_messages), font=("Helvetica", 14))
self.app.target_size_label.config(
text=f"{projected_total_used / (1024**3):.2f} GB / {self.app.destination_total_bytes / (1024**3):.2f} GB")

View File

@@ -1,6 +1,5 @@
import tkinter as tk
from tkinter import ttk
from shared_libs.message import MessageDialog
import keyring
@@ -20,9 +19,10 @@ class EncryptionFrame(ttk.Frame):
self.keyring_status_label = ttk.Label(self, text="")
self.keyring_status_label.grid(
row=1, column=0, sticky="ew", padx=10, pady=5)
self.keyring_usage_label = ttk.Label(self, text="")
self.keyring_usage_label.grid(row=4, column=0, sticky="ew", padx=10, pady=5)
self.keyring_usage_label.grid(
row=4, column=0, sticky="ew", padx=10, pady=5)
self.check_keyring_availability()
@@ -51,7 +51,8 @@ class EncryptionFrame(ttk.Frame):
clear_password_button.grid(row=2, column=1, padx=5, pady=5, sticky="w")
self.status_message_label = ttk.Label(self, text="", foreground="blue")
self.status_message_label.grid(row=3, column=0, sticky="ew", padx=10, pady=5)
self.status_message_label.grid(
row=3, column=0, sticky="ew", padx=10, pady=5)
def set_context(self, username):
self.username = username
@@ -75,39 +76,47 @@ class EncryptionFrame(ttk.Frame):
def set_session_password(self):
password = self.password_entry.get()
if not password:
self.status_message_label.config(text="Password cannot be empty.", foreground="red")
self.status_message_label.config(
text="Password cannot be empty.", foreground="red")
return
self.encryption_manager.set_session_password(password, self.save_to_keyring_var.get())
self.encryption_manager.set_session_password(
password, self.save_to_keyring_var.get())
if self.save_to_keyring_var.get():
if not self.username:
self.status_message_label.config(text="Please select a backup destination first.", foreground="orange")
self.status_message_label.config(
text="Please select a backup destination first.", foreground="orange")
return
if self.encryption_manager.set_password_in_keyring(self.username, password):
self.status_message_label.config(text="Password set for this session and saved to keyring.", foreground="green")
self.status_message_label.config(
text="Password set for this session and saved to keyring.", foreground="green")
self.update_keyring_status()
else:
self.status_message_label.config(text="Password set for this session, but failed to save to keyring.", foreground="orange")
self.status_message_label.config(
text="Password set for this session, but failed to save to keyring.", foreground="orange")
else:
self.status_message_label.config(text="Password set for this session.", foreground="green")
self.status_message_label.config(
text="Password set for this session.", foreground="green")
def clear_session_password(self):
self.encryption_manager.clear_session_password()
self.password_entry.delete(0, tk.END)
self.status_message_label.config(text="Session password cleared.", foreground="green")
self.status_message_label.config(
text="Session password cleared.", foreground="green")
if self.username:
self.encryption_manager.delete_password_from_keyring(self.username)
self.update_keyring_status()
def update_keyring_status(self):
if not self.username:
self.keyring_usage_label.config(text="Select a backup destination to see keyring status.", foreground="blue")
self.keyring_usage_label.config(
text="Select a backup destination to see keyring status.", foreground="blue")
return
if self.encryption_manager.get_password_from_keyring(self.username):
self.keyring_usage_label.config(text=f'Password for "{self.username}" is stored in the keyring.', foreground="green")
self.keyring_usage_label.config(
text=f'Password for "{self.username}" is stored in the keyring.', foreground="green")
else:
self.keyring_usage_label.config(text=f'No password for "{self.username}" found in the keyring.', foreground="orange")
self.keyring_usage_label.config(
text=f'No password for "{self.username}" found in the keyring.', foreground="orange")

View File

@@ -2,7 +2,8 @@ import tkinter as tk
import os
from core.pbp_app_config import Msg
from shared_libs.common_tools import IconManager
from shared_libs.logger import app_logger
class HeaderFrame(tk.Frame):
def __init__(self, container, image_manager, encryption_manager, app, **kwargs):
@@ -62,20 +63,32 @@ class HeaderFrame(tk.Frame):
font=("Helvetica", 10, "bold"),
bg="#455A64",
)
self.keyring_status_label.grid(row=0, column=0, sticky="ne", padx=(10, 10), pady=(10, 0))
self.keyring_status_label.grid(
row=0, column=0, sticky="ne", padx=(10, 10), pady=(10, 0))
self.refresh_status()
def refresh_status(self):
"""Checks the keyring status based on the current destination and updates the label."""
app_logger.log("HeaderFrame: Refreshing status...")
dest_path = self.app.destination_path
app_logger.log(f"HeaderFrame: Destination path is '{dest_path}'")
if not dest_path or not self.encryption_manager.is_encrypted(dest_path):
self.keyring_status_label.config(text="") # Clear status if not encrypted
app_logger.log(
"HeaderFrame: No destination path or not encrypted. Clearing status.")
# Clear status if not encrypted
self.keyring_status_label.config(text="")
return
app_logger.log("HeaderFrame: Destination is encrypted.")
username = os.path.basename(dest_path.rstrip('/'))
app_logger.log(f"HeaderFrame: Username is '{username}'")
if self.encryption_manager.is_mounted(dest_path):
is_mounted = self.encryption_manager.is_mounted(dest_path)
app_logger.log(f"HeaderFrame: Is mounted? {is_mounted}")
if is_mounted:
status_text = "Key: In Use"
auth_method = getattr(self.encryption_manager, 'auth_method', None)
if auth_method == 'keyring':
@@ -86,18 +99,27 @@ class HeaderFrame(tk.Frame):
text=status_text,
fg="#2E8B57" # SeaGreen
)
elif self.encryption_manager.is_key_in_keyring(username):
self.keyring_status_label.config(
text="Key: Available (Keyring)",
fg="#FFD700" # Gold
)
elif os.path.exists(self.encryption_manager.get_key_file_path(dest_path)):
self.keyring_status_label.config(
text="Key: Available (Keyfile)",
fg="#FFD700" # Gold
)
else:
self.keyring_status_label.config(
text="Key: Not Available",
fg="#A9A9A9" # DarkGray
)
key_in_keyring = self.encryption_manager.is_key_in_keyring(
username)
app_logger.log(f"HeaderFrame: Key in keyring? {key_in_keyring}")
key_file_exists = os.path.exists(
self.encryption_manager.get_key_file_path(dest_path))
app_logger.log(f"HeaderFrame: Key file exists? {key_file_exists}")
if key_in_keyring:
self.keyring_status_label.config(
text="Key: Available (Keyring)",
fg="#FFD700" # Gold
)
elif key_file_exists:
self.keyring_status_label.config(
text="Key: Available (Keyfile)",
fg="#FFD700" # Gold
)
else:
self.keyring_status_label.config(
text="Key: Not Available",
fg="#A9A9A9" # DarkGray
)
app_logger.log("HeaderFrame: Status refresh complete.")

View File

@@ -105,15 +105,13 @@ class Navigation:
self.app.restore_size_frame_after.grid_remove()
self.app.mode_button_icon = self.app.image_manager.get_icon(
"forward_extralarge")
self.app.info_label.config(text=Msg.STR["backup_mode_info"])
self.app._update_info_label(Msg.STR["backup_mode"])
self.app.full_backup_cb.config(state="normal")
self.app.incremental_cb.config(state="normal")
self.app.compressed_cb.config(state="normal")
self.app.encrypted_cb.config(state="normal")
self.app.bypass_security_cb.config(
state='disabled') # This one is mode-dependent
# Let the central config function handle the state of these checkboxes
self.app.update_backup_options_from_config()
else: # restore
self.app.source_size_frame.grid_remove()
self.app.target_size_frame.grid_remove()
@@ -121,7 +119,7 @@ class Navigation:
self.app.restore_size_frame_after.grid()
self.app.mode_button_icon = self.app.image_manager.get_icon(
"back_extralarge")
self.app.info_label.config(text=Msg.STR["restore_mode_info"])
self.app._update_info_label(Msg.STR["restore"])
self.app.full_backup_cb.config(state='disabled')
self.app.incremental_cb.config(state='disabled')
self.app.compressed_cb.config(state='disabled')
@@ -135,6 +133,8 @@ class Navigation:
self.app.drawing.redraw_right_canvas()
def toggle_mode(self, mode=None, active_index=None, trigger_calculation=True):
if self.app.refresh_log_var.get():
self.app.log_window.clear_log()
if self.app.backup_is_running:
# If a backup is running, we only want to switch the view to the main backup screen.
# We don't reset anything.
@@ -142,7 +142,6 @@ class Navigation:
self.app.scheduler_frame.hide()
self.app.settings_frame.hide()
self.app.backup_content_frame.grid_remove()
# Show the main content frames
self.app.canvas_frame.grid()
@@ -187,7 +186,7 @@ class Navigation:
self.app.scheduler_frame.hide()
self.app.settings_frame.hide()
self.app.backup_content_frame.grid_remove()
self.app.canvas_frame.grid()
self.app.source_size_frame.grid()
self.app.target_size_frame.grid()
@@ -234,6 +233,8 @@ class Navigation:
self._update_task_bar_visibility("log")
def toggle_scheduler_frame(self, active_index=None):
if self.app.refresh_log_var.get():
self.app.log_window.clear_log()
self._cancel_calculation()
if active_index is not None:
self.app.drawing.update_nav_buttons(active_index)
@@ -242,7 +243,7 @@ class Navigation:
self.app.log_frame.grid_remove()
self.app.settings_frame.hide()
self.app.backup_content_frame.grid_remove()
self.app.source_size_frame.grid_remove()
self.app.target_size_frame.grid_remove()
self.app.restore_size_frame_before.grid_remove()
@@ -252,6 +253,8 @@ class Navigation:
self._update_task_bar_visibility("scheduler")
def toggle_settings_frame(self, active_index=None):
if self.app.refresh_log_var.get():
self.app.log_window.clear_log()
self._cancel_calculation()
if active_index is not None:
self.app.drawing.update_nav_buttons(active_index)
@@ -260,7 +263,7 @@ class Navigation:
self.app.log_frame.grid_remove()
self.app.backup_content_frame.grid_remove()
self.app.scheduler_frame.hide()
self.app.source_size_frame.grid_remove()
self.app.target_size_frame.grid_remove()
self.app.restore_size_frame_before.grid_remove()
@@ -269,40 +272,41 @@ class Navigation:
self.app.top_bar.grid()
self._update_task_bar_visibility("settings")
def toggle_backup_content_frame(self, active_index=None):
# Accept argument but ignore it
def toggle_backup_content_frame(self, _=None):
if self.app.refresh_log_var.get():
self.app.log_window.clear_log()
self._cancel_calculation()
if active_index is not None:
self.app.drawing.update_nav_buttons(active_index)
self.app.drawing.update_nav_buttons(2) # Index 2 for Backup Content
if not self.app.destination_path:
MessageDialog(master=self.app, message_type="info",
MessageDialog(message_type="info",
title=Msg.STR["info_menu"], text=Msg.STR["err_no_dest_folder"])
self.toggle_mode("backup", 0)
return
# Mount the destination if it is encrypted and not already mounted
if self.app.backup_manager.encryption_manager.is_encrypted(self.app.destination_path):
if not self.app.backup_manager.encryption_manager.is_mounted(self.app.destination_path):
mount_point = self.app.backup_manager.encryption_manager.mount(self.app.destination_path)
if not mount_point:
MessageDialog(master=self.app, message_type="error",
title=Msg.STR["error"], text=Msg.STR.get("err_mount_failed", "Mounting failed."))
self.toggle_mode("backup", 0)
return
self.app.header_frame.refresh_status()
self.app.canvas_frame.grid_remove()
self.app.log_frame.grid_remove()
self.app.scheduler_frame.hide()
self.app.settings_frame.hide()
self.app.source_size_frame.grid_remove()
self.app.target_size_frame.grid_remove()
self.app.restore_size_frame_before.grid_remove()
self.app.restore_size_frame_after.grid_remove()
self.app.backup_content_frame.show(self.app.destination_path)
if self.app.next_backup_content_view == 'user':
initial_tab_index = 1
else: # Default to system
initial_tab_index = 0
self.app.backup_content_frame.show(
self.app.destination_path, initial_tab_index)
# Reset to default for next time
self.app.next_backup_content_view = 'system'
self.app.top_bar.grid()
self._update_task_bar_visibility("scheduler")

View File

@@ -1,63 +0,0 @@
import tkinter as tk
from tkinter import ttk, messagebox
class PasswordDialog(tk.Toplevel):
def __init__(self, parent, title="Password Required", confirm=True):
super().__init__(parent)
self.title(title)
self.parent = parent
self.password = None
self.save_to_keyring = tk.BooleanVar()
self.confirm = confirm
self.transient(parent)
self.grab_set()
ttk.Label(self, text="Please enter the password for the encrypted backup:").pack(padx=20, pady=10)
self.password_entry = ttk.Entry(self, show="*")
self.password_entry.pack(padx=20, pady=5, fill="x", expand=True)
self.password_entry.focus_set()
if self.confirm:
ttk.Label(self, text="Confirm password:").pack(padx=20, pady=10)
self.confirm_entry = ttk.Entry(self, show="*")
self.confirm_entry.pack(padx=20, pady=5, fill="x", expand=True)
self.save_to_keyring_cb = ttk.Checkbutton(self, text="Save password to system keyring", variable=self.save_to_keyring)
self.save_to_keyring_cb.pack(padx=20, pady=10)
button_frame = ttk.Frame(self)
button_frame.pack(pady=10)
ok_button = ttk.Button(button_frame, text="OK", command=self.on_ok)
ok_button.pack(side="left", padx=5)
cancel_button = ttk.Button(button_frame, text="Cancel", command=self.on_cancel)
cancel_button.pack(side="left", padx=5)
self.bind("<Return>", lambda event: self.on_ok())
self.bind("<Escape>", lambda event: self.on_cancel())
self.wait_window(self)
def on_ok(self):
password = self.password_entry.get()
if not password:
messagebox.showerror("Error", "Password cannot be empty.", parent=self)
return
if self.confirm:
confirm = self.confirm_entry.get()
if password != confirm:
messagebox.showerror("Error", "Passwords do not match.", parent=self)
return
self.password = password
self.destroy()
def on_cancel(self):
self.password = None
self.destroy()
def get_password(self):
return self.password, self.save_to_keyring.get()

View File

@@ -5,6 +5,7 @@ import os
from shared_libs.custom_file_dialog import CustomFileDialog
from shared_libs.message import MessageDialog
from core.pbp_app_config import Msg
from pyimage_ui.shared_logic import enforce_backup_type_exclusivity
class SchedulerFrame(ttk.Frame):
@@ -17,7 +18,8 @@ class SchedulerFrame(ttk.Frame):
self, text=Msg.STR["scheduled_jobs"], padding=10)
self.jobs_frame.pack(fill=tk.BOTH, expand=True)
columns = ("active", "type", "frequency", "destination", "sources")
columns = ("active", "type", "frequency",
"destination", "sources", "options")
self.jobs_tree = ttk.Treeview(
self.jobs_frame, columns=columns, show="headings")
self.jobs_tree.heading("active", text=Msg.STR["active"])
@@ -25,6 +27,7 @@ class SchedulerFrame(ttk.Frame):
self.jobs_tree.heading("frequency", text=Msg.STR["frequency"])
self.jobs_tree.heading("destination", text=Msg.STR["destination"])
self.jobs_tree.heading("sources", text=Msg.STR["sources"])
self.jobs_tree.heading("options", text=Msg.STR["backup_options"])
for col in columns:
self.jobs_tree.column(col, width=100, anchor="center")
self.jobs_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
@@ -51,20 +54,56 @@ class SchedulerFrame(ttk.Frame):
}
self.frequency = tk.StringVar(value="daily")
self.backup_type_system_var = tk.BooleanVar(value=True)
self.backup_type_user_var = tk.BooleanVar(value=False)
self.freq_daily_var = tk.BooleanVar(value=True)
self.freq_weekly_var = tk.BooleanVar(value=False)
self.freq_monthly_var = tk.BooleanVar(value=False)
type_frame = ttk.LabelFrame(
self.add_job_frame, text=Msg.STR["backup_type"], padding=10)
type_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Radiobutton(type_frame, text=Msg.STR["system_backup_menu"], variable=self.backup_type,
value="system", command=self._toggle_user_sources).pack(anchor=tk.W)
ttk.Radiobutton(type_frame, text=Msg.STR["user_backup_menu"], variable=self.backup_type,
value="user", command=self._toggle_user_sources).pack(anchor=tk.W)
ttk.Checkbutton(type_frame, text=Msg.STR["system_backup_menu"], variable=self.backup_type_system_var,
style="Switch.TCheckbutton", command=lambda: self._handle_backup_type_switch("system")).pack(anchor=tk.W)
ttk.Checkbutton(type_frame, text=Msg.STR["user_backup_menu"], variable=self.backup_type_user_var,
style="Switch.TCheckbutton", command=lambda: self._handle_backup_type_switch("user")).pack(anchor=tk.W)
# Container for source folders and backup options
source_options_container = ttk.Frame(self.add_job_frame)
source_options_container.pack(fill=tk.X, padx=5, pady=5)
source_options_container.columnconfigure(0, weight=1)
source_options_container.columnconfigure(1, weight=1)
self.user_sources_frame = ttk.LabelFrame(
self.add_job_frame, text=Msg.STR["source_folders"], padding=10)
self.user_sources_frame.pack(fill=tk.X, padx=5, pady=5)
source_options_container, text=Msg.STR["source_folders"], padding=10)
self.user_sources_frame.grid(
row=0, column=0, sticky="nsew", padx=5, pady=5)
for name, var in self.user_sources.items():
ttk.Checkbutton(self.user_sources_frame, text=name,
variable=var).pack(anchor=tk.W)
variable=var, style="Switch.TCheckbutton").pack(anchor=tk.W)
options_frame = ttk.LabelFrame(
source_options_container, text=Msg.STR["backup_options"], padding=10)
options_frame.grid(row=0, column=1, sticky="nsew", padx=5, pady=5)
self.full_var = tk.BooleanVar(value=True)
self.incremental_var = tk.BooleanVar(value=False)
self.compress_var = tk.BooleanVar(value=False)
self.encrypt_var = tk.BooleanVar(value=False)
self.full_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["full_backup"], variable=self.full_var, command=lambda: enforce_backup_type_exclusivity(
self.full_var, self.incremental_var, self.full_var.get()), style="Switch.TCheckbutton")
self.full_checkbutton.pack(anchor=tk.W)
self.incremental_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["incremental_backup"], variable=self.incremental_var, command=lambda: enforce_backup_type_exclusivity(
self.incremental_var, self.full_var, self.incremental_var.get()), style="Switch.TCheckbutton")
self.incremental_checkbutton.pack(anchor=tk.W)
self.compress_checkbutton = ttk.Checkbutton(
options_frame, text=Msg.STR["compression"], variable=self.compress_var, command=self._on_compression_toggle_scheduler, style="Switch.TCheckbutton")
self.compress_checkbutton.pack(anchor=tk.W)
self.encrypt_checkbutton = ttk.Checkbutton(
options_frame, text=Msg.STR["encryption"], variable=self.encrypt_var, style="Switch.TCheckbutton")
self.encrypt_checkbutton.pack(anchor=tk.W)
dest_frame = ttk.LabelFrame(
self.add_job_frame, text=Msg.STR["dest_folder"], padding=10)
@@ -77,12 +116,12 @@ class SchedulerFrame(ttk.Frame):
freq_frame = ttk.LabelFrame(
self.add_job_frame, text=Msg.STR["frequency"], padding=10)
freq_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Radiobutton(freq_frame, text=Msg.STR["freq_daily"],
variable=self.frequency, value="daily").pack(anchor=tk.W)
ttk.Radiobutton(freq_frame, text=Msg.STR["freq_weekly"],
variable=self.frequency, value="weekly").pack(anchor=tk.W)
ttk.Radiobutton(freq_frame, text=Msg.STR["freq_monthly"],
variable=self.frequency, value="monthly").pack(anchor=tk.W)
ttk.Checkbutton(freq_frame, text=Msg.STR["freq_daily"], variable=self.freq_daily_var,
style="Switch.TCheckbutton", command=lambda: self._handle_freq_switch("daily")).pack(anchor=tk.W)
ttk.Checkbutton(freq_frame, text=Msg.STR["freq_weekly"], variable=self.freq_weekly_var,
style="Switch.TCheckbutton", command=lambda: self._handle_freq_switch("weekly")).pack(anchor=tk.W)
ttk.Checkbutton(freq_frame, text=Msg.STR["freq_monthly"], variable=self.freq_monthly_var,
style="Switch.TCheckbutton", command=lambda: self._handle_freq_switch("monthly")).pack(anchor=tk.W)
add_button_frame = ttk.Frame(self.add_job_frame)
add_button_frame.pack(pady=10)
@@ -97,6 +136,34 @@ class SchedulerFrame(ttk.Frame):
# Initially, hide the add_job_frame
self.add_job_frame.pack_forget()
def _handle_backup_type_switch(self, changed_var):
if changed_var == "system":
if self.backup_type_system_var.get():
self.backup_type_user_var.set(False)
self.backup_type.set("system")
else:
# Prevent unsetting both
self.backup_type_system_var.set(True)
elif changed_var == "user":
if self.backup_type_user_var.get():
self.backup_type_system_var.set(False)
self.backup_type.set("user")
else:
self.backup_type_user_var.set(True)
self._toggle_user_sources()
def _handle_freq_switch(self, changed_var):
vars = {"daily": self.freq_daily_var,
"weekly": self.freq_weekly_var, "monthly": self.freq_monthly_var}
if vars[changed_var].get():
self.frequency.set(changed_var)
for var_name, var_obj in vars.items():
if var_name != changed_var:
var_obj.set(False)
else:
# Prevent unsetting all
vars[changed_var].set(True)
def show(self):
self.grid(row=2, column=0, sticky="nsew")
self._load_scheduled_jobs()
@@ -104,13 +171,34 @@ class SchedulerFrame(ttk.Frame):
def hide(self):
self.grid_remove()
def _on_compression_toggle_scheduler(self):
if self.compress_var.get():
self.incremental_var.set(False)
self.incremental_checkbutton.config(state="disabled")
self.encrypt_var.set(False)
self.encrypt_checkbutton.config(state="disabled")
else:
self.incremental_checkbutton.config(state="normal")
self.encrypt_checkbutton.config(state="normal")
def _toggle_scheduler_view(self):
if self.jobs_frame.winfo_ismapped():
self.jobs_frame.pack_forget()
self.add_job_frame.pack(fill=tk.BOTH, expand=True)
# Reset or load default values for new job
self.full_var.set(True)
self.incremental_var.set(False)
self.compress_var.set(False)
self.encrypt_var.set(False)
self.destination.set("")
for var in self.user_sources.values():
var.set(False)
self._on_compression_toggle_scheduler() # Update state of incremental checkbox
else:
self.add_job_frame.pack_forget()
self.jobs_frame.pack(fill=tk.BOTH, expand=True)
self._load_scheduled_jobs()
def _toggle_user_sources(self):
state = "normal" if self.backup_type.get() == "user" else "disabled"
@@ -127,7 +215,7 @@ class SchedulerFrame(ttk.Frame):
def _save_job(self):
dest = self.destination.get()
if not dest:
MessageDialog(master=self, message_type="error",
MessageDialog(message_type="error",
title=Msg.STR["error"], text=Msg.STR["err_no_dest_folder"])
return
@@ -139,22 +227,34 @@ class SchedulerFrame(ttk.Frame):
job_sources = [name for name,
var in self.user_sources.items() if var.get()]
if not job_sources:
MessageDialog(master=self, message_type="error",
MessageDialog(message_type="error",
title=Msg.STR["error"], text=Msg.STR["err_no_source_folder"])
return
script_path = os.path.abspath(os.path.join(
os.path.dirname(__file__), "..", "main_app.py"))
os.path.dirname(__file__), "..", "pybackup-cli.py"))
command = f"python3 {script_path} --backup-type {job_type} --destination \"{dest}\""
if self.full_var.get():
command += " --full"
if self.incremental_var.get():
command += " --incremental"
if self.compress_var.get():
command += " --compress"
if self.encrypt_var.get():
command += " --encrypt"
if job_type == "user":
command += f" --sources "
for s in job_sources:
command += f'\"{s}\" ' # This line has an issue with escaping
command += f'\"{s}\" '
comment = f"{self.backup_manager.app_tag}; type:{job_type}; freq:{job_frequency}; dest:{dest}"
if job_type == "user":
comment += f"; sources:{','.join(job_sources)}"
comment += f"; full:{self.full_var.get()}; incremental:{self.incremental_var.get()}; compress:{self.compress_var.get()}; encrypt:{self.encrypt_var.get()}"
job_details = {
"command": command,
"comment": comment,
@@ -172,15 +272,25 @@ class SchedulerFrame(ttk.Frame):
self.jobs_tree.delete(i)
jobs = self.backup_manager.get_scheduled_jobs()
for job in jobs:
options = []
if job.get("full"):
options.append("Full")
if job.get("incremental"):
options.append("Incremental")
if job.get("compress"):
options.append("Compressed")
if job.get("encrypt"):
options.append("Encrypted")
self.jobs_tree.insert("", "end", values=(
job["active"], job["type"], job["frequency"], job["destination"], ", ".join(
job["sources"])
job["sources"]), ", ".join(options)
), iid=job["id"])
def _remove_selected_job(self):
selected_item = self.jobs_tree.focus()
if not selected_item:
MessageDialog(master=self, message_type="error",
MessageDialog(message_type="error",
title=Msg.STR["error"], text=Msg.STR["err_no_job_selected"])
return

View File

@@ -1,27 +1,87 @@
import tkinter as tk
from tkinter import ttk
import os
import shutil
import sys
from pathlib import Path
from core.pbp_app_config import AppConfig, Msg
from pyimage_ui.advanced_settings_frame import AdvancedSettingsFrame
from shared_libs.custom_file_dialog import CustomFileDialog
from shared_libs.message import MessageDialog
from shared_libs.message import MessageDialog, PasswordDialog
class SettingsFrame(ttk.Frame):
def __init__(self, master, navigation, actions, **kwargs):
def __init__(self, master, navigation, actions, encryption_manager, image_manager, config_manager, **kwargs):
super().__init__(master, **kwargs)
self.navigation = navigation
self.actions = actions
self.encryption_manager = encryption_manager
self.image_manager = image_manager
self.config_manager = config_manager
self.pbp_app_config = AppConfig()
self.user_exclude_patterns = []
# --- Action Buttons ---
self.button_frame = ttk.Frame(self)
self.button_frame.pack(fill=tk.X, padx=10)
ttk.Separator(self.button_frame, orient=tk.HORIZONTAL).pack(
fill=tk.X, pady=(0, 5))
self.show_hidden_button = ttk.Button(
self.button_frame, command=self._toggle_hidden_files_view, style="Gray.Toolbutton")
self.show_hidden_button.pack(side=tk.LEFT)
self.unhide_icon = self.image_manager.get_icon(
'hide')
self.hide_icon = self.image_manager.get_icon(
'unhide')
self.show_hidden_button.config(image=self.unhide_icon)
ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack(
side=tk.LEFT, ipady=15, padx=5)
add_to_exclude_button = ttk.Button(
self.button_frame, text=Msg.STR["add_to_exclude_list"], command=self._add_to_exclude_list, style="Gray.Toolbutton")
add_to_exclude_button.pack(side=tk.LEFT, padx=5)
ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack(
side=tk.LEFT, ipady=15, padx=5)
advanced_button = ttk.Button(
self.button_frame, text=Msg.STR["advanced"], command=self._open_advanced_settings, style="Gray.Toolbutton")
advanced_button.pack(side=tk.LEFT, padx=5)
ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack(
side=tk.LEFT, ipady=15, padx=5)
# Right-aligned buttons
hard_reset_button = ttk.Button(
self.button_frame, text=Msg.STR["hard_reset"], command=self._toggle_hard_reset_view, style="Gray.Toolbutton")
hard_reset_button.pack(side=tk.LEFT, padx=5)
ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack(
side=tk.LEFT, ipady=15, padx=5)
reset_button = ttk.Button(
self.button_frame, text=Msg.STR["default_settings"], command=self.actions.reset_to_default_settings, style="Gray.Toolbutton")
reset_button.pack(side=tk.LEFT)
# --- Container for Treeviews ---
self.trees_container = ttk.Frame(self)
self.trees_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
self.trees_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
# --- Bottom Buttons ---
self.bottom_button_frame = ttk.Frame(self)
self.bottom_button_frame.pack(pady=10)
apply_button = ttk.Button(
self.bottom_button_frame, text=Msg.STR["apply"], command=self._apply_changes)
apply_button.pack(side=tk.LEFT, padx=5)
cancel_button = ttk.Button(self.bottom_button_frame, text=Msg.STR["cancel"],
command=lambda: self.navigation.toggle_mode("backup", 0))
cancel_button.pack(side=tk.LEFT, padx=5)
# --- Treeview for file/folder exclusion ---
self.tree_frame = ttk.LabelFrame(
@@ -61,69 +121,100 @@ class SettingsFrame(ttk.Frame):
self.hidden_tree.bind("<Button-1>", self._toggle_include_status_hidden)
self.hidden_tree_frame.pack_forget() # Initially hidden
# --- Action Buttons ---
self.button_frame = ttk.Frame(self)
self.button_frame.pack(fill=tk.X, padx=10, pady=10)
# --- Hard Reset Frame (initially hidden) ---
self.hard_reset_frame = ttk.LabelFrame(
self, text=Msg.STR["full_delete_config_settings"], padding=10)
self.show_hidden_button = ttk.Button(
self.button_frame, command=self._toggle_hidden_files_view, style="TButton.Borderless.Round")
self.show_hidden_button.pack(side=tk.LEFT)
self.unhide_icon = self.master.master.master.image_manager.get_icon(
'hide')
self.hide_icon = self.master.master.master.image_manager.get_icon(
'unhide')
self.show_hidden_button.config(image=self.unhide_icon)
hard_reset_label = ttk.Label(
self.hard_reset_frame, text=Msg.STR["hard_reset_warning"], wraplength=400, justify=tk.LEFT)
hard_reset_label.pack(pady=10)
add_to_exclude_button = ttk.Button(
self.button_frame, text=Msg.STR["add_to_exclude_list"], command=self._add_to_exclude_list)
add_to_exclude_button.pack(side=tk.LEFT, padx=5)
hard_reset_button_frame = ttk.Frame(self.hard_reset_frame)
hard_reset_button_frame.pack(pady=10)
apply_button = ttk.Button(
self.button_frame, text=Msg.STR["apply"], command=self._apply_changes)
apply_button.pack(side=tk.LEFT, padx=5)
delete_now_button = ttk.Button(
hard_reset_button_frame, text=Msg.STR["delete_now"], command=self._perform_hard_reset)
delete_now_button.pack(side=tk.LEFT, padx=5)
cancel_button = ttk.Button(self.button_frame, text=Msg.STR["cancel"],
command=lambda: self.navigation.toggle_mode("backup", 0))
cancel_button.pack(side=tk.LEFT, padx=5)
advanced_button = ttk.Button(
self.button_frame, text=Msg.STR["advanced"], command=self._open_advanced_settings)
advanced_button.pack(side=tk.LEFT, padx=5)
reset_button = ttk.Button(
self.button_frame, text=Msg.STR["default_settings"], command=self.actions.reset_to_default_settings)
reset_button.pack(side=tk.RIGHT)
cancel_hard_reset_button = ttk.Button(
hard_reset_button_frame, text=Msg.STR["cancel"], command=self._toggle_hard_reset_view)
cancel_hard_reset_button.pack(side=tk.LEFT, padx=5)
self.hidden_files_visible = False
self.advanced_settings_frame_instance = None # To hold the instance of AdvancedSettingsFrame
self.hard_reset_visible = False
# To hold the instance of AdvancedSettingsFrame
self.advanced_settings_frame_instance = None
def _perform_hard_reset(self):
if self.encryption_manager.mounted_destinations:
dialog = PasswordDialog(
self, title=Msg.STR["unlock_backup"], confirm=False, translations=Msg.STR)
password, _ = dialog.get_password()
if not password:
return
success, message = self.encryption_manager.unmount_all_encrypted_drives(
password)
if not success:
MessageDialog(message_type="error", text=message).show()
return
try:
shutil.rmtree(AppConfig.APP_DIR)
# Restart the application
os.execl(sys.executable, sys.executable, *sys.argv)
except Exception as e:
MessageDialog(message_type="error", text=str(e)).show()
def _toggle_hard_reset_view(self):
self.hard_reset_visible = not self.hard_reset_visible
if self.hard_reset_visible:
self.trees_container.pack_forget()
self.button_frame.pack_forget()
self.bottom_button_frame.pack_forget()
self.hard_reset_frame.pack(
fill=tk.BOTH, expand=True, padx=10, pady=5)
else:
self.hard_reset_frame.pack_forget()
self.button_frame.pack(fill=tk.X, padx=10)
self.trees_container.pack(
fill=tk.BOTH, expand=True, padx=10, pady=5)
self.bottom_button_frame.pack(pady=10)
def _add_to_exclude_list(self) -> bool:
result = MessageDialog("ask", Msg.STR["exclude_dialog_text"], title=Msg.STR["add_to_exclude_list"], buttons=[
Msg.STR["add_folder_button"], Msg.STR["add_file_button"]]).show()
path = None
if result:
if result == 0: # First button: Folder
dialog = CustomFileDialog(
self, mode="dir", title=Msg.STR["add_to_exclude_list"])
self.wait_window(dialog)
path = dialog.get_result()
dialog.destroy()
else:
elif result == 1: # Second button: File
dialog = CustomFileDialog(
self, filetypes=[("All Files", "*.*")], title=Msg.STR["add_to_exclude_list"])
self, filetypes=[("All Files", "*.*")])
self.wait_window(dialog)
path = dialog.get_result()
dialog.destroy()
if path:
with open(AppConfig.MANUAL_EXCLUDE_LIST_PATH, 'a') as f:
if os.path.isdir(path):
f.write(f"\n{path}/*")
else:
f.write(f"\n{path}")
try:
with open(AppConfig.MANUAL_EXCLUDE_LIST_PATH, 'r') as f:
lines = {line.strip() for line in f if line.strip()}
except FileNotFoundError:
lines = set()
new_entry = f"{path}/*" if os.path.isdir(path) else path
lines.add(new_entry)
with open(AppConfig.MANUAL_EXCLUDE_LIST_PATH, 'w') as f:
for line in sorted(list(lines)):
f.write(f"{line}\n")
self.load_and_display_excludes()
self._load_hidden_files()
if hasattr(self, 'advanced_settings_frame_instance') and self.advanced_settings_frame_instance:
self.advanced_settings_frame_instance._load_manual_excludes()
def show(self):
self.grid(row=2, column=0, sticky="nsew")
@@ -136,15 +227,18 @@ class SettingsFrame(ttk.Frame):
all_patterns = []
if AppConfig.GENERATED_EXCLUDE_LIST_PATH.exists():
with open(AppConfig.GENERATED_EXCLUDE_LIST_PATH, 'r') as f:
all_patterns.extend([line.strip() for line in f if line.strip() and not line.startswith('#')])
all_patterns.extend(
[line.strip() for line in f if line.strip() and not line.startswith('#')])
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'r') as f:
all_patterns.extend([line.strip() for line in f if line.strip() and not line.startswith('#')])
all_patterns.extend(
[line.strip() for line in f if line.strip() and not line.startswith('#')])
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
with open(AppConfig.MANUAL_EXCLUDE_LIST_PATH, 'r') as f:
all_patterns.extend([line.strip() for line in f if line.strip() and not line.startswith('#')])
all_patterns.extend(
[line.strip() for line in f if line.strip() and not line.startswith('#')])
return all_patterns
@@ -246,18 +340,19 @@ class SettingsFrame(ttk.Frame):
def _open_advanced_settings(self):
# Hide main settings UI elements
self.trees_container.pack_forget() # Hide the container for treeviews
self.trees_container.pack_forget() # Hide the container for treeviews
self.button_frame.pack_forget()
self.bottom_button_frame.pack_forget()
# Create AdvancedSettingsFrame if not already created
if not self.advanced_settings_frame_instance:
self.advanced_settings_frame_instance = AdvancedSettingsFrame(
self, # Parent is now self (SettingsFrame)
config_manager=self.master.master.master.config_manager,
self, # Parent is now self (SettingsFrame)
config_manager=self.config_manager,
app_instance=self.master.master.master,
show_main_settings_callback=self._show_main_settings
)
# Pack the AdvancedSettingsFrame
self.advanced_settings_frame_instance.pack(fill=tk.BOTH, expand=True)
@@ -265,10 +360,12 @@ class SettingsFrame(ttk.Frame):
# Hide advanced settings frame
if self.advanced_settings_frame_instance:
self.advanced_settings_frame_instance.pack_forget()
# Show main settings UI elements
self.trees_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # Re-pack the container for treeviews
self.button_frame.pack(fill=tk.X, padx=10, pady=10) # Re-pack the button frame
# Re-pack the container for treeviews
self.button_frame.pack(fill=tk.X, padx=10)
self.trees_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
self.bottom_button_frame.pack(pady=10)
def _toggle_hidden_files_view(self):
self.hidden_files_visible = not self.hidden_files_visible

View File

@@ -15,13 +15,6 @@ class SystemBackupContentFrame(ttk.Frame):
self.system_backups_list = []
self.backup_path = None
self.tag_colors = [
("full_blue", "#0078D7", "inc_blue", "#50E6FF"),
("full_orange", "#E8740C", "inc_orange", "#FFB366"),
("full_green", "#107C10", "inc_green", "#50E680"),
("full_purple", "#8B107C", "inc_purple", "#D46EE5"),
]
columns = ("date", "time", "type", "size", "comment")
self.content_tree = ttk.Treeview(
self, columns=columns, show="headings")
@@ -31,18 +24,19 @@ class SystemBackupContentFrame(ttk.Frame):
self.content_tree.heading("size", text=Msg.STR["size"])
self.content_tree.heading("comment", text=Msg.STR["comment"])
self.content_tree.column("date", width=100, anchor="w")
self.content_tree.column("time", width=80, anchor="center")
self.content_tree.column("type", width=120, anchor="center")
self.content_tree.column("size", width=100, anchor="e")
self.content_tree.column("comment", width=300, anchor="w")
self.content_tree.column("date", width=100, anchor="center")
self.content_tree.column("time", width=100, anchor="center")
self.content_tree.column("type", width=180, anchor="w")
self.content_tree.column("size", width=90, anchor="center")
self.content_tree.column("comment", width=310, anchor="w")
self.content_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.content_tree.bind("<<TreeviewSelect>>", self._on_item_select)
def show(self, backup_path, system_backups):
self.backup_path = backup_path
self.system_backups_list = system_backups
self.system_backups_list = sorted(system_backups, key=lambda b: (
b.get('is_encrypted'), b.get('folder_name', '')))
self._load_backup_content()
def _load_backup_content(self):
@@ -52,27 +46,38 @@ class SystemBackupContentFrame(ttk.Frame):
if not self.system_backups_list:
return
color_index = -1
for i, backup_info in enumerate(self.system_backups_list):
colors = ["#0078D7", "#7e4818", "#8B107C", "#005A9E", "#2b3e4e"]
last_full_backup_tag = {}
color_index = 0
for backup_info in self.system_backups_list:
is_encrypted = backup_info.get("is_encrypted")
group_key = (is_encrypted,)
current_color_tag = ""
if backup_info.get("backup_type_base") == "Full":
color_index = (color_index + 1) % len(self.tag_colors)
full_tag, full_color, inc_tag, inc_color = self.tag_colors[color_index]
current_color_tag = f"color_{color_index}"
self.content_tree.tag_configure(
full_tag, foreground=full_color)
self.content_tree.tag_configure(
inc_tag, foreground=inc_color, font=("Helvetica", 10, "bold"))
current_tag = full_tag
current_color_tag, foreground=colors[color_index % len(colors)])
color_index += 1
last_full_backup_tag[group_key] = current_color_tag
else:
_, _, inc_tag, _ = self.tag_colors[color_index]
current_tag = inc_tag
if group_key in last_full_backup_tag:
current_color_tag = last_full_backup_tag[group_key]
else:
current_color_tag = ""
backup_type_display = backup_info.get("type", "N/A")
if backup_info.get("backup_type_base") != "Full":
backup_type_display = f"{backup_type_display}"
self.content_tree.insert("", "end", values=(
backup_info.get("date", "N/A"),
backup_info.get("time", "N/A"),
backup_info.get("type", "N/A"),
backup_type_display,
backup_info.get("size", "N/A"),
backup_info.get("comment", ""),
), tags=(current_tag,), iid=backup_info.get("folder_name"))
), tags=(current_color_tag,), iid=backup_info.get("folder_name"))
self._on_item_select(None)
@@ -88,18 +93,17 @@ class SystemBackupContentFrame(ttk.Frame):
selected_backup = next((b for b in self.system_backups_list if b.get(
"folder_name") == selected_item_id), None)
if not selected_backup:
return
return
is_encrypted = selected_backup.get('is_encrypted', False)
info_file_name = f"{selected_item_id}{'_encrypted' if is_encrypted else ''}.txt"
info_file_path = os.path.join(
self.backup_path, "pybackup", info_file_name)
if not os.path.exists(info_file_path):
self.backup_manager.update_comment(info_file_path, "")
info_file_path = selected_backup.get('info_file_path')
if not info_file_path or not os.path.isfile(info_file_path):
MessageDialog(message_type="error", title="Error",
text=f"Metadata file not found: {info_file_path}")
return
CommentEditorDialog(self, info_file_path, self.backup_manager)
self.parent_view.show(self.backup_path)
self.parent_view.show(
self.backup_path, self.system_backups_list) # Refresh view
def _restore_selected(self):
selected_item_id = self.content_tree.focus()
@@ -122,7 +126,7 @@ class SystemBackupContentFrame(ttk.Frame):
self.backup_manager.start_restore(
source_path=selected_backup['full_path'],
dest_path=restore_dest_path,
is_compressed=selected_backup['is_compressed']
is_compressed=selected_backup.get('is_compressed', False)
)
def _delete_selected(self):
@@ -142,21 +146,19 @@ class SystemBackupContentFrame(ttk.Frame):
if is_encrypted:
username = os.path.basename(self.backup_path.rstrip('/'))
# Get password in the UI thread before starting the background task
password = self.backup_manager.encryption_manager.get_password(username, confirm=False)
password = self.backup_manager.encryption_manager.get_password(
username, confirm=False)
if not password:
self.actions.logger.log("Password entry cancelled, aborting deletion.")
self.actions.logger.log(
"Password entry cancelled, aborting deletion.")
return
info_file_to_delete = os.path.join(
self.backup_path, "pybackup", f"{selected_item_id}{'_encrypted' if is_encrypted else ''}.txt")
self.actions._set_ui_state(False)
self.parent_view.show_deletion_status(Msg.STR["deleting_backup_in_progress"])
self.parent_view.show_deletion_status(
Msg.STR["deleting_backup_in_progress"])
self.backup_manager.start_delete_backup(
path_to_delete=folder_to_delete,
info_file_path=info_file_to_delete,
path_to_delete=folder_to_delete,
is_encrypted=is_encrypted,
is_system=True,
base_dest_path=self.backup_path,

View File

@@ -1,12 +1,12 @@
import tkinter as tk
from tkinter import ttk
import os
import shutil
from core.pbp_app_config import Msg
from pyimage_ui.comment_editor_dialog import CommentEditorDialog
from shared_libs.message import MessageDialog
class UserBackupContentFrame(ttk.Frame):
def __init__(self, master, backup_manager, actions, parent_view, **kwargs):
super().__init__(master, **kwargs)
@@ -16,35 +16,30 @@ class UserBackupContentFrame(ttk.Frame):
self.user_backups_list = []
self.backup_path = None
self.tag_colors = [
("full_blue", "#0078D7", "inc_blue", "#50E6FF"),
("full_orange", "#E8740C", "inc_orange", "#FFB366"),
("full_green", "#107C10", "inc_green", "#50E680"),
("full_purple", "#8B107C", "inc_purple", "#D46EE5"),
]
columns = ("date", "time", "type", "size", "comment", "folder_name")
self.content_tree = ttk.Treeview(self, columns=columns, show="headings")
columns = ("date", "time", "type", "size", "folder_name", "comment")
self.content_tree = ttk.Treeview(
self, columns=columns, show="headings")
self.content_tree.heading("date", text=Msg.STR["date"])
self.content_tree.heading("time", text=Msg.STR["time"])
self.content_tree.heading("type", text=Msg.STR["type"])
self.content_tree.heading("size", text=Msg.STR["size"])
self.content_tree.heading("comment", text=Msg.STR["comment"])
self.content_tree.heading("folder_name", text=Msg.STR["folder"])
self.content_tree.heading("comment", text=Msg.STR["comment"])
self.content_tree.column("date", width=100, anchor="w")
self.content_tree.column("time", width=80, anchor="center")
self.content_tree.column("type", width=120, anchor="center")
self.content_tree.column("size", width=100, anchor="e")
self.content_tree.column("comment", width=250, anchor="w")
self.content_tree.column("folder_name", width=200, anchor="w")
self.content_tree.column("date", width=100, anchor="center")
self.content_tree.column("time", width=100, anchor="center")
self.content_tree.column("type", width=180, anchor="w")
self.content_tree.column("size", width=90, anchor="center")
self.content_tree.column("folder_name", width=130, anchor="center")
self.content_tree.column("comment", width=180, anchor="w")
self.content_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.content_tree.bind("<<TreeviewSelect>>", self._on_item_select)
def show(self, backup_path, user_backups):
self.backup_path = backup_path
self.user_backups_list = user_backups
self.user_backups_list = sorted(user_backups, key=lambda b: (
b.get('source', ''), b.get('is_encrypted'), b.get('folder_name', '')))
self._load_backup_content()
def _load_backup_content(self):
@@ -54,28 +49,40 @@ class UserBackupContentFrame(ttk.Frame):
if not self.user_backups_list:
return
color_index = -1
for i, backup_info in enumerate(self.user_backups_list):
colors = ["#0078D7", "#854710", "#8B107C", "#005A9E", "#2b3e4e"]
last_full_backup_tag = {}
color_index = 0
for backup_info in self.user_backups_list:
source = backup_info.get("source")
is_encrypted = backup_info.get("is_encrypted")
group_key = (source, is_encrypted)
current_color_tag = ""
if backup_info.get("backup_type_base") == "Full":
color_index = (color_index + 1) % len(self.tag_colors)
full_tag, full_color, inc_tag, inc_color = self.tag_colors[color_index]
current_color_tag = f"color_{color_index}"
self.content_tree.tag_configure(
full_tag, foreground=full_color)
self.content_tree.tag_configure(
inc_tag, foreground=inc_color, font=("Helvetica", 10, "bold"))
current_tag = full_tag
current_color_tag, foreground=colors[color_index % len(colors)])
color_index += 1
last_full_backup_tag[group_key] = current_color_tag
else:
_, _, inc_tag, _ = self.tag_colors[color_index]
current_tag = inc_tag
if group_key in last_full_backup_tag:
current_color_tag = last_full_backup_tag[group_key]
else:
current_color_tag = ""
backup_type_display = backup_info.get("type", "N/A")
if backup_info.get("backup_type_base") != "Full":
backup_type_display = f"{backup_type_display}"
self.content_tree.insert("", "end", values=(
backup_info.get("date", "N/A"),
backup_info.get("time", "N/A"),
backup_info.get("type", "N/A"),
backup_type_display,
backup_info.get("size", "N/A"),
backup_info.get("comment", ""),
backup_info.get("folder_name", "N/A")
), tags=(current_tag,), iid=backup_info.get("folder_name"))
backup_info.get("source", "N/A"),
backup_info.get("comment", "")
), tags=(current_color_tag,), iid=backup_info.get("folder_name"))
self._on_item_select(None)
def _on_item_select(self, event):
@@ -92,23 +99,23 @@ class UserBackupContentFrame(ttk.Frame):
if not selected_backup:
return
is_encrypted = selected_backup.get('is_encrypted', False)
info_file_name = f"{selected_item_id}{'_encrypted' if is_encrypted else ''}.txt"
info_file_path = os.path.join(
self.backup_path, "pybackup", info_file_name)
if not os.path.exists(info_file_path):
self.backup_manager.update_comment(info_file_path, "")
info_file_path = selected_backup.get('info_file_path')
if not info_file_path or not os.path.isfile(info_file_path):
MessageDialog(message_type="error", title="Error",
text=f"Metadata file not found: {info_file_path}")
return
CommentEditorDialog(self, info_file_path, self.backup_manager)
self.parent_view.show(self.backup_path)
self.parent_view.show(self.backup_path, self.user_backups_list)
def _restore_selected(self):
selected_item_id = self.content_tree.focus()
if not selected_item_id:
return
MessageDialog(master=self, message_type="info", title="Info", text="User restore not implemented yet.")
MessageDialog(message_type="info",
title="Info", text="User restore not implemented yet.")
def _delete_selected(self):
selected_item_id = self.content_tree.focus()
@@ -126,22 +133,19 @@ class UserBackupContentFrame(ttk.Frame):
password = None
if is_encrypted:
username = os.path.basename(self.backup_path.rstrip('/'))
# Get password in the UI thread before starting the background task
password = self.backup_manager.encryption_manager.get_password(username, confirm=False)
password = self.backup_manager.encryption_manager.get_password(
confirm=False)
if not password:
self.actions.logger.log("Password entry cancelled, aborting deletion.")
self.actions.logger.log(
"Password entry cancelled, aborting deletion.")
return
info_file_to_delete = os.path.join(
self.backup_path, "pybackup", f"{selected_item_id}{'_encrypted' if is_encrypted else ''}.txt")
self.actions._set_ui_state(False)
self.parent_view.show_deletion_status(Msg.STR["deleting_backup_in_progress"])
self.parent_view.show_deletion_status(
Msg.STR["deleting_backup_in_progress"])
self.backup_manager.start_delete_backup(
path_to_delete=folder_to_delete,
info_file_path=info_file_to_delete,
path_to_delete=folder_to_delete,
is_encrypted=is_encrypted,
is_system=False,
base_dest_path=self.backup_path,