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.
259 lines
12 KiB
Python
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()
|