Files
Py-Backup/main_app.py
Désiré Werner Menrath 9406d3f0e2 refactor(settings): Overhaul Reset and Keyfile UI/UX
This commit introduces a major refactoring of the settings and advanced settings panels to improve user experience and code structure.

Key Changes:

1.  **Reset Functionality Refactored:**
    -   The "Default Settings" logic has been moved from `actions.py` into `settings_frame.py`, where it belongs.
    -   Clicking "Reset" now opens a dedicated view with separate options for a "Default Reset" and a "Hard Reset", each with clear explanations.
    -   A global "Cancel" button was added for this new view, and the layout of the components has been centered and cleaned up.
    -   Error handling for the reset process has been improved to show feedback in the header.

2.  **Keyfile Creation UX Overhauled:**
    -   The "Automation Settings" (Keyfile) view in Advanced Settings is completely redesigned for clarity.
    -   It now explicitly displays the currently selected backup destination, so the user knows which container will be affected.
    -   A detailed description has been added to explain the purpose and prerequisites of creating a keyfile.
    -   The redundant "Apply" button is now hidden in this view.
    -   Feedback for keyfile creation (success or failure) is now shown as a temporary message in the header instead of a blocking dialog.
    -   Error messages are more informative, guiding the user on how to proceed (e.g., by creating an encrypted backup first).

3.  **String Externalization:**
    -   All new UI text for the keyfile and reset features has been added to `pbp_app_config.py` to support translation.
2025-09-15 00:46:47 +02:00

918 lines
42 KiB
Python

