Files
Py-Backup/pyimage_ui/scheduler_frame.py
Désiré Werner Menrath fd6bb6cc1b 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.
2025-09-09 19:04:45 +02:00

259 lines
12 KiB
Python

import tkinter as tk
from tkinter import ttk
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):
def __init__(self, master, backup_manager, **kwargs):
super().__init__(master, **kwargs)
self.backup_manager = backup_manager
# --- Jobs List View ---
self.jobs_frame = ttk.LabelFrame(
self, text=Msg.STR["scheduled_jobs"], padding=10)
self.jobs_frame.pack(fill=tk.BOTH, expand=True)
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"])
self.jobs_tree.heading("type", text=Msg.STR["col_type"])
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)
self._load_scheduled_jobs()
list_button_frame = ttk.Frame(self.jobs_frame)
list_button_frame.pack(pady=10)
ttk.Button(list_button_frame, text=Msg.STR["add"],
command=self._toggle_scheduler_view).pack(side=tk.LEFT, padx=5)
ttk.Button(list_button_frame, text=Msg.STR["remove"],
command=self._remove_selected_job).pack(side=tk.LEFT, padx=5)
# --- Add Job View ---
self.add_job_frame = ttk.LabelFrame(
self, text=Msg.STR["add_job_title"], padding=10)
self.backup_type = tk.StringVar(value="system")
self.destination = tk.StringVar()
self.user_sources = {
Msg.STR["cat_images"]: tk.BooleanVar(value=False),
Msg.STR["cat_documents"]: tk.BooleanVar(value=False),
Msg.STR["cat_music"]: tk.BooleanVar(value=False),
Msg.STR["cat_videos"]: tk.BooleanVar(value=False)
}
self.frequency = tk.StringVar(value="daily")
type_frame = ttk.LabelFrame(
self.add_job_frame, text=Msg.STR["backup_type"], padding=10)
type_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Radiobutton(type_frame, text=Msg.STR["system_backup_menu"], variable=self.backup_type,
value="system", command=self._toggle_user_sources).pack(anchor=tk.W)
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(
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)
ttk.Entry(dest_frame, textvariable=self.destination, state="readonly",
width=50).pack(side=tk.LEFT, fill=tk.X, expand=True)
ttk.Button(dest_frame, text=Msg.STR["browse"], command=self._select_destination).pack(
side=tk.RIGHT)
freq_frame = ttk.LabelFrame(
self.add_job_frame, text=Msg.STR["frequency"], padding=10)
freq_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Radiobutton(freq_frame, text=Msg.STR["freq_daily"],
variable=self.frequency, value="daily").pack(anchor=tk.W)
ttk.Radiobutton(freq_frame, text=Msg.STR["freq_weekly"],
variable=self.frequency, value="weekly").pack(anchor=tk.W)
ttk.Radiobutton(freq_frame, text=Msg.STR["freq_monthly"],
variable=self.frequency, value="monthly").pack(anchor=tk.W)
add_button_frame = ttk.Frame(self.add_job_frame)
add_button_frame.pack(pady=10)
ttk.Button(add_button_frame, text=Msg.STR["save"], command=self._save_job).pack(
side=tk.LEFT, padx=5)
ttk.Button(add_button_frame, text=Msg.STR["back"],
command=self._toggle_scheduler_view).pack(side=tk.LEFT, padx=5)
# Set initial state for the user sources
self._toggle_user_sources()
# Initially, hide the add_job_frame
self.add_job_frame.pack_forget()
def show(self):
self.grid(row=2, column=0, sticky="nsew")
self._load_scheduled_jobs()
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"
for child in self.user_sources_frame.winfo_children():
child.configure(state=state)
def _select_destination(self):
dialog = CustomFileDialog(
self, mode="dir", title=Msg.STR["select_dest_folder_title"])
result = dialog.get_result()
if result:
self.destination.set(result)
def _save_job(self):
dest = self.destination.get()
if not dest:
MessageDialog(master=self, message_type="error",
title=Msg.STR["error"], text=Msg.STR["err_no_dest_folder"])
return
job_type = self.backup_type.get()
job_frequency = self.frequency.get()
job_sources = []
if job_type == "user":
job_sources = [name for name,
var in self.user_sources.items() if var.get()]
if not job_sources:
MessageDialog(master=self, message_type="error",
title=Msg.STR["error"], text=Msg.STR["err_no_source_folder"])
return
script_path = os.path.abspath(os.path.join(
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}\" '
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,
"type": job_type,
"frequency": job_frequency,
"destination": dest,
"sources": job_sources
}
self.backup_manager.add_scheduled_job(job_details)
self._load_scheduled_jobs()
self._toggle_scheduler_view()
def _load_scheduled_jobs(self):
for i in self.jobs_tree.get_children():
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"]), ", ".join(options)
), iid=job["id"])
def _remove_selected_job(self):
selected_item = self.jobs_tree.focus()
if not selected_item:
MessageDialog(master=self, message_type="error",
title=Msg.STR["error"], text=Msg.STR["err_no_job_selected"])
return
job_id = self.jobs_tree.item(selected_item)["values"][0]
self.backup_manager.remove_scheduled_job(job_id)
self._load_scheduled_jobs()