This commit introduces several UI enhancements and bug fixes based on user feedback. - **Backup Content View:** - The "Folder" column is now displayed before the "Comment" column for user backups. - The "Folder" column now shows the simple source name (e.g., "Dokumente") instead of the full backup name. - Column alignment and widths have been adjusted in both system and user backup views for better readability. - A timing issue causing progress bars to be missing after unlocking an encrypted volume has been resolved. - **Advanced Settings:** - New options have been added to control file deletion behavior (use trash or delete directly). - The layout of the advanced settings panel has been refined. - **Encryption:** - Introduces a keyfile-based authentication mechanism for LUKS containers, reducing the need for password prompts. - Replaces temporary script files with a dedicated runner script (`privileged_script_runner.sh`) for executing root commands, improving security and robustness.
825 lines
37 KiB
Python
825 lines
37 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()
|
|
self._update_sync_mode_display() # Call after loading state
|
|
|
|
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.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)
|
|
|
|
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()
|
|
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_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()
|
|
# 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()
|