#!/usr/bin/python3
import tkinter as tk
from tkinter import ttk
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
from shared_libs.animated_icon import AnimatedIcon
from shared_libs.common_tools import IconManager
from shared_libs.message import MessageDialog
from core.config_manager import ConfigManager
from core.backup_manager import BackupManager
from core.pbp_app_config import AppConfig, Msg
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 core.data_processing import DataProcessing
from pyimage_ui.drawing import Drawing
from pyimage_ui.navigation import Navigation
from pyimage_ui.actions import Actions
class MainApplication(tk.Tk):
def __init__(self):
super().__init__()
AppConfig.ensure_directories()
self.title(AppConfig.UI_CONFIG["window_title"])
self.geometry("1000x800")
self.style = ttk.Style()
self.tk.call("source", "/usr/share/TK-Themes/water.tcl")
self.tk.call("set_theme", "light")
self.style.configure("Custom.TFrame", background="#2b3e4f")
self.style.configure("Sidebar.TButton", background="#2b3e4f",
foreground="white", font=("Ubuntu", 12, "bold"))
self.style.layout("Sidebar.TButton", self.style.layout(
"SidebarHover.TButton.Borderless.Round"))
self.style.map("Toolbutton", background=[
("active", "#000000")], foreground=[("active", "black")])
self.style.layout("Gray.Toolbutton",
self.style.layout("Toolbutton"))
self.style.configure("Gray.Toolbutton", foreground="gray")
self.style.configure("Green.Sidebar.TButton", foreground="green")
# Custom button styles for BackupContentFrame
self.style.configure(
"Success.TButton", foreground="#2E7D32") # Darker Green
self.style.configure(
"Danger.TButton", foreground="#C62828") # Darker Red
# Custom LabelFrame style for Mount frame
self.style.configure("Mount.TLabelFrame", bordercolor="#0078D7")
self.style.configure("Mount.TLabelFrame.Label", foreground="#0078D7")
# Custom button styles for BackupContentFrame
self.style.configure(
"Success.TButton", foreground="#2E7D32") # Darker Green
self.style.configure(
"Danger.TButton", foreground="#C62828") # Darker Red
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)
self.grid_columnconfigure(0, weight=1)
sidebar = ttk.Frame(main_frame, style="Custom.TFrame")
sidebar.grid(row=0, column=0, sticky="nsew")
main_frame.grid_rowconfigure(0, weight=1)
main_frame.grid_columnconfigure(0, weight=0, minsize=180)
main_frame.grid_columnconfigure(1, weight=1)
self.content_frame = ttk.Frame(main_frame)
self.content_frame.grid(row=0, column=1, sticky="nsew")
self.content_frame.grid_rowconfigure(0, weight=0)
self.content_frame.grid_rowconfigure(1, weight=0)
self.content_frame.grid_rowconfigure(2, weight=1)
self.content_frame.grid_rowconfigure(3, weight=0)
self.content_frame.grid_rowconfigure(4, weight=0)
self.content_frame.grid_rowconfigure(5, weight=0)
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)
self.data_processing = DataProcessing(self)
self.drawing = Drawing(self)
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.incremental_calculation_running = False
self.is_first_backup = False
self.left_canvas_animation = None
self.right_canvas_animation = None
self.right_calculation_thread = None
self.right_calculation_stop_event = None
self.destination_path = None
self.source_size_bytes = 0
self.destination_used_bytes = 0
self.destination_total_bytes = 0
self.backup_left_canvas_data = {}
self.backup_right_canvas_data = {}
self.restore_left_canvas_data = {}
self.restore_right_canvas_data = {}
self.left_canvas_data = {}
self.right_canvas_data = {}
self.restore_destination_folder_size_bytes = 0
self.restore_source_size_bytes = 0
try:
lx_backup_icon = self.image_manager.get_icon('lx_backup_large')
if lx_backup_icon:
self.iconphoto(True, lx_backup_icon)
except Exception:
pass
lx_backup_label = ttk.Label(
sidebar, image=lx_backup_icon, background="#2b3e4f")
lx_backup_label.image = lx_backup_icon
lx_backup_label.pack(pady=10)
self.sidebar_buttons_frame = ttk.Frame(sidebar, style="Custom.TFrame")
self.sidebar_buttons_frame.pack(pady=20)
self.buttons_map = {
"Computer": {"icon": "computer_extralarge"},
"Documents": {"icon": "documents_extralarge"},
"Pictures": {"icon": "pictures_extralarge"},
"Music": {"icon": "music_extralarge"},
"Videos": {"icon": "video_extralarge_folder"},
}
for text, data in self.buttons_map.items():
button = ttk.Button(self.sidebar_buttons_frame, text=text, style="Sidebar.TButton",
command=lambda t=text: self.actions.on_sidebar_button_click(t))
button.pack(fill=tk.X, pady=10)
self.schedule_dialog_button = ttk.Button(
self.sidebar_buttons_frame, text=Msg.STR["scheduling"], command=lambda: self.navigation.toggle_scheduler_frame(3), style="Sidebar.TButton")
self.schedule_dialog_button.pack(fill=tk.X, pady=10)
self.settings_button = ttk.Button(
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")
self.top_bar = ttk.Frame(self.content_frame)
self.top_bar.grid(row=1, column=0, sticky="ew", pady=10)
top_nav_frame = ttk.Frame(self.top_bar)
top_nav_frame.pack(side=tk.LEFT)
self.nav_buttons_defs = [
(Msg.STR["backup_menu"], lambda: self.navigation.toggle_mode(
"backup", 0, trigger_calculation=True)),
(Msg.STR["restore"], lambda: self.navigation.toggle_mode(
"restore", 1, trigger_calculation=True)),
(Msg.STR["backup_content"],
lambda: self.navigation.toggle_backup_content_frame(2)),
(Msg.STR["scheduling"],
lambda: self.navigation.toggle_scheduler_frame(3)),
(Msg.STR["settings"],
lambda: self.navigation.toggle_settings_frame(4)),
(Msg.STR["log"], lambda: self.navigation.toggle_log_window(5)),
]
self.nav_buttons = []
self.nav_progress_bars = []
for i, (text, command) in enumerate(self.nav_buttons_defs):
button_frame = ttk.Frame(top_nav_frame)
button_frame.pack(side=tk.LEFT, padx=5)
button = ttk.Button(button_frame, text=text,
command=command, style="TButton.Borderless.Round")
button.pack(side=tk.TOP)
self.nav_buttons.append(button)
progress_bar = ttk.Progressbar(
button_frame, orient="horizontal", length=50, mode="determinate", style="Small.Horizontal.TProgressbar")
progress_bar.pack_forget()
self.nav_progress_bars.append(progress_bar)
if i < len(self.nav_buttons_defs) - 1:
ttk.Separator(top_nav_frame, orient=tk.VERTICAL).pack(
side=tk.LEFT, fill=tk.Y, padx=2)
self.canvas_frame = ttk.Frame(self.content_frame)
self.canvas_frame.grid(
row=2, column=0, sticky="nsew", padx=10, pady=(15, 5))
self.canvas_frame.grid_columnconfigure(0, weight=1)
self.canvas_frame.grid_columnconfigure(2, weight=1)
self.canvas_frame.grid_rowconfigure(0, weight=1)
self.left_canvas = tk.Canvas(
self.canvas_frame, relief="solid", borderwidth=1)
self.left_canvas.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
self.left_canvas.bind("<Configure>", self.drawing.redraw_left_canvas)
button_frame = ttk.Frame(self.canvas_frame)
button_frame.grid(row=0, column=1, padx=15)
self.mode_button_icon = self.image_manager.get_icon(
"forward_extralarge")
self.mode_button = ttk.Button(
button_frame, image=self.mode_button_icon, command=self.navigation.toggle_mode, style="TButton.Borderless.Round")
self.mode_button.pack(pady=30)
self.right_canvas = tk.Canvas(
self.canvas_frame, relief="solid", borderwidth=1)
self.right_canvas.grid(row=0, column=2, sticky="nsew", padx=5, pady=5)
self.right_canvas.bind("<Configure>", self.drawing.redraw_right_canvas)
self.right_canvas.bind(
"<Button-1>", self.actions.on_right_canvas_click)
self._setup_scheduler_frame()
self._setup_settings_frame()
self._setup_backup_content_frame()
self._setup_task_bar()
self.source_size_frame = ttk.LabelFrame(
self.content_frame, text=Msg.STR["source"], padding=10)
self.source_size_frame.grid(
row=4, column=0, sticky="ew", padx=10, pady=5)
self.source_size_frame.grid_columnconfigure(0, weight=1)
self.source_size_canvas = tk.Canvas(
self.source_size_frame, height=20, relief="solid", borderwidth=1)
self.source_size_canvas.grid(row=0, column=0, sticky="ew")
source_label_frame = ttk.Frame(self.source_size_frame)
source_label_frame.grid(row=1, column=0, sticky="ew")
self.source_size_label = ttk.Label(
source_label_frame, text="0.00 GB / 0.00 GB")
self.source_size_label.pack(side=tk.RIGHT)
self.target_size_frame = ttk.LabelFrame(
self.content_frame, text=Msg.STR["projected_usage_label"], padding=10)
self.target_size_frame.grid(
row=5, column=0, sticky="ew", padx=10, pady=5)
self.target_size_frame.grid_columnconfigure(0, weight=1)
self.target_size_canvas = tk.Canvas(
self.target_size_frame, height=20, relief="solid", borderwidth=1)
self.target_size_canvas.grid(row=0, column=0, sticky="ew")
target_label_frame = ttk.Frame(self.target_size_frame)
target_label_frame.grid(row=1, column=0, sticky="ew")
self.target_size_label = ttk.Label(
target_label_frame, text="0.00 GB / 0.00 GB")
self.target_size_label.pack(side=tk.RIGHT)
self.restore_size_frame_before = ttk.LabelFrame(
self.content_frame, text="Before Restoration", padding=10)
self.restore_size_frame_before.grid(
row=4, column=0, sticky="ew", padx=10, pady=5)
self.restore_size_frame_before.grid_columnconfigure(0, weight=1)
self.restore_size_canvas_before = tk.Canvas(
self.restore_size_frame_before, height=20, relief="solid", borderwidth=1)
self.restore_size_canvas_before.grid(row=0, column=0, sticky="ew")
restore_label_frame_before = ttk.Frame(self.restore_size_frame_before)
restore_label_frame_before.grid(row=1, column=0, sticky="ew")
self.restore_size_label_before = ttk.Label(
restore_label_frame_before, text="0.00 GB / 0.00 GB")
self.restore_size_label_before.pack(side=tk.RIGHT)
self.restore_size_frame_after = ttk.LabelFrame(
self.content_frame, text="After Restoration", padding=10)
self.restore_size_frame_after.grid(
row=5, column=0, sticky="ew", padx=10, pady=5)
self.restore_size_frame_after.grid_columnconfigure(0, weight=1)
self.restore_size_canvas_after = tk.Canvas(
self.restore_size_frame_after, height=20, relief="solid", borderwidth=1)
self.restore_size_canvas_after.grid(row=0, column=0, sticky="ew")
restore_label_frame_after = ttk.Frame(self.restore_size_frame_after)
restore_label_frame_after.grid(row=1, column=0, sticky="ew")
self.restore_size_label_after = ttk.Label(
restore_label_frame_after, text="0.00 GB / 0.00 GB")
self.restore_size_label_after.pack(side=tk.RIGHT)
self.restore_size_label_diff = ttk.Label(
restore_label_frame_after, text="")
self.restore_size_label_diff.pack(side=tk.RIGHT)
self.restore_size_frame_before.grid_remove()
self.restore_size_frame_after.grid_remove()
self._process_queue_id = None # Initialize the ID for the scheduled queue processing
self._load_state_and_initialize()
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")
if backup_source_path and os.path.isdir(backup_source_path):
folder_name = next((name for name, path_obj in AppConfig.FOLDER_PATHS.items(
) if str(path_obj) == backup_source_path), None)
if folder_name:
icon_name = self.buttons_map[folder_name]['icon']
else:
folder_name = os.path.basename(backup_source_path.rstrip('/'))
icon_name = 'folder_extralarge'
self.backup_left_canvas_data.update({
'icon': icon_name,
'folder': folder_name,
'path_display': backup_source_path,
})
backup_dest_path = self.config_manager.get_setting(
"backup_destination_path")
if backup_dest_path and os.path.isdir(backup_dest_path):
self.destination_path = backup_dest_path
total, used, free = shutil.disk_usage(backup_dest_path)
self.backup_right_canvas_data.update({
'folder': os.path.basename(backup_dest_path.rstrip('/')),
'path_display': backup_dest_path,
'size': f"{used / (1024**3):.2f} GB / {total / (1024**3):.2f} GB"
})
self.destination_total_bytes = total
self.destination_used_bytes = used
# Check for any pre-existing mounts for all known profiles
known_profiles = list(AppConfig.FOLDER_PATHS.keys()) + ["system"]
for profile_name in known_profiles:
if self.backup_manager.encryption_manager.is_mounted(backup_dest_path, profile_name):
app_logger.log(
f"Adopting pre-existing mount for profile {profile_name} at {backup_dest_path} into session.")
self.backup_manager.encryption_manager.mounted_destinations.add(
(backup_dest_path, profile_name))
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):
self.restore_right_canvas_data.update({
'folder': os.path.basename(restore_src_path.rstrip('/')),
'path_display': restore_src_path,
})
restore_dest_path = self.config_manager.get_setting(
"restore_destination_path")
if restore_dest_path and os.path.isdir(restore_dest_path):
folder_name = ""
for name, path_obj in AppConfig.FOLDER_PATHS.items():
if str(path_obj) == restore_dest_path:
folder_name = name
break
if folder_name:
self.restore_left_canvas_data.update({
'icon': self.buttons_map[folder_name]['icon'],
'folder': folder_name,
'path_display': restore_dest_path,
})
self.navigation.initialize_ui_for_mode(last_mode)
if last_mode == 'backup':
self.after(100, self.actions.on_sidebar_button_click,
self.backup_left_canvas_data.get('folder', 'Computer'))
elif last_mode == 'restore':
if restore_src_path:
self.drawing.calculate_restore_folder_size()
restore_dest_folder = self.restore_left_canvas_data.get(
'folder', 'Computer')
self.after(100, self.actions.on_sidebar_button_click,
restore_dest_folder)
self._process_queue()
self._update_sync_mode_display() # Call after loading state
self.update_backup_options_from_config() # Apply defaults on startup
def _setup_log_window(self):
self.log_frame = ttk.Frame(self.content_frame)
self.log_window = LogWindow(self.log_frame)
self.log_window.pack(fill=tk.BOTH, expand=True)
app_logger.init_logger(self.log_window.log_message)
self.log_frame.grid(row=2, column=0, sticky="nsew")
self.log_frame.grid_remove()
def _setup_scheduler_frame(self):
self.scheduler_frame = SchedulerFrame(
self.content_frame, self.backup_manager, padding=10)
self.scheduler_frame.grid(row=2, column=0, sticky="nsew")
self.scheduler_frame.grid_remove()
def _setup_settings_frame(self):
self.settings_frame = SettingsFrame(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()
def _setup_backup_content_frame(self):
self.backup_content_frame = BackupContentFrame(
self.content_frame, self.backup_manager, self.actions, self, padding=10)
self.backup_content_frame.grid(row=2, column=0, sticky="nsew")
self.backup_content_frame.grid_remove()
def _setup_task_bar(self):
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)
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(
self.info_checkbox_frame, text="", foreground="blue")
self.sync_mode_label.pack(anchor=tk.W, fill=tk.X, pady=2)
self.time_info_frame = ttk.Frame(self.info_checkbox_frame)
self.time_info_frame.pack(anchor=tk.W, fill=tk.X, pady=5)
self.start_time_label = ttk.Label(
self.time_info_frame, text="Start: --:--:--")
self.start_time_label.pack(side=tk.LEFT, padx=5)
self.duration_label = ttk.Label(
self.time_info_frame, text="Dauer: --:--:--")
self.duration_label.pack(side=tk.LEFT, padx=5)
self.end_time_label = ttk.Label(
self.time_info_frame, text="Ende: --:--:--")
self.end_time_label.pack(side=tk.LEFT, padx=5)
incremental_size_frame = ttk.Frame(self.time_info_frame)
incremental_size_frame.pack(side=tk.LEFT, padx=20)
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)
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.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.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, 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, style="Switch.TCheckbutton")
self.encrypted_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")
self.action_frame.grid_columnconfigure(1, weight=1)
bg_color = self.style.lookup('TFrame', 'background')
backup_animation_type = self.config_manager.get_setting(
"backup_animation_type", "counter_arc")
initial_animation_type = "blink"
if backup_animation_type == "line":
initial_animation_type = "line"
self.animated_icon = AnimatedIcon(
self.action_frame, width=20, height=20, use_pillow=True, bg=bg_color, animation_type=initial_animation_type)
self.animated_icon.grid(row=0, column=0, rowspan=2, padx=5)
self.animated_icon.stop("DISABLE")
self.animated_icon.animation_type = backup_animation_type
progress_container = ttk.Frame(self.action_frame)
progress_container.grid(row=0, column=1, sticky="ew", padx=5)
progress_container.grid_columnconfigure(0, weight=1)
self.current_file_label = ttk.Label(
progress_container, text="", anchor="w")
self.current_file_label.grid(row=0, column=0, sticky="ew")
self.task_progress = ttk.Progressbar(
progress_container, orient="horizontal", length=100, mode="determinate")
self.task_progress.grid(row=1, column=0, sticky="ew", pady=(5, 0))
self.start_cancel_button = ttk.Button(
self.action_frame, text=Msg.STR["start"], command=self.actions.toggle_start_cancel, state="disabled")
self.start_cancel_button.grid(row=0, column=2, rowspan=2, padx=5)
def on_closing(self):
# First, always attempt to unmount all encrypted drives
unmount_success = self.backup_manager.encryption_manager.unmount_all()
# If unmounting fails, show an error and prevent the app from closing
if not unmount_success and self.backup_manager.encryption_manager.mounted_destinations:
app_logger.log(
"WARNING: Not all encrypted drives could be unmounted. Preventing application closure.")
MessageDialog(
message_type="error", title=Msg.STR["unmount_failed_title"], text=Msg.STR["unmount_failed_message"]).show()
return # Prevent application from closing
self.config_manager.set_setting(
"refresh_log", self.refresh_log_var.get())
self.config_manager.set_setting("last_mode", self.mode)
if self.backup_left_canvas_data.get('path_display'):
self.config_manager.set_setting(
"backup_source_path", self.backup_left_canvas_data['path_display'])
else:
self.config_manager.set_setting("backup_source_path", None)
if self.backup_right_canvas_data.get('path_display'):
self.config_manager.set_setting(
"backup_destination_path", self.backup_right_canvas_data['path_display'])
else:
self.config_manager.set_setting("backup_destination_path", None)
if self.restore_left_canvas_data.get('path_display'):
self.config_manager.set_setting(
"restore_destination_path", self.restore_left_canvas_data['path_display'])
else:
self.config_manager.set_setting("restore_destination_path", None)
if self.restore_right_canvas_data.get('path_display'):
self.config_manager.set_setting(
"restore_source_path", self.restore_right_canvas_data['path_display'])
else:
self.config_manager.set_setting("restore_source_path", None)
# Finally, clean up UI resources and destroy the window
if self.left_canvas_animation:
self.left_canvas_animation.stop()
self.left_canvas_animation.destroy()
self.left_canvas_animation = None
if self.right_canvas_animation:
self.right_canvas_animation.stop()
self.right_canvas_animation.destroy()
self.right_canvas_animation = None
if self.animated_icon:
self.animated_icon.stop()
self.animated_icon.destroy()
self.animated_icon = None
if self._process_queue_id:
self.after_cancel(self._process_queue_id)
app_logger.log(Msg.STR["app_quit"])
try:
self.destroy()
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):
message = self.queue.get_nowait()
if isinstance(message, tuple) and len(message) in [3, 5]:
calc_type, status = None, None
if len(message) == 5:
button_text, folder_size, mode_when_started, calc_type, status = message
else:
button_text, folder_size, mode_when_started = message
if mode_when_started != self.mode:
if calc_type == 'incremental_incremental':
self.actions._set_ui_state(True)
self.incremental_calculation_running = False
self.animated_icon.stop("DISABLE")
else:
current_folder_name = self.left_canvas_data.get(
'folder')
if current_folder_name == button_text:
if self.left_canvas_animation:
self.left_canvas_animation.stop()
self.left_canvas_animation.destroy()
self.left_canvas_animation = None
size_in_gb = folder_size / (1024**3)
size_str = f"{size_in_gb:.2f} GB" if size_in_gb >= 1 else f"{folder_size / (1024*1024):.2f} MB"
self.left_canvas_data['size'] = size_str
self.left_canvas_data['total_bytes'] = folder_size
self.left_canvas_data['calculating'] = False
self.drawing.redraw_left_canvas()
self.source_size_bytes = folder_size
if self.mode == 'backup':
if button_text in AppConfig.FOLDER_PATHS:
total_disk_size, _, _ = shutil.disk_usage(
AppConfig.FOLDER_PATHS[button_text])
if folder_size > total_disk_size:
self.source_larger_than_partition = True
else:
self.source_larger_than_partition = False
percentage = (
folder_size / total_disk_size) * 100 if total_disk_size > 0 else 0
self.source_size_canvas.delete("all")
fill_width = (
self.source_size_canvas.winfo_width() / 100) * percentage
self.source_size_canvas.create_rectangle(
0, 0, fill_width, self.source_size_canvas.winfo_height(), fill="#0078d7", outline="")
self.source_size_label.config(
text=f"{folder_size / (1024**3):.2f} GB / {total_disk_size / (1024**3):.2f} GB")
self.drawing.update_target_projection()
if calc_type == 'incremental_incremental':
self.source_size_bytes = folder_size
self.drawing.update_target_projection()
self.animated_icon.stop("DISABLE")
self.task_progress.stop()
self.task_progress.config(
mode="determinate", value=0)
self.actions._set_ui_state(True)
self.incremental_calculation_running = False
self.start_cancel_button.config(
text=Msg.STR["start"])
if status == 'success':
self._update_info_label(
Msg.STR["incremental_size_success"], color="#0078d7")
self.current_file_label.config(text="")
else:
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:
message_type, value = message
if message_type == 'progress':
self.task_progress["value"] = 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._update_info_label(value)
elif message_type == 'progress_mode':
self.task_progress.config(mode=value)
if value == 'indeterminate':
self.task_progress.start()
else:
self.task_progress.stop()
elif message_type == 'cancel_button_state':
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()
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_cancel_button["text"] = "Start"
self.backup_is_running = False
elif message_type == 'completion':
status_info = value
status = 'error'
if isinstance(status_info, dict):
status = status_info.get('status', 'error')
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._update_info_label(
Msg.STR["backup_finished_successfully"])
elif status == 'warning':
self._update_info_label(
Msg.STR["backup_finished_with_warnings"])
elif status == 'error':
self._update_info_label(Msg.STR["backup_failed"])
elif status == 'cancelled':
self._update_info_label(Msg.STR["backup_mode"])
self.animated_icon.stop("DISABLE")
self.start_cancel_button["text"] = "Start"
self.task_progress["value"] = 0
self.current_file_label.config(text="")
if self.start_time:
end_time = datetime.datetime.now()
end_str = end_time.strftime("%H:%M:%S")
self.end_time_label.config(text=f"Ende: {end_str}")
self.start_time = None
self.backup_is_running = False
self.actions._set_ui_state(True)
self.backup_content_frame.system_backups_frame._load_backup_content()
elif message_type == 'completion_accurate':
pass
else:
app_logger.log(f"Unknown message in queue: {message}")
except Empty:
pass
finally:
self._process_queue_id = self.after(100, self._process_queue)
def _update_duration(self):
if self.backup_is_running and self.start_time:
duration = datetime.datetime.now() - self.start_time
total_seconds = int(duration.total_seconds())
hours, remainder = divmod(total_seconds, 3600)
minutes, seconds = divmod(remainder, 60)
duration_str = f"{hours:02}:{minutes:02}:{seconds:02}"
self.duration_label.config(text=f"Dauer: {duration_str}")
self.after(1000, self._update_duration)
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']} and profile {lock['profile_name']}. Attempting to close.")
self.backup_manager.encryption_manager.unmount_and_reset_owner(
lock['base_path'], lock['profile_name'], force_unmap=True)
if not stale_mounts_found:
app_logger.log("No stale mounts detected.")
if locks:
# If no stale mounts were found, the lock file should be empty.
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)
force_incremental = self.config_manager.get_setting(
"force_incremental_backup", False)
if force_full:
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.full_backup_var.set(False)
self.incremental_var.set(True)
self.full_backup_cb.config(state="disabled")
self.incremental_cb.config(state="disabled")
force_compression = self.config_manager.get_setting(
"force_compression", False)
if force_compression:
self.compressed_var.set(True)
self.compressed_cb.config(state="disabled")
force_encryption = self.config_manager.get_setting(
"force_encryption", False)
if force_encryption:
self.encrypted_var.set(True)
self.encrypted_cb.config(state="disabled")
self.actions._refresh_backup_options_ui()
# Update sync mode display after options are loaded
self._update_sync_mode_display()
def _update_sync_mode_display(self):
use_trash_bin = self.config_manager.get_setting("use_trash_bin", False)
no_trash_bin = self.config_manager.get_setting("no_trash_bin", False)
if self.left_canvas_data.get('folder') == "Computer":
# Not applicable for system backups
self.sync_mode_label.config(text="")
return
if no_trash_bin:
self.sync_mode_label.config(
text=Msg.STR["sync_mode_pure_sync"], foreground="red")
elif use_trash_bin:
self.sync_mode_label.config(
text=Msg.STR["sync_mode_trash_bin"], foreground="orange")
else:
self.sync_mode_label.config(
text=Msg.STR["sync_mode_no_delete"], foreground="green")
if __name__ == "__main__":
import argparse
import sys
parser = argparse.ArgumentParser(description="Py-Backup Application.")
parser.add_argument(
"--backup-type", choices=['user', 'system'], help="Type of backup to perform.")
parser.add_argument(
"--destination", help="Destination directory for the backup.")
parser.add_argument("--sources", nargs='+',
help="List of sources for user backup (e.g., Picture, Documents).")
parser.add_argument("--mode", choices=['full', 'incremental'],
default='incremental', help="Mode for system backup.")
args = parser.parse_args()
if args.backup_type and args.destination:
from shared_libs.logger import app_logger as cli_logger
backup_manager = BackupManager(cli_logger)
if args.backup_type == 'user':
if not args.sources:
print("Error: --sources are required for user backup.")
sys.exit(1)
backup_manager._run_user_backup(
args.sources, args.destination, None, None)
elif args.backup_type == 'system':
backup_manager._run_system_backup(
args.destination, args.mode, None, None)
sys.exit(0)
else:
app = MainApplication()
app.mainloop()