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.
This commit is contained in:
2025-09-05 01:48:49 +02:00
parent 4b6062981a
commit 827f3a1e08
5 changed files with 314 additions and 42 deletions

View File

@@ -123,12 +123,13 @@ rm -f '{info_file}'
else:
self.logger.log("No active backup process to cancel.")
def start_backup(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool = False, exclude_files: Optional[List[Path]] = None, source_size: int = 0, is_compressed: bool = False, is_encrypted: bool = False, mode: str = "incremental", password: str = None):
def start_backup(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool = False, exclude_files: Optional[List[Path]] = None, source_size: int = 0, is_compressed: bool = False, is_encrypted: bool = False, mode: str = "incremental", password: Optional[str] = None, key_file: Optional[str] = None):
self.is_system_process = is_system
thread = threading.Thread(target=self._run_backup_path, args=(
queue, source_path, dest_path, is_system, is_dry_run, exclude_files, source_size, is_compressed, is_encrypted, mode, password))
queue, source_path, dest_path, is_system, is_dry_run, exclude_files, source_size, is_compressed, is_encrypted, mode, password, key_file))
thread.daemon = True
thread.start()
return thread
def _find_latest_backup(self, base_backup_path: str) -> Optional[str]:
"""Finds the most recent backup directory in a given path."""
@@ -199,13 +200,13 @@ set -e
self.logger.log(f"An unexpected error occurred during local compression/cleanup: {e}")
return False
def _run_backup_path(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool, exclude_files: Optional[List[Path]], source_size: int, is_compressed: bool, is_encrypted: bool, mode: str, password: str):
def _run_backup_path(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool, exclude_files: Optional[List[Path]], source_size: int, is_compressed: bool, is_encrypted: bool, mode: str, password: Optional[str], key_file: Optional[str]):
try:
mount_point = None
if is_encrypted:
base_dest_path = os.path.dirname(dest_path)
size_gb = int(source_size / (1024**3) * 1.1) + 1
mount_point = self.encryption_manager.setup_encrypted_backup(queue, base_dest_path, size_gb, password)
mount_point = self.encryption_manager.setup_encrypted_backup(queue, base_dest_path, size_gb, password=password, key_file=key_file)
if not mount_point:
return

View File

