encrypt backups part one
This commit is contained in:
@@ -128,10 +128,9 @@ rm -f '{info_file}'
|
||||
else:
|
||||
self.logger.log("No active backup process to cancel.")
|
||||
|
||||
def start_backup(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool = False, exclude_files: Optional[List[Path]] = None, source_size: int = 0, is_compressed: bool = False, is_encrypted: bool = False, mode: str = "incremental"):
|
||||
"""Starts a generic backup process for a specific path, reporting to a queue."""
|
||||
def start_backup(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool = False, exclude_files: Optional[List[Path]] = None, source_size: int = 0, is_compressed: bool = False, is_encrypted: bool = False, mode: str = "incremental", password: str = None):
|
||||
thread = threading.Thread(target=self._run_backup_path, args=(
|
||||
queue, source_path, dest_path, is_system, is_dry_run, exclude_files, source_size, is_compressed, is_encrypted, mode))
|
||||
queue, source_path, dest_path, is_system, is_dry_run, exclude_files, source_size, is_compressed, is_encrypted, mode, password))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
@@ -209,14 +208,14 @@ set -e
|
||||
|
||||
|
||||
|
||||
def _run_backup_path(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool, exclude_files: Optional[List[Path]], source_size: int, is_compressed: bool, is_encrypted: bool, mode: str):
|
||||
def _run_backup_path(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool, exclude_files: Optional[List[Path]], source_size: int, is_compressed: bool, is_encrypted: bool, mode: str, password: str):
|
||||
try:
|
||||
if is_encrypted:
|
||||
# For encrypted backups, the dest_path is the container file.
|
||||
container_path = dest_path + ".luks"
|
||||
# Estimate container size to be 110% of source size
|
||||
size_gb = int(source_size / (1024**3) * 1.1) + 1
|
||||
mount_point = self.encryption_manager.setup_encrypted_backup(queue, container_path, size_gb)
|
||||
mount_point = self.encryption_manager.setup_encrypted_backup(queue, container_path, size_gb, password)
|
||||
if not mount_point:
|
||||
return # Error or cancellation already handled in setup method
|
||||
|
||||
@@ -244,7 +243,7 @@ set -e
|
||||
else:
|
||||
command.extend(['rsync', '-av'])
|
||||
|
||||
if mode == "incremental" and latest_backup_path and not is_dry_run:
|
||||
if mode == "incremental" and latest_backup_path and not is_dry_run and not is_encrypted:
|
||||
self.logger.log(f"Using --link-dest='{latest_backup_path}'")
|
||||
command.append(f"--link-dest={latest_backup_path}")
|
||||
|
||||
@@ -596,6 +595,14 @@ set -e
|
||||
details[key.strip()] = value.strip()
|
||||
return details
|
||||
|
||||
def has_encrypted_backups(self, base_backup_path: str) -> bool:
|
||||
"""Checks if any encrypted system backups exist in the destination."""
|
||||
system_backups = self.list_system_backups(base_backup_path)
|
||||
for backup in system_backups:
|
||||
if backup.get('is_encrypted'):
|
||||
return True
|
||||
return False
|
||||
|
||||
def list_backups(self, base_backup_path: str) -> List[str]:
|
||||
backups = []
|
||||
if os.path.isdir(base_backup_path):
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import keyring
|
||||
import keyring.errors
|
||||
from keyring.backends import SecretService
|
||||
@@ -13,11 +12,14 @@ from pyimage_ui.password_dialog import PasswordDialog
|
||||
|
||||
class EncryptionManager:
|
||||
def __init__(self, logger, app=None):
|
||||
try:
|
||||
keyring.set_keyring(SecretService.Keyring())
|
||||
except Exception as e:
|
||||
logger.log(f"Failed to set keyring backend to SecretService: {e}")
|
||||
self.logger = logger
|
||||
self.app = app
|
||||
self.service_id = "py-backup-encryption"
|
||||
self.session_password = None
|
||||
self.save_to_keyring = False
|
||||
|
||||
def get_password_from_keyring(self, username: str) -> Optional[str]:
|
||||
try:
|
||||
@@ -29,12 +31,14 @@ class EncryptionManager:
|
||||
self.logger.log(f"Could not get password from keyring: {e}")
|
||||
return None
|
||||
|
||||
def set_password_in_keyring(self, username: str, password: str):
|
||||
def set_password_in_keyring(self, username: str, password: str) -> bool:
|
||||
try:
|
||||
keyring.set_password(self.service_id, username, password)
|
||||
self.logger.log(f"Password for {username} stored in keyring.")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.log(f"Could not set password in keyring: {e}")
|
||||
return False
|
||||
|
||||
def delete_password_from_keyring(self, username: str):
|
||||
try:
|
||||
@@ -43,34 +47,29 @@ class EncryptionManager:
|
||||
except Exception as e:
|
||||
self.logger.log(f"Could not delete password from keyring: {e}")
|
||||
|
||||
def set_session_password(self, password: str, save_to_keyring: bool):
|
||||
self.session_password = password
|
||||
self.save_to_keyring = save_to_keyring
|
||||
|
||||
def clear_session_password(self):
|
||||
self.session_password = None
|
||||
self.save_to_keyring = False
|
||||
|
||||
def get_password(self, username: str, confirm: bool = True) -> Optional[str]:
|
||||
if self.session_password:
|
||||
if self.save_to_keyring:
|
||||
self.set_password_in_keyring(username, self.session_password)
|
||||
return self.session_password
|
||||
|
||||
password = self.get_password_from_keyring(username)
|
||||
if password:
|
||||
self.session_password = password
|
||||
return password
|
||||
|
||||
# If not in keyring, prompt the user
|
||||
dialog = PasswordDialog(self.app, title=f"Enter password for {username}", confirm=confirm)
|
||||
password = dialog.get_password()
|
||||
password, save_to_keyring = dialog.get_password()
|
||||
if password and save_to_keyring:
|
||||
self.set_password_in_keyring(username, password)
|
||||
|
||||
if password:
|
||||
# Ask to save the password
|
||||
# For now, we don't save it automatically. This will be a UI option.
|
||||
pass
|
||||
self.session_password = password
|
||||
|
||||
return password
|
||||
|
||||
def setup_encrypted_backup(self, queue, container_path: str, size_gb: int) -> Optional[str]:
|
||||
def setup_encrypted_backup(self, queue, container_path: str, size_gb: int, password: str) -> Optional[str]:
|
||||
"""Sets up a LUKS encrypted container for the backup."""
|
||||
self.logger.log(f"Setting up encrypted container at {container_path}")
|
||||
|
||||
@@ -83,12 +82,10 @@ class EncryptionManager:
|
||||
mount_point = f"/mnt/{mapper_name}"
|
||||
|
||||
if os.path.exists(container_path):
|
||||
# Container exists, try to open it
|
||||
self.logger.log(f"Encrypted container {container_path} already exists. Attempting to unlock.")
|
||||
password = self.get_password(os.path.basename(container_path), confirm=False)
|
||||
if not password:
|
||||
self.logger.log("User cancelled password entry.")
|
||||
queue.put(('completion', {'status': 'cancelled', 'returncode': -1}))
|
||||
self.logger.log("No password provided for existing encrypted container.")
|
||||
queue.put(('error', "No password provided for existing encrypted container."))
|
||||
return None
|
||||
|
||||
script = f"""
|
||||
@@ -101,11 +98,9 @@ class EncryptionManager:
|
||||
queue.put(('error', "Failed to unlock existing encrypted container."))
|
||||
return None
|
||||
else:
|
||||
# Container does not exist, create it
|
||||
password = self.get_password(os.path.basename(container_path), confirm=True)
|
||||
if not password:
|
||||
self.logger.log("User cancelled password entry.")
|
||||
queue.put(('completion', {'status': 'cancelled', 'returncode': -1}))
|
||||
self.logger.log("No password provided to create encrypted container.")
|
||||
queue.put(('error', "No password provided to create encrypted container."))
|
||||
return None
|
||||
|
||||
script = f"""
|
||||
@@ -131,8 +126,8 @@ class EncryptionManager:
|
||||
self.logger.log(f"Cleaning up encrypted backup: {mapper_name}")
|
||||
script = f"""
|
||||
umount {mount_point} || echo "Mount point not found or already unmounted."
|
||||
cryptsetup luksClose {mapper_name} || echo "Mapper not found or already closed."
|
||||
rmdir {mount_point} || echo "Mount point directory not found."
|
||||
cryptsetup luksClose {mapper_name} || echo "Mapper not found or already closed."
|
||||
rmdir {mount_point} || echo "Mount point directory not found."
|
||||
"""
|
||||
if not self._execute_as_root(script):
|
||||
self.logger.log("Encrypted backup cleanup script failed.")
|
||||
@@ -141,14 +136,12 @@ class EncryptionManager:
|
||||
"""Executes a shell script with root privileges using pkexec."""
|
||||
script_path = ''
|
||||
try:
|
||||
# Use tempfile for secure temporary file creation
|
||||
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") # Exit on error
|
||||
tmp_script.write("set -e\n\n")
|
||||
tmp_script.write(script_content)
|
||||
script_path = tmp_script.name
|
||||
|
||||
# Make the script executable
|
||||
os.chmod(script_path, stat.S_IRWXU | stat.S_IRGRP |
|
||||
stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
|
||||
|
||||
|
||||
22
main_app.py
22
main_app.py
@@ -16,7 +16,7 @@ from pyimage_ui.scheduler_frame import SchedulerFrame
|
||||
from pyimage_ui.backup_content_frame import BackupContentFrame
|
||||
from pyimage_ui.header_frame import HeaderFrame
|
||||
from pyimage_ui.settings_frame import SettingsFrame
|
||||
from pyimage_ui.encryption_frame import EncryptionFrame
|
||||
|
||||
from core.data_processing import DataProcessing
|
||||
from pyimage_ui.drawing import Drawing
|
||||
from pyimage_ui.navigation import Navigation
|
||||
@@ -48,6 +48,8 @@ class MainApplication(tk.Tk):
|
||||
self.style.layout("Toolbutton"))
|
||||
self.style.configure("Gray.Toolbutton", foreground="gray")
|
||||
|
||||
self.style.configure("Green.Sidebar.TButton", foreground="green")
|
||||
|
||||
main_frame = ttk.Frame(self)
|
||||
main_frame.grid(row=0, column=0, sticky="nsew")
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
@@ -144,9 +146,7 @@ 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.encryption_button = ttk.Button(
|
||||
self.sidebar_buttons_frame, text="Encryption", command=lambda: self.navigation.toggle_encryption_frame(5), style="Sidebar.TButton")
|
||||
self.encryption_button.pack(fill=tk.X, pady=10)
|
||||
|
||||
|
||||
self.header_frame = HeaderFrame(self.content_frame, self.image_manager)
|
||||
self.header_frame.grid(row=0, column=0, sticky="nsew")
|
||||
@@ -222,7 +222,7 @@ class MainApplication(tk.Tk):
|
||||
self._setup_scheduler_frame()
|
||||
self._setup_settings_frame()
|
||||
self._setup_backup_content_frame()
|
||||
self._setup_encryption_frame()
|
||||
|
||||
|
||||
self._setup_task_bar()
|
||||
|
||||
@@ -339,6 +339,8 @@ class MainApplication(tk.Tk):
|
||||
self.destination_total_bytes = total
|
||||
self.destination_used_bytes = used
|
||||
|
||||
|
||||
|
||||
restore_src_path = self.config_manager.get_setting(
|
||||
"restore_source_path")
|
||||
if restore_src_path and os.path.isdir(restore_src_path):
|
||||
@@ -407,11 +409,7 @@ class MainApplication(tk.Tk):
|
||||
self.backup_content_frame.grid(row=2, column=0, sticky="nsew")
|
||||
self.backup_content_frame.grid_remove()
|
||||
|
||||
def _setup_encryption_frame(self):
|
||||
self.encryption_frame = EncryptionFrame(
|
||||
self.content_frame, self.backup_manager.encryption_manager, padding=10)
|
||||
self.encryption_frame.grid(row=2, column=0, sticky="nsew")
|
||||
self.encryption_frame.grid_remove()
|
||||
|
||||
|
||||
def _setup_task_bar(self):
|
||||
# Define all boolean vars at the top to ensure they exist before use.
|
||||
@@ -514,6 +512,8 @@ class MainApplication(tk.Tk):
|
||||
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)
|
||||
|
||||
|
||||
|
||||
def on_closing(self):
|
||||
"""Handles window closing events and saves the app state."""
|
||||
self.config_manager.set_setting("last_mode", self.mode)
|
||||
@@ -769,6 +769,8 @@ class MainApplication(tk.Tk):
|
||||
self.encrypted_var.set(True)
|
||||
self.encrypted_cb.config(state="disabled")
|
||||
|
||||
self.actions._refresh_backup_options_ui()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
@@ -22,34 +22,17 @@ class Actions:
|
||||
if backup_type == "full":
|
||||
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")
|
||||
elif backup_type == "incremental":
|
||||
self.app.vollbackup_var.set(False)
|
||||
self.app.inkrementell_var.set(True)
|
||||
self.app.full_backup_cb.config(state="normal")
|
||||
self.app.incremental_cb.config(state="normal")
|
||||
self.app.accurate_size_cb.config(state="normal")
|
||||
|
||||
def _update_backup_type_controls(self):
|
||||
"""
|
||||
Updates the state of the Full/Incremental backup radio buttons based on
|
||||
advanced settings and the content of the destination folder.
|
||||
This logic only applies to 'Computer' backups.
|
||||
"""
|
||||
# Do nothing if the backup mode is not 'backup' or source is not 'Computer'
|
||||
if self.app.mode != 'backup' or self.app.left_canvas_data.get('folder') != "Computer":
|
||||
self.app.full_backup_cb.config(state="normal")
|
||||
self.app.incremental_cb.config(state="normal")
|
||||
return
|
||||
|
||||
# Respect that advanced settings might have already disabled the controls
|
||||
# This check is based on the user's confirmation that this logic exists elsewhere
|
||||
if self.app.full_backup_cb.cget('state') == 'disabled':
|
||||
return
|
||||
|
||||
# --- Standard Logic ---
|
||||
full_backup_exists = False
|
||||
if self.app.destination_path and os.path.isdir(self.app.destination_path):
|
||||
system_backups = self.app.backup_manager.list_system_backups(
|
||||
@@ -64,6 +47,33 @@ class Actions:
|
||||
else:
|
||||
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")
|
||||
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.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()
|
||||
|
||||
def handle_backup_type_change(self, changed_var_name):
|
||||
if changed_var_name == 'voll':
|
||||
if self.app.vollbackup_var.get():
|
||||
@@ -73,61 +83,26 @@ class Actions:
|
||||
self._set_backup_type("incremental")
|
||||
|
||||
def handle_compression_change(self):
|
||||
if self.app.compressed_var.get():
|
||||
# Compression is enabled, force 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")
|
||||
# Also disable accurate incremental size calculation, as it's irrelevant
|
||||
self.app.accurate_size_cb.config(state="disabled")
|
||||
self.app.genaue_berechnung_var.set(False)
|
||||
else:
|
||||
# Compression is disabled, restore normal logic
|
||||
self.app.full_backup_cb.config(state="normal")
|
||||
# The state of the incremental checkbox depends on whether a full backup exists
|
||||
self._update_backup_type_controls()
|
||||
self._refresh_backup_options_ui()
|
||||
|
||||
def handle_encryption_change(self):
|
||||
if self.app.encrypted_var.get():
|
||||
# Encryption is enabled, force full backup and disable compression
|
||||
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.compressed_var.set(False)
|
||||
self.app.compressed_cb.config(state="disabled")
|
||||
self.app.accurate_size_cb.config(state="disabled")
|
||||
self.app.genaue_berechnung_var.set(False)
|
||||
else:
|
||||
# Encryption is disabled, restore normal logic
|
||||
self.app.full_backup_cb.config(state="normal")
|
||||
self.app.compressed_cb.config(state="normal")
|
||||
self._update_backup_type_controls()
|
||||
self._refresh_backup_options_ui()
|
||||
|
||||
def on_toggle_accurate_size_calc(self):
|
||||
# This method is called when the user clicks the "Genaue inkrem. Größe" checkbox.
|
||||
if not self.app.genaue_berechnung_var.get():
|
||||
# Box was unchecked by user, but this shouldn't happen if disabled.
|
||||
# Or it was unchecked automatically after a run. Do nothing.
|
||||
return
|
||||
|
||||
# If a normal calculation is running, stop it.
|
||||
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.")
|
||||
|
||||
# --- Start the accurate calculation ---
|
||||
app_logger.log("Accurate incremental size calculation requested.")
|
||||
self.app.accurate_calculation_running = True
|
||||
# Lock the UI, keep Cancel enabled
|
||||
self._set_ui_state(False, keep_cancel_enabled=True)
|
||||
self.app.start_pause_button.config(text=Msg.STR["cancel_backup"])
|
||||
|
||||
# Immediately clear the projection canvases
|
||||
self.app.drawing.reset_projection_canvases()
|
||||
|
||||
# Update canvas to show it's calculating and start animation
|
||||
self.app.info_label.config(
|
||||
text=Msg.STR["please_wait"], foreground="#0078d7")
|
||||
self.app.task_progress.config(mode="indeterminate")
|
||||
@@ -139,21 +114,20 @@ class Actions:
|
||||
self.app.drawing.start_backup_calculation_display()
|
||||
self.app.animated_icon.start()
|
||||
|
||||
# Get folder path from the current canvas data
|
||||
folder_path = self.app.left_canvas_data.get('path_display')
|
||||
button_text = self.app.left_canvas_data.get('folder')
|
||||
|
||||
if not folder_path or not button_text:
|
||||
app_logger.log(
|
||||
"Cannot start accurate calculation, source folder info missing.")
|
||||
self._set_ui_state(True) # Unlock UI
|
||||
self.app.genaue_berechnung_var.set(False) # Uncheck the box
|
||||
self._set_ui_state(True)
|
||||
self.app.genaue_berechnung_var.set(False)
|
||||
self.app.accurate_calculation_running = False
|
||||
self.app.animated_icon.stop("DISABLE")
|
||||
return
|
||||
|
||||
def threaded_incremental_calc():
|
||||
status = 'failure' # Default to failure
|
||||
status = 'failure'
|
||||
size = 0
|
||||
try:
|
||||
exclude_file_paths = []
|
||||
@@ -176,15 +150,12 @@ class Actions:
|
||||
is_system=True,
|
||||
exclude_files=exclude_file_paths
|
||||
)
|
||||
# If rsync fails, size is 0, so status will be failure.
|
||||
status = 'success' if size > 0 else 'failure'
|
||||
except Exception as e:
|
||||
app_logger.log(f"Error during threaded_incremental_calc: {e}")
|
||||
status = 'failure'
|
||||
finally:
|
||||
# This message MUST be sent to unlock the UI and update the display.
|
||||
# It is now handled by the main data_processing queue.
|
||||
if self.app.accurate_calculation_running: # Only post if not cancelled
|
||||
if self.app.accurate_calculation_running:
|
||||
self.app.queue.put(
|
||||
(button_text, size, self.app.mode, 'accurate_incremental', status))
|
||||
|
||||
@@ -206,7 +177,6 @@ class Actions:
|
||||
|
||||
self.app.log_window.clear_log()
|
||||
|
||||
# Reverse map from translated UI string to canonical key
|
||||
REVERSE_FOLDER_MAP = {
|
||||
"Computer": "Computer",
|
||||
Msg.STR["cat_documents"]: "Documents",
|
||||
@@ -235,28 +205,24 @@ class Actions:
|
||||
|
||||
icon_name = self.app.buttons_map[button_text]['icon']
|
||||
|
||||
# Determine the correct description based on mode and selection
|
||||
extra_info = ""
|
||||
if button_text == "Computer":
|
||||
if self.app.mode == 'backup':
|
||||
extra_info = Msg.STR["system_backup_info"]
|
||||
else: # restore
|
||||
else:
|
||||
extra_info = Msg.STR["system_restore_info"]
|
||||
else: # User folder
|
||||
else:
|
||||
if self.app.mode == 'backup':
|
||||
extra_info = Msg.STR["user_backup_info"]
|
||||
else: # restore
|
||||
else:
|
||||
extra_info = Msg.STR["user_restore_info"]
|
||||
|
||||
# CORRECTED LOGIC ORDER:
|
||||
# 1. Update backup type checkboxes BEFORE starting the calculation.
|
||||
if self.app.mode == 'backup':
|
||||
self._update_backup_type_controls()
|
||||
else: # restore mode
|
||||
else:
|
||||
self.app.config_manager.set_setting(
|
||||
"restore_destination_path", folder_path)
|
||||
|
||||
# 2. Unified logic for starting a calculation on the left canvas.
|
||||
self._start_left_canvas_calculation(
|
||||
button_text, str(folder_path), icon_name, extra_info)
|
||||
|
||||
@@ -303,8 +269,6 @@ class Actions:
|
||||
|
||||
self.app.calculation_stop_event = threading.Event()
|
||||
|
||||
# For system backup, now we default to the simple, fast (but for incremental inaccurate) full size calculation.
|
||||
# The accurate calculation is now triggered explicitly by the new checkbox.
|
||||
if button_text == "Computer":
|
||||
app_logger.log(
|
||||
"Using default (full) size calculation for system backup display.")
|
||||
@@ -313,7 +277,6 @@ class Actions:
|
||||
args = (folder_path, button_text, self.app.calculation_stop_event,
|
||||
exclude_patterns, self.app.mode)
|
||||
else:
|
||||
# For user folders, do not use any exclusions
|
||||
target_method = self.app.data_processing.get_user_folder_size_threaded
|
||||
args = (folder_path, button_text,
|
||||
self.app.calculation_stop_event, self.app.mode)
|
||||
@@ -325,7 +288,7 @@ class Actions:
|
||||
|
||||
if self.app.mode == 'backup':
|
||||
self._update_backup_type_controls()
|
||||
else: # restore mode
|
||||
else:
|
||||
self.app.config_manager.set_setting(
|
||||
"restore_destination_path", folder_path)
|
||||
|
||||
@@ -416,7 +379,6 @@ class Actions:
|
||||
self.app.config_manager.set_setting("restore_source_path", None)
|
||||
self.app.config_manager.set_setting("restore_destination_path", None)
|
||||
|
||||
# Remove advanced settings
|
||||
self.app.config_manager.remove_setting("backup_animation_type")
|
||||
self.app.config_manager.remove_setting("calculation_animation_type")
|
||||
self.app.config_manager.remove_setting("force_full_backup")
|
||||
@@ -424,7 +386,6 @@ class Actions:
|
||||
self.app.config_manager.remove_setting("force_compression")
|
||||
self.app.config_manager.remove_setting("force_encryption")
|
||||
|
||||
# Update the main UI to reflect the cleared settings
|
||||
self.app.update_backup_options_from_config()
|
||||
|
||||
AppConfig.generate_and_write_final_exclude_list()
|
||||
@@ -437,7 +398,6 @@ class Actions:
|
||||
self.app.destination_path = None
|
||||
self.app.start_pause_button.config(state="disabled")
|
||||
|
||||
# Clear the canvases and reset the UI to its initial state for the current mode
|
||||
self.app.backup_left_canvas_data.clear()
|
||||
self.app.backup_right_canvas_data.clear()
|
||||
self.app.restore_left_canvas_data.clear()
|
||||
@@ -455,13 +415,12 @@ class Actions:
|
||||
title=Msg.STR["settings_reset_title"], text=Msg.STR["settings_reset_text"])
|
||||
|
||||
def _parse_size_string_to_bytes(self, size_str: str) -> int:
|
||||
"""Parses a size string like '38.61 GB' into bytes."""
|
||||
if not size_str or size_str == Msg.STR["calculating_size"]:
|
||||
return 0
|
||||
|
||||
parts = size_str.split()
|
||||
if len(parts) != 2:
|
||||
return 0 # Invalid format
|
||||
return 0
|
||||
|
||||
try:
|
||||
value = float(parts[0])
|
||||
@@ -483,33 +442,24 @@ class Actions:
|
||||
return 0
|
||||
|
||||
def _set_ui_state(self, enable: bool, keep_cancel_enabled: bool = False, allow_log_and_backup_toggle: bool = False):
|
||||
# Sidebar Buttons
|
||||
for text, data in self.app.buttons_map.items():
|
||||
# Find the actual button widget in the sidebar_buttons_frame
|
||||
# This assumes the order of creation is consistent or we can identify by text
|
||||
# A more robust way would be to store references to the buttons in a dict in MainApplication
|
||||
# For now, let's iterate through children and match text
|
||||
for child in self.app.sidebar_buttons_frame.winfo_children():
|
||||
if isinstance(child, tk.ttk.Button) and child.cget("text") == text:
|
||||
child.config(state="normal" if enable else "disabled")
|
||||
break
|
||||
|
||||
# Schedule and Settings buttons in sidebar
|
||||
self.app.schedule_dialog_button.config(
|
||||
state="normal" if enable else "disabled")
|
||||
self.app.settings_button.config(
|
||||
state="normal" if enable else "disabled")
|
||||
|
||||
# Mode Button (arrow between canvases)
|
||||
self.app.mode_button.config(state="normal" if enable else "disabled")
|
||||
|
||||
# Top Navigation Buttons
|
||||
for i, button in enumerate(self.app.nav_buttons):
|
||||
if allow_log_and_backup_toggle and self.app.nav_buttons_defs[i][0] in [Msg.STR["log"], Msg.STR["backup_menu"]]:
|
||||
continue
|
||||
button.config(state="normal" if enable else "disabled")
|
||||
|
||||
# Right Canvas (Destination/Restore Source)
|
||||
if enable:
|
||||
self.app.right_canvas.bind(
|
||||
"<Button-1>", self.app.actions.on_right_canvas_click)
|
||||
@@ -518,14 +468,10 @@ class Actions:
|
||||
self.app.right_canvas.unbind("<Button-1>")
|
||||
self.app.right_canvas.config(cursor="")
|
||||
|
||||
# Checkboxes in the task bar
|
||||
if enable:
|
||||
# When enabling, re-run the logic that sets the correct state
|
||||
# for all checkboxes based on config and context.
|
||||
self.app.update_backup_options_from_config()
|
||||
self.app.actions._update_backup_type_controls()
|
||||
else:
|
||||
# When disabling, just disable all of them.
|
||||
checkboxes = [
|
||||
self.app.full_backup_cb,
|
||||
self.app.incremental_cb,
|
||||
@@ -538,35 +484,29 @@ class Actions:
|
||||
for cb in checkboxes:
|
||||
cb.config(state="disabled")
|
||||
|
||||
# Special handling for the cancel button during accurate calculation
|
||||
if keep_cancel_enabled:
|
||||
self.app.start_pause_button.config(state="normal")
|
||||
|
||||
def toggle_start_cancel(self):
|
||||
# If an accurate calculation is running, cancel it.
|
||||
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)
|
||||
|
||||
# Stop all animations and restore UI
|
||||
self.app.animated_icon.stop("DISABLE")
|
||||
if self.app.left_canvas_animation:
|
||||
self.app.left_canvas_animation.stop()
|
||||
self.app.left_canvas_data['calculating'] = False
|
||||
self.app.drawing.redraw_left_canvas()
|
||||
|
||||
# Restore bottom action bar
|
||||
self.app.task_progress.stop()
|
||||
self.app.task_progress.config(mode="determinate", value=0)
|
||||
self.app.info_label.config(
|
||||
# Orange for cancelled
|
||||
text=Msg.STR["accurate_calc_cancelled"], foreground="#E8740C")
|
||||
self.app.start_pause_button.config(text=Msg.STR["start"])
|
||||
self._set_ui_state(True)
|
||||
return
|
||||
|
||||
# If a backup is already running, we must be cancelling.
|
||||
if self.app.backup_is_running:
|
||||
self.app.animated_icon.stop("DISABLE")
|
||||
|
||||
@@ -612,27 +552,23 @@ class Actions:
|
||||
if hasattr(self.app, 'current_backup_path'):
|
||||
self.app.current_backup_path = None
|
||||
|
||||
# Reset state
|
||||
self.app.backup_is_running = False
|
||||
self.app.start_pause_button["text"] = Msg.STR["start"]
|
||||
self._set_ui_state(True)
|
||||
|
||||
# Otherwise, we are starting a new backup.
|
||||
else:
|
||||
if self.app.start_pause_button['state'] == 'disabled':
|
||||
return
|
||||
|
||||
self.app.backup_is_running = True
|
||||
|
||||
# --- Record and Display Start Time ---
|
||||
self.app.start_time = datetime.datetime.now()
|
||||
start_str = self.app.start_time.strftime("%H:%M:%S")
|
||||
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_duration() # Start the continuous duration update
|
||||
# --- End Time Logic ---
|
||||
self.app._update_duration()
|
||||
|
||||
self.app.start_pause_button["text"] = Msg.STR["cancel_backup"]
|
||||
self.app.update_idletasks()
|
||||
@@ -664,6 +600,18 @@ class Actions:
|
||||
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:
|
||||
@@ -675,7 +623,6 @@ class Actions:
|
||||
time_str = now.strftime("%H%M%S")
|
||||
folder_name = f"{date_str}_{time_str}_system_{mode}"
|
||||
final_dest = os.path.join(base_dest, "pybackup", folder_name)
|
||||
# Store the path for potential deletion
|
||||
self.app.current_backup_path = final_dest
|
||||
|
||||
source_size_bytes = self.app.left_canvas_data.get('total_bytes', 0)
|
||||
@@ -690,7 +637,6 @@ class Actions:
|
||||
|
||||
is_dry_run = self.app.testlauf_var.get()
|
||||
is_compressed = self.app.compressed_var.get()
|
||||
is_encrypted = self.app.encrypted_var.get()
|
||||
|
||||
self.app.backup_manager.start_backup(
|
||||
queue=self.app.queue,
|
||||
@@ -702,7 +648,8 @@ class Actions:
|
||||
source_size=source_size_bytes,
|
||||
is_compressed=is_compressed,
|
||||
is_encrypted=is_encrypted,
|
||||
mode=mode)
|
||||
mode=mode,
|
||||
password=password)
|
||||
|
||||
def _start_user_backup(self, sources):
|
||||
dest = self.app.destination_path
|
||||
@@ -718,4 +665,4 @@ class Actions:
|
||||
source_path=source,
|
||||
dest_path=dest,
|
||||
is_system=False,
|
||||
is_dry_run=is_dry_run)
|
||||
is_dry_run=is_dry_run)
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from shared_libs.message import MessageDialog
|
||||
@@ -6,9 +5,11 @@ import keyring
|
||||
|
||||
|
||||
class EncryptionFrame(ttk.Frame):
|
||||
def __init__(self, parent, encryption_manager, **kwargs):
|
||||
def __init__(self, parent, app, encryption_manager, **kwargs):
|
||||
super().__init__(parent, **kwargs)
|
||||
self.app = app
|
||||
self.encryption_manager = encryption_manager
|
||||
self.username = None
|
||||
|
||||
self.columnconfigure(0, weight=1)
|
||||
|
||||
@@ -19,6 +20,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.check_keyring_availability()
|
||||
|
||||
# Password section
|
||||
@@ -48,6 +53,10 @@ class EncryptionFrame(ttk.Frame):
|
||||
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)
|
||||
|
||||
def set_context(self, username):
|
||||
self.username = username
|
||||
self.update_keyring_status()
|
||||
|
||||
def check_keyring_availability(self):
|
||||
try:
|
||||
kr = keyring.get_keyring()
|
||||
@@ -55,6 +64,9 @@ class EncryptionFrame(ttk.Frame):
|
||||
self.keyring_status_label.config(
|
||||
text="No system keyring found. Passwords will not be saved.", foreground="orange")
|
||||
self.save_to_keyring_cb.config(state="disabled")
|
||||
else:
|
||||
self.keyring_status_label.config(
|
||||
text="System keyring is available.", foreground="green")
|
||||
except keyring.errors.NoKeyringError:
|
||||
self.keyring_status_label.config(
|
||||
text="No system keyring found. Passwords will not be saved.", foreground="orange")
|
||||
@@ -67,9 +79,35 @@ class EncryptionFrame(ttk.Frame):
|
||||
return
|
||||
|
||||
self.encryption_manager.set_session_password(password, self.save_to_keyring_var.get())
|
||||
self.status_message_label.config(text="Password set for this session.", foreground="green")
|
||||
|
||||
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")
|
||||
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.update_keyring_status()
|
||||
else:
|
||||
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")
|
||||
|
||||
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
else:
|
||||
self.keyring_usage_label.config(text=f'No password for "{self.username}" found in the keyring.', foreground="orange")
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ class Navigation:
|
||||
self.app.scheduler_frame.hide()
|
||||
self.app.settings_frame.hide()
|
||||
self.app.backup_content_frame.hide()
|
||||
self.app.encryption_frame.grid_remove()
|
||||
|
||||
|
||||
# Show the main content frames
|
||||
self.app.canvas_frame.grid()
|
||||
@@ -187,7 +187,7 @@ class Navigation:
|
||||
self.app.scheduler_frame.hide()
|
||||
self.app.settings_frame.hide()
|
||||
self.app.backup_content_frame.hide()
|
||||
self.app.encryption_frame.grid_remove()
|
||||
|
||||
self.app.canvas_frame.grid()
|
||||
self.app.source_size_frame.grid()
|
||||
self.app.target_size_frame.grid()
|
||||
@@ -242,7 +242,7 @@ class Navigation:
|
||||
self.app.log_frame.grid_remove()
|
||||
self.app.settings_frame.hide()
|
||||
self.app.backup_content_frame.hide()
|
||||
self.app.encryption_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()
|
||||
@@ -260,7 +260,7 @@ class Navigation:
|
||||
self.app.log_frame.grid_remove()
|
||||
self.app.backup_content_frame.hide()
|
||||
self.app.scheduler_frame.hide()
|
||||
self.app.encryption_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()
|
||||
@@ -284,7 +284,7 @@ class Navigation:
|
||||
self.app.log_frame.grid_remove()
|
||||
self.app.scheduler_frame.hide()
|
||||
self.app.settings_frame.hide()
|
||||
self.app.encryption_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()
|
||||
@@ -293,19 +293,4 @@ class Navigation:
|
||||
self.app.top_bar.grid()
|
||||
self._update_task_bar_visibility("scheduler")
|
||||
|
||||
def toggle_encryption_frame(self, active_index=None):
|
||||
self._cancel_calculation()
|
||||
self.app.drawing.update_nav_buttons(-1)
|
||||
|
||||
self.app.canvas_frame.grid_remove()
|
||||
self.app.log_frame.grid_remove()
|
||||
self.app.scheduler_frame.hide()
|
||||
self.app.settings_frame.hide()
|
||||
self.app.backup_content_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.encryption_frame.grid()
|
||||
self.app.top_bar.grid()
|
||||
self._update_task_bar_visibility("settings")
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
|
||||
@@ -8,6 +7,7 @@ class PasswordDialog(tk.Toplevel):
|
||||
self.title(title)
|
||||
self.parent = parent
|
||||
self.password = None
|
||||
self.save_to_keyring = tk.BooleanVar()
|
||||
self.confirm = confirm
|
||||
|
||||
self.transient(parent)
|
||||
@@ -23,6 +23,9 @@ class PasswordDialog(tk.Toplevel):
|
||||
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)
|
||||
|
||||
@@ -57,4 +60,4 @@ class PasswordDialog(tk.Toplevel):
|
||||
self.destroy()
|
||||
|
||||
def get_password(self):
|
||||
return self.password
|
||||
return self.password, self.save_to_keyring.get()
|
||||
Reference in New Issue
Block a user