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.
This commit is contained in:
2025-09-09 19:04:45 +02:00
parent e932dff8a6
commit fd6bb6cc1b
4 changed files with 120 additions and 26 deletions

View File

@@ -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
}

View File

@@ -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):

View File

@@ -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(

View File

@@ -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):