Implements a new feature for creating compressed full backups and restoring from them. - Backups can now be created as compressed .tar.gz archives. - This option is only available for full backups to maintain the efficiency of incremental backups. - The UI now forces a full backup when compression is selected. - The backup list correctly identifies and labels compressed backups. - The restore process can handle both compressed and uncompressed backups. fix: Improve backup process feedback and reliability - Fixes a critical bug where backups could be overwritten if created on the same day. Backup names now include a timestamp to ensure uniqueness. - Improves UI feedback during compressed backups by showing distinct stages (transfer, compress) and using an indeterminate progress bar during the compression phase. - Disables the cancel button during the non-cancellable compression stage. - Fixes a bug where the incremental backup size was written to the info file for full backups. The correct total size is now used.
716 lines
32 KiB
Python
716 lines
32 KiB
Python
#!/usr/bin/python3
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
import os
|
|
import datetime
|
|
from queue import Queue, Empty
|
|
|
|
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.config_manager import ConfigManager
|
|
from backup_manager import BackupManager
|
|
from 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")
|
|
|
|
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.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.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.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() # Add this call
|
|
self.protocol("WM_DELETE_WINDOW", self.on_closing)
|
|
|
|
def _load_state_and_initialize(self):
|
|
"""Loads saved state from config and initializes the UI."""
|
|
last_mode = self.config_manager.get_setting("last_mode", "backup")
|
|
|
|
# Pre-load data from config before initializing the UI
|
|
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:
|
|
# Handle custom folder path
|
|
folder_name = os.path.basename(backup_source_path.rstrip('/'))
|
|
icon_name = 'folder_extralarge' # A generic folder icon
|
|
|
|
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 # Still needed for some logic
|
|
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
|
|
|
|
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):
|
|
# Find the corresponding button_text for the 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,
|
|
})
|
|
|
|
# Initialize UI for the last active mode
|
|
self.navigation.initialize_ui_for_mode(last_mode)
|
|
|
|
# Trigger calculations if needed
|
|
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':
|
|
# Trigger calculation for the right canvas (source) if a path is set
|
|
if restore_src_path:
|
|
self.drawing.calculate_restore_folder_size()
|
|
# Trigger calculation for the left canvas (destination) based on last selection
|
|
restore_dest_folder = self.restore_left_canvas_data.get(
|
|
'folder', 'Computer')
|
|
self.after(100, self.actions.on_sidebar_button_click,
|
|
restore_dest_folder)
|
|
self.data_processing.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, padding=10)
|
|
self.backup_content_frame.grid(row=2, column=0, sticky="nsew")
|
|
self.backup_content_frame.grid_remove()
|
|
|
|
def _setup_task_bar(self):
|
|
# Define all boolean vars at the top to ensure they exist before use.
|
|
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)
|
|
|
|
# Frame for time info
|
|
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.end_time_label = ttk.Label(
|
|
self.time_info_frame, text="Ende: --:--:--")
|
|
self.end_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)
|
|
|
|
# --- Accurate Size Calculation Frame (on the right) ---
|
|
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)
|
|
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):
|
|
"""Handles window closing events and saves the app state."""
|
|
self.config_manager.set_setting("last_mode", self.mode)
|
|
|
|
# Save paths from the data dictionaries
|
|
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)
|
|
|
|
# Stop any ongoing animations before destroying the application
|
|
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"])
|
|
self.destroy()
|
|
|
|
def _process_backup_queue(self):
|
|
"""Processes messages from the backup thread queue to update the UI safely."""
|
|
try:
|
|
while True:
|
|
message = self.queue.get_nowait()
|
|
|
|
if 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 == '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' # Default
|
|
if isinstance(status_info, dict):
|
|
status = status_info.get('status', 'error')
|
|
elif status_info is None: # Fallback for older logic
|
|
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':
|
|
# This is handled in actions.py, but we clean up here.
|
|
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()
|
|
duration = end_time - 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}"
|
|
end_str = end_time.strftime("%H:%M:%S")
|
|
self.end_time_label.config(text=f"Ende: {end_str}")
|
|
self.duration_label.config(
|
|
text=f"Dauer: {duration_str}")
|
|
self.start_time = None
|
|
|
|
self.backup_is_running = False
|
|
self.actions._set_ui_state(True)
|
|
elif message_type == 'completion_accurate':
|
|
if value == 'success':
|
|
self.info_label.config(
|
|
text=Msg.STR["accurate_size_success"])
|
|
else:
|
|
self.info_label.config(
|
|
text=Msg.STR["accurate_size_failed"])
|
|
self.actions._set_ui_state(True)
|
|
else:
|
|
self.queue.put(message)
|
|
break
|
|
|
|
except Empty:
|
|
pass
|
|
|
|
self.after(100, self._process_backup_queue)
|
|
|
|
def quit(self):
|
|
self.on_closing()
|
|
|
|
def update_backup_options_from_config(self):
|
|
"""
|
|
Reads the 'force' settings from the config and updates the main UI checkboxes.
|
|
A 'force' setting overrides the user's selection and disables the control.
|
|
"""
|
|
# Full/Incremental Logic
|
|
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")
|
|
|
|
# Compression Logic
|
|
force_compression = self.config_manager.get_setting(
|
|
"force_compression", False)
|
|
if force_compression:
|
|
self.compressed_var.set(True)
|
|
self.compressed_cb.config(state="disabled")
|
|
|
|
# Encryption Logic
|
|
force_encryption = self.config_manager.get_setting(
|
|
"force_encryption", False)
|
|
if force_encryption:
|
|
self.encrypted_var.set(True)
|
|
self.encrypted_cb.config(state="disabled")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
import sys
|
|
import shutil
|
|
|
|
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()
|