Files
Py-Backup/schedule_job_dialog.py
Désiré Werner Menrath 827f3a1e08 feat: Implement CLI script and key file support for automated backups
- Introduces `pybackup-cli.py` as a command-line interface for non-interactive backups.
- Adds support for LUKS key files in `EncryptionManager` for passwordless container operations.
- Updates `BackupManager` to pass key file arguments to encryption routines.
- Modifies `AdvancedSettingsFrame` to provide a GUI for creating and managing key files.
- Integrates `pybackup-cli.py` and key file options into `schedule_job_dialog.py` for cronjob generation.
2025-09-05 01:48:49 +02:00

214 lines
9.1 KiB
Python

import tkinter as tk
from tkinter import ttk
import os
from shared_libs.message import MessageDialog
from shared_libs.custom_file_dialog import CustomFileDialog
from pbp_app_config import Msg
class ScheduleJobDialog(tk.Toplevel):
def __init__(self, parent, backup_manager):
super().__init__(parent)
self.parent = parent
self.backup_manager = backup_manager
self.result = None
self.title(Msg.STR["add_job_title"])
self.geometry("550x550") # Increased size
self.transient(parent)
self.grab_set()
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")
self.use_key_file = tk.BooleanVar(value=False)
self.key_file_path = tk.StringVar()
self._create_widgets()
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
self.wait_window()
def _create_widgets(self):
main_frame = ttk.Frame(self, padding=10)
main_frame.pack(fill=tk.BOTH, expand=True)
# Backup Type
type_frame = ttk.LabelFrame(
main_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)
# Destination
dest_frame = ttk.LabelFrame(
main_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)
# User Sources (initially hidden)
self.user_sources_frame = ttk.LabelFrame(
main_frame, text=Msg.STR["source_folders"], padding=10)
for name, var in self.user_sources.items():
ttk.Checkbutton(self.user_sources_frame, text=name,
variable=var).pack(anchor=tk.W)
self._toggle_user_sources() # Set initial visibility
# Key File Options
key_file_frame = ttk.LabelFrame(main_frame, text="Key File for Encrypted Backups", padding=10)
key_file_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Checkbutton(key_file_frame, text="Use Key File for automated access", variable=self.use_key_file, command=self._toggle_key_file_entry).pack(anchor=tk.W)
self.key_file_entry = ttk.Entry(key_file_frame, textvariable=self.key_file_path, state="disabled", width=50)
self.key_file_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
self.key_file_browse_button = ttk.Button(key_file_frame, text=Msg.STR["browse"], command=self._select_key_file, state="disabled")
self.key_file_browse_button.pack(side=tk.RIGHT)
# Frequency
freq_frame = ttk.LabelFrame(
main_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)
# Buttons
button_frame = ttk.Frame(main_frame)
button_frame.pack(pady=10)
ttk.Button(button_frame, text=Msg.STR["save"], command=self._on_save).pack(
side=tk.LEFT, padx=5)
ttk.Button(button_frame, text=Msg.STR["cancel"], command=self._on_cancel).pack(
side=tk.LEFT, padx=5)
def _toggle_user_sources(self):
if self.backup_type.get() == "user":
self.user_sources_frame.pack(fill=tk.X, padx=5, pady=5)
else:
self.user_sources_frame.pack_forget()
def _toggle_key_file_entry(self):
state = "normal" if self.use_key_file.get() else "disabled"
self.key_file_entry.config(state=state)
self.key_file_browse_button.config(state=state)
def _select_destination(self):
dialog = CustomFileDialog(
self, mode="dir", title=Msg.STR["select_dest_folder_title"])
self.wait_window(dialog)
result = dialog.get_result()
if result:
self.destination.set(result)
def _select_key_file(self):
dialog = CustomFileDialog(
self, mode="file", title="Select Key File", filetypes=[("Key Files", "*.key"), ("All Files", "*.*")]
)
self.wait_window(dialog)
result = dialog.get_result()
if result:
self.key_file_path.set(result)
def _on_save(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
# Check if destination is an encrypted container
is_encrypted_container = os.path.exists(os.path.join(dest, "pybackup", "pybackup_encrypted.luks"))
if is_encrypted_container and not self.use_key_file.get():
MessageDialog(master=self, message_type="error",
title=Msg.STR["error"], text="For encrypted destinations, 'Use Key File' must be selected for automated backups.")
return
if self.use_key_file.get() and not self.key_file_path.get():
MessageDialog(master=self, message_type="error",
title=Msg.STR["error"], text="Please select a key file path.")
return
# Construct the CLI command
script_path = os.path.abspath(os.path.join(
os.path.dirname(__file__), "pybackup-cli.py")) # Call the CLI script
command = f"python3 {script_path} --backup-type {job_type} --destination \"{dest}\""
if job_type == "user":
# For CLI, we assume a single source path for simplicity
if len(job_sources) > 1:
MessageDialog(master=self, message_type="warning",
title="Warning", text="For user backups, only the first selected source will be used in the automated job.")
if job_sources:
# Use the actual path from AppConfig.FOLDER_PATHS
source_folder_name = job_sources[0]
source_path = str(AppConfig.FOLDER_PATHS.get(source_folder_name, ""))
if not source_path:
MessageDialog(master=self, message_type="error",
title=Msg.STR["error"], text=f"Unknown source folder: {source_folder_name}")
return
command += f" --source \"{source_path}\""
else:
MessageDialog(master=self, message_type="error",
title=Msg.STR["error"], text="Please select a source folder for user backup.")
return
if is_encrypted_container:
command += " --encrypted"
if self.use_key_file.get():
command += f" --key-file \"{self.key_file_path.get()}\""
# No --password option for cronjobs, as it's insecure
# Construct the cron job comment
comment = f"{self.backup_manager.app_tag}; type:{job_type}; freq:{job_frequency}; dest:{dest}"
if job_type == "user":
comment += f"; source:{source_path}"
if is_encrypted_container:
comment += "; encrypted"
if self.use_key_file.get():
comment += "; key_file"
self.result = {
"command": command,
"comment": comment,
"type": job_type,
"frequency": job_frequency,
"destination": dest,
"sources": job_sources # Keep original sources for display if needed
}
self.destroy()
def _on_cancel(self):
self.result = None
self.destroy()
def show(self):
self.parent.wait_window(self)
return self.result