Files
Py-Backup/main_app.py
Désiré Werner Menrath e1b12227d0 feat: Improve encrypted backup management and UI feedback
This commit introduces significant improvements to how encrypted backups are handled,
focusing on user experience and system integration.

- Persistent Mounts: Encrypted backup containers now remain mounted across UI view changes,
  eliminating repeated password prompts when navigating the application. The container is
  automatically unmounted when the destination changes or the application closes.
- Key Management Fallback: The mounting process now intelligently falls back from
  keyring to keyfile, and finally to a user password prompt if previous methods fail.
- Enhanced UI Status: The header now provides detailed feedback on the encryption key
  status, indicating whether a key is available (via keyring or keyfile) and if the
  container is currently in use.
- Reduced pkexec Prompts: By keeping containers mounted, the number of system-level
  pkexec authentication prompts is drastically reduced, improving workflow.
- Bug Fixes:
    - Corrected a SyntaxError in encryption_manager.py related to string escaping.
    - Fixed an AttributeError in header_frame.py by restoring the is_key_in_keyring method.
    - Addressed a TclError on application shutdown by safely destroying Tkinter widgets.
2025-09-06 15:39:59 +02:00

789 lines
36 KiB
Python

#!/usr/bin/python3
import tkinter as tk
from tkinter import ttk
import os
import datetime
from queue import Queue, Empty
import shutil
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 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")
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.backup_manager = BackupManager(app_logger, self)
self.queue = Queue()
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.mode = "backup" # Default mode
self.backup_is_running = False
self.start_time = None
self.calculation_thread = None
self.calculation_stop_event = None
self.source_larger_than_partition = False
self.accurate_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.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_log_window()
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._load_state_and_initialize()
self.update_backup_options_from_config()
self.protocol("WM_DELETE_WINDOW", self.on_closing)
def _load_state_and_initialize(self):
self.log_window.clear_log()
last_mode = self.config_manager.get_setting("last_mode", "backup")
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
if hasattr(self, 'header_frame'):
self.header_frame.refresh_status()
container_path = os.path.join(backup_dest_path, "pybackup_encrypted.luks")
if os.path.exists(container_path):
username = os.path.basename(backup_dest_path.rstrip('/'))
password = self.backup_manager.encryption_manager.get_password_from_keyring(username)
if password:
self.backup_manager.encryption_manager.unlock_container(backup_dest_path, password)
app_logger.log("Automatically unlocked encrypted container.")
if hasattr(self, 'header_frame'):
self.header_frame.refresh_status()
restore_src_path = self.config_manager.get_setting(
"restore_source_path")
if restore_src_path and os.path.isdir(restore_src_path):
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()
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, padding=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.vollbackup_var = tk.BooleanVar()
self.inkrementell_var = tk.BooleanVar()
self.genaue_berechnung_var = tk.BooleanVar()
self.testlauf_var = tk.BooleanVar()
self.compressed_var = tk.BooleanVar()
self.encrypted_var = tk.BooleanVar()
self.bypass_security_var = tk.BooleanVar()
self.info_checkbox_frame = ttk.Frame(self.content_frame, padding=10)
self.info_checkbox_frame.grid(row=3, column=0, sticky="ew")
self.info_label = ttk.Label(
self.info_checkbox_frame, text=Msg.STR["info_text_placeholder"])
self.info_label.pack(anchor=tk.W, fill=tk.X, pady=5)
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)
accurate_size_frame = ttk.Frame(self.time_info_frame)
accurate_size_frame.pack(side=tk.LEFT, padx=20)
self.accurate_size_cb = ttk.Checkbutton(accurate_size_frame, text=Msg.STR["accurate_size_cb_label"],
variable=self.genaue_berechnung_var, command=self.actions.on_toggle_accurate_size_calc)
self.accurate_size_cb.pack(side=tk.LEFT, padx=5)
accurate_size_info_label = ttk.Label(
accurate_size_frame, text=Msg.STR["accurate_size_info_label"], foreground="gray")
accurate_size_info_label.pack(side=tk.LEFT)
checkbox_frame = ttk.Frame(self.info_checkbox_frame)
checkbox_frame.pack(fill=tk.X, pady=5)
self.full_backup_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["full_backup"],
variable=self.vollbackup_var, command=lambda: self.actions.handle_backup_type_change('voll'))
self.full_backup_cb.pack(side=tk.LEFT, padx=5)
self.incremental_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["incremental"],
variable=self.inkrementell_var, command=lambda: self.actions.handle_backup_type_change('inkrementell'))
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)
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)
self.encrypted_cb.pack(side=tk.LEFT, padx=5)
self.test_run_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["test_run"],
variable=self.testlauf_var)
self.test_run_cb.pack(side=tk.LEFT, padx=5)
self.bypass_security_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["bypass_security"],
variable=self.bypass_security_var)
self.bypass_security_cb.pack(side=tk.LEFT, padx=5)
self.action_frame = ttk.Frame(self.content_frame, padding=10)
self.action_frame.grid(row=6, column=0, sticky="ew")
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_pause_button = ttk.Button(
self.action_frame, text=Msg.STR["start"], command=self.actions.toggle_start_cancel, state="disabled")
self.start_pause_button.grid(row=0, column=2, rowspan=2, padx=5)
def on_closing(self):
self.backup_manager.encryption_manager.unmount_all()
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)
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
app_logger.log(Msg.STR["app_quit"])
try:
self.destroy()
except tk.TclError:
pass # App is already destroyed
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 == 'accurate_incremental':
self.actions._set_ui_state(True)
self.genaue_berechnung_var.set(False)
self.accurate_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 == 'accurate_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.genaue_berechnung_var.set(False)
self.accurate_calculation_running = False
self.start_pause_button.config(text=Msg.STR["start"])
if status == 'success':
self.info_label.config(
text=Msg.STR["accurate_size_success"], foreground="#0078d7")
self.current_file_label.config(text="")
else:
self.info_label.config(
text=Msg.STR["accurate_size_failed"], foreground="#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.info_label.config(text=f"Fortschritt: {value}%")
elif message_type == 'file_update':
max_len = 120
if len(value) > max_len:
value = "..." + value[-max_len:]
self.current_file_label.config(text=value)
elif message_type == 'status_update':
self.info_label.config(text=value)
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_pause_button.config(state=value)
elif message_type == 'deletion_complete':
self.actions._set_ui_state(True)
self.backup_content_frame.hide_deletion_status()
self.backup_content_frame.system_backups_frame._load_backup_content()
self.backup_content_frame.user_backups_frame._load_backup_content()
elif message_type == 'error':
self.animated_icon.stop("DISABLE")
self.start_pause_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 == 'success':
self.info_label.config(
text=Msg.STR["backup_finished_successfully"])
elif status == 'warning':
self.info_label.config(
text=Msg.STR["backup_finished_with_warnings"])
elif status == 'error':
self.info_label.config(
text=Msg.STR["backup_failed"])
elif status == 'cancelled':
pass
self.animated_icon.stop("DISABLE")
self.start_pause_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.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 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.vollbackup_var.set(True)
self.inkrementell_var.set(False)
self.full_backup_cb.config(state="disabled")
self.incremental_cb.config(state="disabled")
elif force_incremental:
self.vollbackup_var.set(False)
self.inkrementell_var.set(True)
self.full_backup_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()
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()