From fd6bb6cc1b3bdfbdd0d428d2ebe6e94e4ee0be14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A9sir=C3=A9=20Werner=20Menrath?= Date: Tue, 9 Sep 2025 19:04:45 +0200 Subject: [PATCH] feat: Implement backup option logic and UI improvements This commit introduces the following changes: - Enhanced Backup Option Logic: - Implemented mutual exclusivity between 'Compressed' and 'Incremental' backups: - If 'Compressed' is selected, 'Incremental' is deselected and disabled, and 'Full' is automatically selected. - Implemented mutual exclusivity between 'Compressed' and 'Encrypted' backups: - If 'Compressed' is selected, 'Encrypted' is deselected and disabled. - If 'Encrypted' is selected, 'Compressed' is deselected and disabled. - If 'Incremental' is selected, 'Compressed' is deselected and disabled. - These rules are applied consistently across the main backup window, advanced settings, and scheduler. - UI Improvements: - Added 'Full', 'Incremental', 'Compressed', and 'Encrypted' checkboxes to the scheduler view. - Adjusted the layout in the scheduler view to place 'Backup Options' next to 'Folders to back up'. - Added missing string definitions for new UI elements in core/pbp_app_config.py. - Refactoring: - Updated _refresh_backup_options_ui in pyimage_ui/actions.py to handle the new logic. - Modified _on_compression_toggle in pyimage_ui/advanced_settings_frame.py and _on_compression_toggle_scheduler in pyimage_ui/scheduler_frame.py to reflect the updated exclusivity rules. - Adjusted _save_job and _load_scheduled_jobs in pyimage_ui/scheduler_frame.py to include and parse the new backup options. - Updated _parse_job_comment in core/backup_manager.py to correctly parse new backup options from cron job comments. --- core/pbp_app_config.py | 4 ++ pyimage_ui/actions.py | 28 +++++---- pyimage_ui/advanced_settings_frame.py | 33 ++++++++--- pyimage_ui/scheduler_frame.py | 81 +++++++++++++++++++++++++-- 4 files changed, 120 insertions(+), 26 deletions(-) diff --git a/core/pbp_app_config.py b/core/pbp_app_config.py index cca6b51..c6d180c 100644 --- a/core/pbp_app_config.py +++ b/core/pbp_app_config.py @@ -255,6 +255,7 @@ class Msg: "log": _("Log"), "full_backup": _("Full backup"), "incremental": _("Incremental"), + "incremental_backup": _("Incremental backup"), # New "test_run": _("Test run"), "start": _("Start"), "cancel_backup": _("Cancel"), @@ -345,7 +346,9 @@ class Msg: "header_subtitle": _("Simple GUI for rsync"), "encrypted_backup_content": _("Encrypted Backups"), "compressed": _("Compressed"), + "compression": _("Compression"), # New "encrypted": _("Encrypted"), + "encryption": _("Encryption"), # New "bypass_security": _("Bypass security"), "comment": _("Kommentar"), "force_full_backup": _("Always force full backup"), @@ -364,4 +367,5 @@ class Msg: "automation_settings_title": _("Automation Settings"), # New "create_add_key_file": _("Create/Add Key File"), # New "key_file_not_created": _("Key file not created."), # New + "backup_options": _("Backup Options"), # New } diff --git a/pyimage_ui/actions.py b/pyimage_ui/actions.py index b38434b..0ba0d0f 100644 --- a/pyimage_ui/actions.py +++ b/pyimage_ui/actions.py @@ -68,26 +68,30 @@ class Actions: self._set_backup_type("full") def _refresh_backup_options_ui(self): + # Reset all to normal state first self.app.full_backup_cb.config(state="normal") self.app.incremental_cb.config(state="normal") self.app.compressed_cb.config(state="normal") self.app.encrypted_cb.config(state="normal") self.app.accurate_size_cb.config(state="normal") - if self.app.encrypted_var.get(): - self.app.compressed_var.set(False) + # Apply specific logic based on current states + if self.app.compressed_var.get(): + self.app.inkrementell_var.set(False) # If compressed, cannot be incremental + self.app.vollbackup_var.set(True) # Force full if compressed + self.app.incremental_cb.config(state="disabled") + self.app.encrypted_var.set(False) # If compressed, cannot be encrypted + self.app.encrypted_cb.config(state="disabled") + self.app.accurate_size_cb.config(state="disabled") # Accurate size not applicable for compressed + self.app.genaue_berechnung_var.set(False) + elif self.app.encrypted_var.get(): + self.app.compressed_var.set(False) # If encrypted, cannot be compressed + self.app.compressed_cb.config(state="disabled") + elif self.app.inkrementell_var.get(): + self.app.compressed_var.set(False) # If incremental, cannot be compressed self.app.compressed_cb.config(state="disabled") - if self.app.compressed_var.get(): - self.app.encrypted_var.set(False) - self.app.encrypted_cb.config(state="disabled") - self.app.vollbackup_var.set(True) - self.app.inkrementell_var.set(False) - self.app.full_backup_cb.config(state="disabled") - self.app.incremental_cb.config(state="disabled") - self.app.accurate_size_cb.config(state="disabled") - self.app.genaue_berechnung_var.set(False) - + # Update backup type controls (full/incremental) based on current state self._update_backup_type_controls() def handle_backup_type_change(self, changed_var_name): diff --git a/pyimage_ui/advanced_settings_frame.py b/pyimage_ui/advanced_settings_frame.py index 50a02da..255e2f4 100644 --- a/pyimage_ui/advanced_settings_frame.py +++ b/pyimage_ui/advanced_settings_frame.py @@ -125,14 +125,19 @@ class AdvancedSettingsFrame(ttk.Frame): self.use_trash_bin_var = tk.BooleanVar() self.no_trash_bin_var = tk.BooleanVar() - ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_full_backup"], variable=self.force_full_var, command=lambda: enforce_backup_type_exclusivity( - self.force_full_var, self.force_incremental_var, self.force_full_var.get())).pack(anchor=tk.W) - ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_incremental_backup"], variable=self.force_incremental_var, command=lambda: enforce_backup_type_exclusivity( - self.force_incremental_var, self.force_full_var, self.force_incremental_var.get())).pack(anchor=tk.W) - ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_compression"], variable=self.force_compression_var, command=lambda: enforce_backup_type_exclusivity( - self.force_compression_var, self.force_encryption_var, self.force_compression_var.get())).pack(anchor=tk.W) - ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_encryption"], variable=self.force_encryption_var, command=lambda: enforce_backup_type_exclusivity( - self.force_encryption_var, self.force_compression_var, self.force_encryption_var.get())).pack(anchor=tk.W) + self.full_backup_checkbutton = ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_full_backup"], variable=self.force_full_var, command=lambda: enforce_backup_type_exclusivity( + self.force_full_var, self.force_incremental_var, self.force_full_var.get())) + self.full_backup_checkbutton.pack(anchor=tk.W) + + self.incremental_checkbutton = ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_incremental_backup"], variable=self.force_incremental_var, command=lambda: enforce_backup_type_exclusivity( + self.force_incremental_var, self.force_full_var, self.force_incremental_var.get())) + self.incremental_checkbutton.pack(anchor=tk.W) + + self.compression_checkbutton = ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_compression"], variable=self.force_compression_var, command=self._on_compression_toggle) + self.compression_checkbutton.pack(anchor=tk.W) + + self.encryption_checkbutton = ttk.Checkbutton(self.backup_defaults_frame, text=Msg.STR["force_encryption"], variable=self.force_encryption_var) + self.encryption_checkbutton.pack(anchor=tk.W) ttk.Separator(self.backup_defaults_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10) @@ -196,6 +201,17 @@ class AdvancedSettingsFrame(ttk.Frame): self._switch_view(self.current_view_index) + def _on_compression_toggle(self): + if self.force_compression_var.get(): + self.force_incremental_var.set(False) + self.incremental_checkbutton.config(state="disabled") + + self.force_encryption_var.set(False) + self.encryption_checkbutton.config(state="disabled") + else: + self.incremental_checkbutton.config(state="normal") + self.encryption_checkbutton.config(state="normal") + def _handle_trash_checkbox_click(self, changed_var, other_var): enforce_backup_type_exclusivity(changed_var, other_var, changed_var.get()) self._on_trash_setting_change() @@ -341,6 +357,7 @@ class AdvancedSettingsFrame(ttk.Frame): self.config_manager.get_setting("use_trash_bin", False)) self.no_trash_bin_var.set( self.config_manager.get_setting("no_trash_bin", False)) + self._on_compression_toggle() def _load_animation_settings(self): backup_anim = self.config_manager.get_setting( diff --git a/pyimage_ui/scheduler_frame.py b/pyimage_ui/scheduler_frame.py index 1262cef..05c1e8f 100644 --- a/pyimage_ui/scheduler_frame.py +++ b/pyimage_ui/scheduler_frame.py @@ -5,6 +5,7 @@ import os from shared_libs.custom_file_dialog import CustomFileDialog from shared_libs.message import MessageDialog from core.pbp_app_config import Msg +from pyimage_ui.shared_logic import enforce_backup_type_exclusivity class SchedulerFrame(ttk.Frame): @@ -17,7 +18,7 @@ class SchedulerFrame(ttk.Frame): self, text=Msg.STR["scheduled_jobs"], padding=10) self.jobs_frame.pack(fill=tk.BOTH, expand=True) - columns = ("active", "type", "frequency", "destination", "sources") + columns = ("active", "type", "frequency", "destination", "sources", "options") self.jobs_tree = ttk.Treeview( self.jobs_frame, columns=columns, show="headings") self.jobs_tree.heading("active", text=Msg.STR["active"]) @@ -25,6 +26,7 @@ class SchedulerFrame(ttk.Frame): self.jobs_tree.heading("frequency", text=Msg.STR["frequency"]) self.jobs_tree.heading("destination", text=Msg.STR["destination"]) self.jobs_tree.heading("sources", text=Msg.STR["sources"]) + self.jobs_tree.heading("options", text=Msg.STR["backup_options"]) for col in columns: self.jobs_tree.column(col, width=100, anchor="center") self.jobs_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) @@ -59,13 +61,37 @@ class SchedulerFrame(ttk.Frame): ttk.Radiobutton(type_frame, text=Msg.STR["user_backup_menu"], variable=self.backup_type, value="user", command=self._toggle_user_sources).pack(anchor=tk.W) + # Container for source folders and backup options + source_options_container = ttk.Frame(self.add_job_frame) + source_options_container.pack(fill=tk.X, padx=5, pady=5) + source_options_container.columnconfigure(0, weight=1) + source_options_container.columnconfigure(1, weight=1) + self.user_sources_frame = ttk.LabelFrame( - self.add_job_frame, text=Msg.STR["source_folders"], padding=10) - self.user_sources_frame.pack(fill=tk.X, padx=5, pady=5) + source_options_container, text=Msg.STR["source_folders"], padding=10) + self.user_sources_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) for name, var in self.user_sources.items(): ttk.Checkbutton(self.user_sources_frame, text=name, variable=var).pack(anchor=tk.W) + options_frame = ttk.LabelFrame( + source_options_container, text=Msg.STR["backup_options"], padding=10) + options_frame.grid(row=0, column=1, sticky="nsew", padx=5, pady=5) + + self.full_var = tk.BooleanVar(value=True) + self.incremental_var = tk.BooleanVar(value=False) + self.compress_var = tk.BooleanVar(value=False) + self.encrypt_var = tk.BooleanVar(value=False) + + self.full_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["full_backup"], variable=self.full_var, command=lambda: enforce_backup_type_exclusivity(self.full_var, self.incremental_var, self.full_var.get())) + self.full_checkbutton.pack(anchor=tk.W) + self.incremental_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["incremental_backup"], variable=self.incremental_var, command=lambda: enforce_backup_type_exclusivity(self.incremental_var, self.full_var, self.incremental_var.get())) + self.incremental_checkbutton.pack(anchor=tk.W) + self.compress_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["compression"], variable=self.compress_var, command=self._on_compression_toggle_scheduler) + self.compress_checkbutton.pack(anchor=tk.W) + self.encrypt_checkbutton = ttk.Checkbutton(options_frame, text=Msg.STR["encryption"], variable=self.encrypt_var) + self.encrypt_checkbutton.pack(anchor=tk.W) + dest_frame = ttk.LabelFrame( self.add_job_frame, text=Msg.STR["dest_folder"], padding=10) dest_frame.pack(fill=tk.X, padx=5, pady=5) @@ -104,13 +130,34 @@ class SchedulerFrame(ttk.Frame): def hide(self): self.grid_remove() + def _on_compression_toggle_scheduler(self): + if self.compress_var.get(): + self.incremental_var.set(False) + self.incremental_checkbutton.config(state="disabled") + + self.encrypt_var.set(False) + self.encrypt_checkbutton.config(state="disabled") + else: + self.incremental_checkbutton.config(state="normal") + self.encrypt_checkbutton.config(state="normal") + def _toggle_scheduler_view(self): if self.jobs_frame.winfo_ismapped(): self.jobs_frame.pack_forget() self.add_job_frame.pack(fill=tk.BOTH, expand=True) + # Reset or load default values for new job + self.full_var.set(True) + self.incremental_var.set(False) + self.compress_var.set(False) + self.encrypt_var.set(False) + self.destination.set("") + for var in self.user_sources.values(): + var.set(False) + self._on_compression_toggle_scheduler() # Update state of incremental checkbox else: self.add_job_frame.pack_forget() self.jobs_frame.pack(fill=tk.BOTH, expand=True) + self._load_scheduled_jobs() def _toggle_user_sources(self): state = "normal" if self.backup_type.get() == "user" else "disabled" @@ -144,17 +191,29 @@ class SchedulerFrame(ttk.Frame): return script_path = os.path.abspath(os.path.join( - os.path.dirname(__file__), "..", "main_app.py")) + os.path.dirname(__file__), "..", "pybackup-cli.py")) command = f"python3 {script_path} --backup-type {job_type} --destination \"{dest}\"" + + if self.full_var.get(): + command += " --full" + if self.incremental_var.get(): + command += " --incremental" + if self.compress_var.get(): + command += " --compress" + if self.encrypt_var.get(): + command += " --encrypt" + if job_type == "user": command += f" --sources " for s in job_sources: - command += f'\"{s}\" ' # This line has an issue with escaping + command += f'\"{s}\" ' comment = f"{self.backup_manager.app_tag}; type:{job_type}; freq:{job_frequency}; dest:{dest}" if job_type == "user": comment += f"; sources:{','.join(job_sources)}" + comment += f"; full:{self.full_var.get()}; incremental:{self.incremental_var.get()}; compress:{self.compress_var.get()}; encrypt:{self.encrypt_var.get()}" + job_details = { "command": command, "comment": comment, @@ -172,9 +231,19 @@ class SchedulerFrame(ttk.Frame): self.jobs_tree.delete(i) jobs = self.backup_manager.get_scheduled_jobs() for job in jobs: + options = [] + if job.get("full"): + options.append("Full") + if job.get("incremental"): + options.append("Incremental") + if job.get("compress"): + options.append("Compressed") + if job.get("encrypt"): + options.append("Encrypted") + self.jobs_tree.insert("", "end", values=( job["active"], job["type"], job["frequency"], job["destination"], ", ".join( - job["sources"]) + job["sources"]), ", ".join(options) ), iid=job["id"]) def _remove_selected_job(self):