encrypt backups part one

This commit is contained in:
2025-09-04 01:49:56 +02:00
parent 158bc6ec97
commit adf78124a4
7 changed files with 154 additions and 179 deletions

View File

@@ -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):

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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")

View File

@@ -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")

View File

@@ -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()