@@ -9,6 +9,7 @@ import stat
import re
from typing import Optional
from pbp_app_config import AppConfig
from pyimage_ui.password_dialog import PasswordDialog
class EncryptionManager:
@@ -22,11 +23,60 @@ class EncryptionManager:
self.service_id = "py-backup-encryption"
self.session_password = None
def get_key_file_path(self, base_dest_path: str) -> str:
"""Generates the standard path for the key file for a given destination."""
key_filename = f"keyfile_{os.path.basename(base_dest_path.rstrip('/'))}.key"
return os.path.join(AppConfig.CONFIG_DIR, key_filename)
def create_and_add_key_file(self, base_dest_path: str, password: str) -> Optional[str]:
"""Creates a new key file and adds it as a valid key to the LUKS container."""
self.logger.log(f"Attempting to create and add key file for {base_dest_path}")
pybackup_dir = os.path.join(base_dest_path, "pybackup")
container_path = os.path.join(pybackup_dir, "pybackup_encrypted.luks")
key_file_path = self.get_key_file_path(base_dest_path)
if not os.path.exists(container_path):
self.logger.log(f"Container does not exist at {container_path}. Cannot add key file.")
return None
if os.path.exists(key_file_path):
self.logger.log(f"Key file already exists at {key_file_path}. Aborting.")
return key_file_path
# Create a temporary file for the new key
try:
with tempfile.NamedTemporaryFile(mode='w', delete=False, prefix="temp_keyfile_") as tmp_keyfile:
tmp_keyfile_path = tmp_keyfile.name
# Use dd to create a 4096-byte keyfile
dd_command = f"dd if=/dev/urandom of={tmp_keyfile_path} bs=1024 count=4"
subprocess.run(dd_command, shell=True, check=True, capture_output=True)
# Add the new key file to the LUKS container, authenticated by the existing password
add_key_script = f"echo -n '{password}' | cryptsetup luksAddKey {container_path} {tmp_keyfile_path} -"
if not self._execute_as_root(add_key_script):
self.logger.log("Failed to add new key file to LUKS container.")
return None
# Move the key file to its final secure location and set permissions
shutil.move(tmp_keyfile_path, key_file_path)
os.chmod(key_file_path, stat.S_IRUSR) # Read-only for user
self.logger.log(f"Successfully created and added key file: {key_file_path}")
return key_file_path
except Exception as e:
self.logger.log(f"An error occurred during key file creation: {e}")
return None
finally:
if 'tmp_keyfile_path' in locals() and os.path.exists(tmp_keyfile_path):
os.remove(tmp_keyfile_path)
def get_password_from_keyring(self, username: str) -> Optional[str]:
try:
return keyring.get_password(self.service_id, username)
except keyring.errors.InitError as e:
self.logger.log(f"Could not initialize keyring. Keyring is not available on this system or is not configured correctly. Error: {e}")
self.logger.log(f"Could not initialize keyring: {e}")
return None
except Exception as e:
self.logger.log(f"Could not get password from keyring: {e}")
@@ -78,14 +128,17 @@ class EncryptionManager:
return password
def is_container_mounted(self, base_dest_path: str) -> bool:
"""Checks if the LUKS container for a given base path is currently mounted."""
mapper_name = f"pybackup_{os.path.basename(base_dest_path.rstrip('/'))}"
mount_point = f"/mnt/{mapper_name}"
return os.path.ismount(mount_point)
def unlock_container(self, base_dest_path: str, password: str) -> Optional[str]:
def unlock_container(self, base_dest_path: str, password: Optional[str] = None, key_file: Optional[str] = None) -> Optional[str]:
self.logger.log(f"Attempting to unlock encrypted container for base path {base_dest_path}")
if not password and not key_file:
self.logger.log("Unlock failed: Either password or key_file must be provided.")
return None
pybackup_dir = os.path.join(base_dest_path, "pybackup")
container_path = os.path.join(pybackup_dir, "pybackup_encrypted.luks")
if not os.path.exists(container_path):
@@ -99,13 +152,18 @@ class EncryptionManager:
self.logger.log(f"Container already mounted at {mount_point}")
return mount_point
if password:
auth_part = f"echo -n '{password}' | cryptsetup luksOpen {container_path} {mapper_name} -"
else: # key_file is provided
auth_part = f"cryptsetup luksOpen {container_path} {mapper_name} --key-file {key_file}"
script = f"""
mkdir -p {mount_point}
echo -n '{password}' | cryptsetup luksOpen {container_path} {mapper_name} -
{auth_part}
mount /dev/mapper/{mapper_name} {mount_point}
"""
if not self._execute_as_root(script):
self.logger.log("Failed to unlock existing encrypted container. Check password or permissions.")
self.logger.log("Failed to unlock existing encrypted container. Check password/key file or permissions.")
self.cleanup_encrypted_backup(base_dest_path)
return None
@@ -115,7 +173,7 @@ class EncryptionManager:
def lock_container(self, base_dest_path: str):
self.cleanup_encrypted_backup(base_dest_path)
def setup_encrypted_backup(self, queue, base_dest_path: str, size_gb: int, password: str) -> Optional[str]:
def setup_encrypted_backup(self, queue, base_dest_path: str, size_gb: int, password: Optional[str] = None, key_file: Optional[str] = None) -> Optional[str]:
self.logger.log(f"Setting up encrypted container at {base_dest_path}")
if not shutil.which("cryptsetup"):
@@ -128,9 +186,9 @@ class EncryptionManager:
mapper_name = f"pybackup_{os.path.basename(base_dest_path.rstrip('/'))}"
mount_point = f"/mnt/{mapper_name}"
if not password:
self.logger.log("No password provided for encryption.")
queue.put(('error', "No password provided for encryption."))
if not password and not key_file:
self.logger.log("No password or key file provided for encryption.")
queue.put(('error', "No password or key file provided for encryption."))
return None
if os.path.ismount(mount_point):
@@ -139,15 +197,23 @@ class EncryptionManager:
if os.path.exists(container_path):
self.logger.log(f"Encrypted container {container_path} already exists. Attempting to unlock.")
return self.unlock_container(base_dest_path, password)
return self.unlock_container(base_dest_path, password=password, key_file=key_file)
else:
self.logger.log(f"Creating new encrypted container: {container_path}")
if password:
format_auth_part = f"echo -n '{password}' | cryptsetup luksFormat {container_path} -"
open_auth_part = f"echo -n '{password}' | cryptsetup luksOpen {container_path} {mapper_name} -"
else: # key_file is provided
format_auth_part = f"cryptsetup luksFormat {container_path} --key-file {key_file}"
open_auth_part = f"cryptsetup luksOpen {container_path} {mapper_name} --key-file {key_file}"
script = f"""
mkdir -p {pybackup_dir}
fallocate -l {size_gb}G {container_path}
echo -n '{password}' | cryptsetup luksFormat {container_path} -
{format_auth_part}
mkdir -p {mount_point}
echo -n '{password}' | cryptsetup luksOpen {container_path} {mapper_name} -
{open_auth_part}
mkfs.ext4 /dev/mapper/{mapper_name}
mount /dev/mapper/{mapper_name} {mount_point}
"""
@@ -212,4 +278,4 @@ class EncryptionManager:
return False
finally:
if script_path and os.path.exists(script_path):
os.remove(script_path)
os.remove(script_path)

