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.
918 lines
42 KiB
Python
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()
|