96
pybackup-cli.py Normal file
View File

@@ -0,0 +1,96 @@
#!/usr/bin/python3
import argparse
import sys
import os
from queue import Queue
import threading
from backup_manager import BackupManager
from shared_libs.logger import app_logger
from pbp_app_config import AppConfig
# A simple logger for the CLI that just prints to the console
class CliLogger:
def log(self, message):
print(f"[CLI] {message}")
def init_logger(self, log_method):
pass # Not needed for CLI
def main():
parser = argparse.ArgumentParser(description="Py-Backup Command-Line Interface.")
parser.add_argument("--backup-type", choices=['user', 'system'], required=True, help="Type of backup to perform.")
parser.add_argument("--destination", required=True, help="Destination directory for the backup.")
parser.add_argument("--source", help="Source directory for user backup. Required for --backup-type user.")
parser.add_argument("--mode", choices=['full', 'incremental'], default='incremental', help="Mode for system backup.")
parser.add_argument("--encrypted", action='store_true', help="Flag to indicate the backup should be encrypted.")
parser.add_argument("--key-file", help="Path to the key file for unlocking an encrypted container.")
parser.add_argument("--password", help="Password for the encrypted container (use with caution). If --key-file is not provided, this will be used.")
parser.add_argument("--compressed", action='store_true', help="Flag to indicate the backup should be compressed.")
args = parser.parse_args()
if args.backup_type == 'user' and not args.source:
parser.error("--source is required for --backup-type 'user'.")
if args.encrypted and not (args.key_file or args.password):
parser.error("For encrypted backups, either --key-file or --password must be provided.")
cli_logger = CliLogger()
backup_manager = BackupManager(cli_logger)
queue = Queue() # Dummy queue for now, might be used for progress later
source_path = "/" # Default for system backup
if args.backup_type == 'user':
source_path = args.source
if not os.path.isdir(source_path):
cli_logger.log(f"Error: Source path '{source_path}' does not exist or is not a directory.")
sys.exit(1)
# Determine password or key_file to pass
auth_password = None
auth_key_file = None
if args.encrypted:
if args.key_file:
auth_key_file = args.key_file
if not os.path.exists(auth_key_file):
cli_logger.log(f"Error: Key file '{auth_key_file}' does not exist.")
sys.exit(1)
elif args.password:
auth_password = args.password
cli_logger.log(f"Starting backup with the following configuration:")
cli_logger.log(f" Type: {args.backup_type}")
cli_logger.log(f" Source: {source_path}")
cli_logger.log(f" Destination: {args.destination}")
cli_logger.log(f" Mode: {args.mode}")
cli_logger.log(f" Encrypted: {args.encrypted}")
cli_logger.log(f" Compressed: {args.compressed}")
if auth_key_file:
cli_logger.log(f" Auth Method: Key File ({auth_key_file})")
elif auth_password:
cli_logger.log(f" Auth Method: Password (REDACTED)")
# Call the backup manager
backup_thread = backup_manager.start_backup(
queue=queue,
source_path=source_path,
dest_path=args.destination,
is_system=(args.backup_type == 'system'),
is_dry_run=False,
exclude_files=None, # Excludes are handled by AppConfig.MANUAL_EXCLUDE_LIST_PATH
source_size=0, # Not accurately calculable in CLI without scanning, set to 0
is_compressed=args.compressed,
is_encrypted=args.encrypted,
mode=args.mode,
password=auth_password,
key_file=auth_key_file
)
# Wait for the backup thread to complete
backup_thread.join()
cli_logger.log("CLI backup process finished.")
if __name__ == "__main__":
main()

View File

@@ -6,7 +6,8 @@ from pathlib import Path
from pbp_app_config import AppConfig, Msg
from shared_libs.animated_icon import AnimatedIcon
from pyimage_ui.shared_logic import enforce_backup_type_exclusivity
from shared_libs.message import MessageDialog
from pyimage_ui.password_dialog import PasswordDialog
class AdvancedSettingsFrame(tk.Toplevel):
def __init__(self, master, config_manager, app_instance, **kwargs):
@@ -17,12 +18,10 @@ class AdvancedSettingsFrame(tk.Toplevel):
self.app_instance = app_instance
self.current_view_index = 0
# --- Warning Label ---
self.info_label = ttk.Label(
self, text=Msg.STR["advanced_settings_warning"], wraplength=780, justify="left")
self.info_label.pack(pady=10, fill=tk.X, padx=10)
# --- Navigation ---
nav_frame = ttk.Frame(self)
nav_frame.pack(fill=tk.X, padx=10, pady=5)
@@ -53,11 +52,9 @@ class AdvancedSettingsFrame(tk.Toplevel):
ttk.Separator(top_nav_frame, orient=tk.VERTICAL).pack(
side=tk.LEFT, fill=tk.Y, padx=2)
# --- Container for the two views ---
view_container = ttk.Frame(self)
view_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# --- Treeview for system folder exclusion ---
self.tree_frame = ttk.LabelFrame(
view_container, text=Msg.STR["exclude_system_folders"], padding=10)
@@ -71,12 +68,9 @@ class AdvancedSettingsFrame(tk.Toplevel):
self.tree.column("name", anchor="center")
self.tree.column("included", width=100, anchor="center")
self.tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.tree.tag_configure("backup_dest_exclude", foreground="gray")
self.tree.bind("<Button-1>", self._toggle_include_status)
# --- Manual Excludes Frame ---
self.manual_excludes_frame = ttk.LabelFrame(
view_container, text=Msg.STR["manual_excludes"], padding=10)
@@ -89,7 +83,6 @@ class AdvancedSettingsFrame(tk.Toplevel):
self.manual_excludes_frame, text=Msg.STR["delete"], command=self._delete_manual_exclude)
delete_button.pack(pady=5)
# --- Animation Settings ---
animation_frame = ttk.LabelFrame(
self, text=Msg.STR["animation_settings_title"], padding=10)
animation_frame.pack(fill=tk.X, padx=10, pady=5)
@@ -116,7 +109,6 @@ class AdvancedSettingsFrame(tk.Toplevel):
animation_frame.columnconfigure(1, weight=1)
# --- Backup Default Settings ---
defaults_frame = ttk.LabelFrame(
self, text="Backup Defaults", padding=10)
defaults_frame.pack(fill=tk.X, padx=10, pady=5)
@@ -142,6 +134,25 @@ class AdvancedSettingsFrame(tk.Toplevel):
defaults_frame, text=Msg.STR["encryption_note_system_backup"], wraplength=750, justify="left")
encryption_note.pack(anchor=tk.W, pady=5)
# --- Automation Settings ---
automation_frame = ttk.LabelFrame(self, text="Automation (Cronjob)", padding=10)
automation_frame.pack(fill=tk.X, padx=10, pady=5)
key_file_button = ttk.Button(automation_frame, text="Create/Add Key File", command=self._create_key_file)
key_file_button.grid(row=0, column=0, padx=5, pady=5)
self.key_file_status_var = tk.StringVar(value="Key file not created.")
key_file_status_label = ttk.Label(automation_frame, textvariable=self.key_file_status_var, foreground="gray")
key_file_status_label.grid(row=0, column=1, padx=5, pady=5, sticky="w")
sudoers_info_text = (f"To run automated backups, an administrator must create a file in /etc/sudoers.d/\n"
f"with the following content (replace 'punix' with the correct username):\n"
f"punix ALL=(ALL) NOPASSWD: /path/to/pybackup-cli.py") # Path needs to be updated
sudoers_info_label = ttk.Label(automation_frame, text=sudoers_info_text, justify="left")
sudoers_info_label.grid(row=1, column=0, columnspan=2, sticky="w", padx=5, pady=5)
automation_frame.columnconfigure(1, weight=1)
# --- Action Buttons ---
button_frame = ttk.Frame(self)
button_frame.pack(pady=10)
@@ -155,9 +166,48 @@ class AdvancedSettingsFrame(tk.Toplevel):
self._load_animation_settings()
self._load_backup_defaults()
self._load_manual_excludes()
self._update_key_file_status()
self._switch_view(self.current_view_index)
def _create_key_file(self):
if not self.app_instance.destination_path:
MessageDialog(self, message_type="error", title="Error", text="Please select a backup destination first.")
return
pybackup_dir = os.path.join(self.app_instance.destination_path, "pybackup")
container_path = os.path.join(pybackup_dir, "pybackup_encrypted.luks")
if not os.path.exists(container_path):
MessageDialog(self, message_type="error", title="Error", text="No encrypted container found at the destination.")
return
# Prompt for the existing password to authorize adding a new key
password_dialog = PasswordDialog(self, title="Enter Existing Password", confirm=False)
password, _ = password_dialog.get_password()
if not password:
return # User cancelled
key_file_path = self.app_instance.backup_manager.encryption_manager.create_and_add_key_file(self.app_instance.destination_path, password)
if key_file_path:
MessageDialog(self, message_type="info", title="Success", text=f"Key file created and added successfully!\nPath: {key_file_path}")
else:
MessageDialog(self, message_type="error", title="Error", text="Failed to create or add key file. See log for details.")
self._update_key_file_status()
def _update_key_file_status(self):
if not self.app_instance.destination_path:
self.key_file_status_var.set("Key file status unknown (no destination set).")
return
key_file_path = self.app_instance.backup_manager.encryption_manager.get_key_file_path(self.app_instance.destination_path)
if os.path.exists(key_file_path):
self.key_file_status_var.set(f"Key file exists: {key_file_path}")
else:
self.key_file_status_var.set("Key file has not been created for this destination.")
def _switch_view(self, index):
self.current_view_index = index
self.update_nav_buttons(index)
@@ -272,7 +322,6 @@ class AdvancedSettingsFrame(tk.Toplevel):
item_values = items_to_display[item_path_str]
tag = "yes" if item_values[0] == Msg.STR["yes"] else "no"
# Special tag for the backup destination, which is always excluded and read-only
is_backup_dest = (self.app_instance and self.app_instance.destination_path and
item_path_str == str(Path(f"/{self.app_instance.destination_path.strip('/').split('/')[0]}").absolute()))
is_restore_src = (restore_src_path and
@@ -314,10 +363,8 @@ class AdvancedSettingsFrame(tk.Toplevel):
if self.app_instance:
self.app_instance.update_backup_options_from_config()
# Destroy the old icon
self.app_instance.animated_icon.destroy()
# Create a new one
bg_color = self.app_instance.style.lookup('TFrame', 'background')
backup_animation_type = self.backup_anim_var.get()
@@ -328,11 +375,9 @@ class AdvancedSettingsFrame(tk.Toplevel):
self.app_instance.animated_icon = AnimatedIcon(
self.app_instance.action_frame, width=20, height=20, use_pillow=True, bg=bg_color, animation_type=initial_animation_type)
# Pack it in the correct order
self.app_instance.animated_icon.pack(
side=tk.LEFT, padx=5, before=self.app_instance.task_progress)
# Set the correct state
self.app_instance.animated_icon.stop("DISABLE")
self.app_instance.animated_icon.animation_type = backup_animation_type
@@ -374,7 +419,6 @@ class AdvancedSettingsFrame(tk.Toplevel):
for path in final_excludes:
f.write(f"{path}\n")
# Save manual excludes
with open(AppConfig.MANUAL_EXCLUDE_LIST_PATH, 'w') as f:
for item in self.manual_excludes_listbox.get(0, tk.END):
f.write(f"{item}\n")
@@ -405,4 +449,4 @@ class AdvancedSettingsFrame(tk.Toplevel):
user_patterns.extend(
[line.strip() for line in f if line.strip() and not line.startswith('#')])
return generated_patterns, user_patterns
return generated_patterns, user_patterns

View File

@@ -15,7 +15,7 @@ class ScheduleJobDialog(tk.Toplevel):
self.result = None
self.title(Msg.STR["add_job_title"])
self.geometry("500x400")
self.geometry("550x550") # Increased size
self.transient(parent)
self.grab_set()
@@ -28,6 +28,8 @@ class ScheduleJobDialog(tk.Toplevel):
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()
@@ -64,6 +66,17 @@ class ScheduleJobDialog(tk.Toplevel):
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)
@@ -89,6 +102,11 @@ class ScheduleJobDialog(tk.Toplevel):
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"])
@@ -97,6 +115,15 @@ class ScheduleJobDialog(tk.Toplevel):
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:
@@ -116,19 +143,57 @@ class ScheduleJobDialog(tk.Toplevel):
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__), "main_app.py"))
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":
command += f" --sources "
for s in job_sources:
command += f'\"{s}\" '
# 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"; sources:{','.join(job_sources)}"
comment += f"; source:{source_path}"
if is_encrypted_container:
comment += "; encrypted"
if self.use_key_file.get():
comment += "; key_file"
self.result = {
"command": command,
@@ -136,7 +201,7 @@ class ScheduleJobDialog(tk.Toplevel):
"type": job_type,
"frequency": job_frequency,
"destination": dest,
"sources": job_sources
"sources": job_sources # Keep original sources for display if needed
}
self.destroy()
@@ -146,4 +211,4 @@ class ScheduleJobDialog(tk.Toplevel):
def show(self):
self.parent.wait_window(self)
return self.result
return self.result