Compare commits
6 Commits
7decaf46ef
...
3a59bccddc
Author | SHA1 | Date | |
---|---|---|---|
3a59bccddc | |||
9406d3f0e2 | |||
34234d2d14 | |||
6504758b7b | |||
ff640ca9ef | |||
7d64544c37 |
@@ -88,7 +88,11 @@ class BackupManager:
|
||||
|
||||
if is_system:
|
||||
return os.path.join(base_data_dir, "system")
|
||||
# For user backups, store them directly in the mount point if encrypted.
|
||||
elif is_encrypted:
|
||||
return base_data_dir
|
||||
else:
|
||||
# Keep old structure for unencrypted user backups
|
||||
return os.path.join(base_data_dir, "user", source_name)
|
||||
|
||||
def check_for_full_backup(self, dest_path: str, source_name: str, is_encrypted: bool) -> bool:
|
||||
@@ -201,7 +205,9 @@ class BackupManager:
|
||||
full_system_cmd = f"mkdir -p '{rsync_dest}' && {rsync_cmd_str}"
|
||||
command = ['pkexec', 'bash', '-c', full_system_cmd]
|
||||
else:
|
||||
rsync_command_parts.extend([source_path, rsync_dest])
|
||||
# Add a trailing slash to the source path to ensure rsync copies the content, not the directory itself.
|
||||
source_with_slash = source_path.rstrip('/') + '/'
|
||||
rsync_command_parts.extend([source_with_slash, rsync_dest])
|
||||
os.makedirs(rsync_dest, exist_ok=True)
|
||||
command = rsync_command_parts
|
||||
|
||||
@@ -273,7 +279,7 @@ class BackupManager:
|
||||
if is_system:
|
||||
command.extend(['pkexec', 'rsync', '-aAXHvn', '--stats'])
|
||||
else:
|
||||
command.extend(['rsync', '-avn', '--stats'])
|
||||
command.extend(['rsync', '-ain', '--dry-run', '--stats'])
|
||||
|
||||
command.append(f"--link-dest={latest_backup_path}")
|
||||
|
||||
@@ -283,6 +289,9 @@ class BackupManager:
|
||||
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as dummy_dest:
|
||||
# For user backups, add a trailing slash to copy contents, not the dir itself.
|
||||
if not is_system:
|
||||
source_path = source_path.rstrip('/') + '/'
|
||||
command.extend([source_path, dummy_dest])
|
||||
|
||||
self.logger.log(
|
||||
|
@@ -141,11 +141,23 @@ do_resize() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Resizing LUKS volume."
|
||||
cryptsetup resize "$mapper_name"
|
||||
|
||||
log "Checking and resizing filesystem."
|
||||
log "Checking filesystem before resize."
|
||||
e2fsck -fy "/dev/mapper/$mapper_name"
|
||||
|
||||
log "Resizing LUKS volume and filesystem."
|
||||
# Re-authenticate for resize, as some cryptsetup versions require it.
|
||||
if [ -n "$LUKSPASS" ]; then
|
||||
log "Resizing with password."
|
||||
echo -n "$LUKSPASS" | cryptsetup resize "$mapper_name" -
|
||||
elif [ -n "$key_file_arg" ]; then
|
||||
log "Resizing with keyfile."
|
||||
cryptsetup resize "$mapper_name" --key-file "$key_file_arg"
|
||||
else
|
||||
# This should not happen if luksOpen succeeded.
|
||||
# Fallback to the previous non-interactive attempt.
|
||||
log "No password or keyfile for resize, trying non-interactively."
|
||||
cryptsetup resize "$mapper_name" < /dev/null
|
||||
fi
|
||||
resize2fs "/dev/mapper/$mapper_name"
|
||||
|
||||
# 4. Mount it again
|
||||
|
@@ -27,6 +27,7 @@ class EncryptionManager:
|
||||
self.lock_file = AppConfig.LOCK_FILE_PATH
|
||||
|
||||
def _write_lock_file(self, data):
|
||||
|
||||
with open(self.lock_file, 'w') as f:
|
||||
json.dump(data, f)
|
||||
|
||||
@@ -93,33 +94,62 @@ class EncryptionManager:
|
||||
return os.path.join(pybackup_dir, container_filename)
|
||||
|
||||
def get_key_file_path(self, base_dest_path: str, profile_name: str) -> str:
|
||||
pybackup_dir = os.path.join(base_dest_path, "pybackup")
|
||||
# base_dest_path is ignored for security. Keyfiles are stored centrally.
|
||||
secure_key_dir = "/usr/local/etc/luks"
|
||||
key_filename = f"luks_{profile_name}.keyfile"
|
||||
return os.path.join(pybackup_dir, key_filename)
|
||||
return os.path.join(secure_key_dir, key_filename)
|
||||
|
||||
def create_and_add_key_file(self, base_dest_path: str, profile_name: str, password: str) -> Optional[str]:
|
||||
# Get the new secure path for the keyfile
|
||||
key_file_path = self.get_key_file_path(base_dest_path, profile_name)
|
||||
secure_key_dir = os.path.dirname(key_file_path)
|
||||
|
||||
# Container path remains the same, on the destination drive
|
||||
container_path = self.get_container_path(base_dest_path, profile_name)
|
||||
|
||||
# This script will be executed as root. It creates the secure directory,
|
||||
# generates the keyfile, sets its permissions, and adds it to the LUKS container.
|
||||
# Using shlex.quote to be safe with paths
|
||||
script = f"""
|
||||
mkdir -p {shlex.quote(secure_key_dir)} && \
|
||||
dd if=/dev/urandom of={shlex.quote(key_file_path)} bs=1 count=4096 && \
|
||||
chmod 0400 {shlex.quote(key_file_path)} && \
|
||||
cryptsetup luksAddKey {shlex.quote(container_path)} {shlex.quote(key_file_path)}
|
||||
"""
|
||||
|
||||
if self._execute_as_root(script, password):
|
||||
self.logger.log(
|
||||
f"Successfully created keyfile at {key_file_path} and added to LUKS container.")
|
||||
return key_file_path
|
||||
else:
|
||||
self.logger.log(
|
||||
"Failed to create and add key file. The script might have failed midway. Attempting cleanup.")
|
||||
# Attempt to clean up the keyfile if the script failed, as we can't be sure at which stage it failed.
|
||||
cleanup_script = f"rm -f {shlex.quote(key_file_path)}"
|
||||
self._execute_as_root(cleanup_script)
|
||||
return None
|
||||
|
||||
def delete_key_file(self, base_dest_path: str, profile_name: str, password: str) -> bool:
|
||||
key_file_path = self.get_key_file_path(base_dest_path, profile_name)
|
||||
container_path = self.get_container_path(base_dest_path, profile_name)
|
||||
try:
|
||||
with open(key_file_path, 'wb') as f:
|
||||
f.write(os.urandom(4096))
|
||||
os.chmod(key_file_path, 0o400)
|
||||
self.logger.log(f"Generated new key file at {key_file_path}")
|
||||
|
||||
script = f'cryptsetup luksAddKey "{container_path}" "{key_file_path}"'
|
||||
if self._execute_as_root(script, password):
|
||||
self.logger.log(
|
||||
"Successfully added key file to LUKS container.")
|
||||
return key_file_path
|
||||
else:
|
||||
self.logger.log("Failed to add key file to LUKS container.")
|
||||
os.remove(key_file_path)
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.log(f"Error creating key file: {e}")
|
||||
if os.path.exists(key_file_path):
|
||||
os.remove(key_file_path)
|
||||
return None
|
||||
if not os.path.exists(key_file_path):
|
||||
self.logger.log(f"Keyfile for profile {profile_name} does not exist. Nothing to delete.")
|
||||
return True # Considered a success as the desired state is "deleted"
|
||||
|
||||
# This script removes the key from the LUKS container and then deletes the keyfile.
|
||||
# It requires a valid password for the container to authorize the key removal.
|
||||
script = f"""
|
||||
cryptsetup luksRemoveKey {shlex.quote(container_path)} {shlex.quote(key_file_path)} && \
|
||||
rm -f {shlex.quote(key_file_path)}
|
||||
"""
|
||||
|
||||
if self._execute_as_root(script, password):
|
||||
self.logger.log(f"Successfully removed keyfile for profile {profile_name}.")
|
||||
return True
|
||||
else:
|
||||
self.logger.log(f"Failed to remove keyfile for profile {profile_name}.")
|
||||
return False
|
||||
|
||||
def _get_password_and_keyfile(self, base_dest_path: str, profile_name: str, username: str) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""Gets the password (from cache, keyring, or user) or the keyfile path."""
|
||||
@@ -177,9 +207,11 @@ class EncryptionManager:
|
||||
free_space = shutil.disk_usage(mount_point).free
|
||||
required_space = int(source_size * 1.10)
|
||||
|
||||
if required_space > free_space:
|
||||
BUFFER = 4 * 1024 * 1024 * 1024 # 4 GB
|
||||
# Trigger resize if projected free space is less than the buffer
|
||||
if free_space - required_space < BUFFER:
|
||||
self.logger.log(
|
||||
f"Resize needed for {profile_name}. Free: {free_space}, Required: {required_space}")
|
||||
f"Resize needed for {profile_name}. Free: {free_space}, Required: {required_space}, Buffer: {BUFFER}")
|
||||
queue.put(
|
||||
('status_update', f"Container für {profile_name} zu klein. Vergrößere..."))
|
||||
|
||||
@@ -192,8 +224,11 @@ class EncryptionManager:
|
||||
return None
|
||||
|
||||
current_total = shutil.disk_usage(mount_point).total
|
||||
needed_additional = required_space - free_space - \
|
||||
(4 * 1024 * 1024 * 1024) # (4 * 1024 * 1024 * 1024) = 4 GB
|
||||
|
||||
# How much space we need to add to meet the future demand and have the buffer left over.
|
||||
# This is the deficit (required_space - free_space) plus the desired buffer.
|
||||
needed_additional = (required_space - free_space) + BUFFER
|
||||
|
||||
new_total_size = current_total + needed_additional
|
||||
new_total_size = math.ceil(new_total_size / 4096) * 4096
|
||||
|
||||
|
@@ -229,6 +229,7 @@ class Msg:
|
||||
"ready_for_first_backup": _("Everything is ready for your first backup."),
|
||||
"backup_mode": _("Backup Mode"),
|
||||
"backup_mode_info": _("Backup Mode: You can start a backup here."),
|
||||
"encrypted_unmounted_warning": _("Accurate detection is only possible when the drive is mounted."),
|
||||
"restore_mode_info": _("Restore Mode: You can start a restore here."),
|
||||
"advanced_settings_title": _("Advanced Settings"),
|
||||
"animation_settings_title": _("Animation Settings"),
|
||||
@@ -256,7 +257,7 @@ class Msg:
|
||||
"log": _("Log"),
|
||||
"full_backup": _("Full backup"),
|
||||
"incremental": _("Incremental"),
|
||||
"incremental_backup": _("Incremental backup"), # New
|
||||
"incremental_backup": _("Incremental backup"),
|
||||
"test_run": _("Test run"),
|
||||
"start": _("Start"),
|
||||
"cancel_backup": _("Cancel"),
|
||||
@@ -279,8 +280,8 @@ class Msg:
|
||||
"system_excludes": _("System Excludes"),
|
||||
"manual_excludes": _("Manual Excludes"),
|
||||
"manual_excludes_info": _("Here, manually add files or folders to be excluded from the backup. Each entry should be on a new line."),
|
||||
"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 'user' with the correct username):\n"
|
||||
"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 'user' with the correct username):\n"
|
||||
f"user ALL=(ALL) NOPASSWD: /path/to/pybackup-cli.py"),
|
||||
|
||||
# Menus
|
||||
@@ -352,9 +353,9 @@ class Msg:
|
||||
"header_subtitle": _("Simple GUI for rsync"),
|
||||
"encrypted_backup_content": _("Encrypted Backups"),
|
||||
"compressed": _("Compressed"),
|
||||
"compression": _("Compression"), # New
|
||||
"compression": _("Compression"),
|
||||
"encrypted": _("Encrypted"),
|
||||
"encryption": _("Encryption"), # New
|
||||
"encryption": _("Encryption"),
|
||||
"bypass_security": _("Bypass security"),
|
||||
"refresh_log": _("Refresh log"),
|
||||
"comment": _("Kommentar"),
|
||||
@@ -369,15 +370,38 @@ class Msg:
|
||||
"sync_mode_trash_bin": _("Sync Mode: Archive. Files deleted from the source will be moved to a trash folder in the backup."),
|
||||
"sync_mode_no_delete": _("Sync Mode: Additive. Files deleted from the source will be kept in the backup."),
|
||||
"encryption_note_system_backup": _("Note: For system backups, encryption only applies to files directly within the /home directory. Folders are not automatically encrypted unless explicitly included in the backup."),
|
||||
"keyfile_settings": _("Keyfile Settings"), # New
|
||||
"backup_defaults_title": _("Backup Defaults"), # New
|
||||
"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
|
||||
"hard_reset": _("Hard reset"),
|
||||
|
||||
# Advanced Settings - Keyfile
|
||||
"keyfile_settings": _("Keyfile Settings"),
|
||||
"backup_defaults_title": _("Backup Defaults"),
|
||||
"automation_settings_title": _("Automation Settings"),
|
||||
"create_add_key_file": _("Create/Add Key File"),
|
||||
"key_file_not_created": _("Key file not created."),
|
||||
"keyfile_automation_info": _("A keyfile provides an alternative way to unlock an encrypted backup without entering a password, which is useful for automation.\n\n**Prerequisite:** You must first create at least one encrypted backup for a profile. This process will then add a keyfile to that existing encrypted container."),
|
||||
"err_no_encrypted_container": _("No encrypted container found at the destination. Please create an encrypted backup for this profile first."),
|
||||
"enter_existing_password_title": _("Enter Existing Password"),
|
||||
"keyfile_creation_success": _("Keyfile created successfully!"),
|
||||
"keyfile_creation_failed": _("Failed to create keyfile. See log for details."),
|
||||
"keyfile_status_unknown": _("Keyfile status unknown (no destination set)."),
|
||||
"keyfile_exists": _("Keyfile exists: {key_file_path}"),
|
||||
"keyfile_not_found_for_dest": _("Keyfile has not been created for this destination."),
|
||||
"select_profile_first": _("Please select at least one profile from the list."),
|
||||
"current_destination": _("Current Target Destination"),
|
||||
"no_destination_selected": _("None. Please select a destination in the main backup view."),
|
||||
"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 'user' with the correct username):\n"
|
||||
f"user ALL=(ALL) NOPASSWD: /path/to/pybackup-cli.py"),
|
||||
|
||||
# General Settings
|
||||
"backup_options": _("Backup Options"),
|
||||
"reset": _("Reset"),
|
||||
"hard_reset_success": _("Hard reset successful.\nRestarting..."),
|
||||
"default_reset_success": _("Default reset successful"),
|
||||
"default_reset_failed": _("Default reset failed\nPlease click Delete now on Full_delete_config_settings..."),
|
||||
"default_reset_info": _("Click to restore all settings to their original defaults. Your existing backup schedules and any manually added paths in the exclusion list will be preserved."),
|
||||
"hard_reset_warning": _("This will reset the application to its initial state, as if it were opened for the first time. This can be useful if you are experiencing problems with the app. Clicking 'Delete now' will delete the '.config/py_backup' folder in your home directory without an additional dialog. The application will then automatically restart."),
|
||||
"delete_now": _("Delete now"),
|
||||
"default_config_settings": _("Default config settings"),
|
||||
"full_delete_config_settings": _("Full delete config settings"),
|
||||
"password_required": _("Password Required"),
|
||||
"enter_password_prompt": _("Please enter the password for the encrypted backup:"),
|
||||
@@ -386,5 +410,7 @@ class Msg:
|
||||
"password_empty_error": _("Password cannot be empty."),
|
||||
"passwords_do_not_match_error": _("Passwords do not match."),
|
||||
"ok": _("OK"),
|
||||
"settings_saved": _("Settings saved successfully!"),
|
||||
"delete_success": _("Deletion successful!"),
|
||||
"unlock_backup": _("Unlock Backup"),
|
||||
}
|
||||
|
28
main_app.py
28
main_app.py
@@ -54,16 +54,20 @@ class MainApplication(tk.Tk):
|
||||
self.style.configure("Green.Sidebar.TButton", foreground="green")
|
||||
|
||||
# Custom button styles for BackupContentFrame
|
||||
self.style.configure("Success.TButton", foreground="#2E7D32") # Darker Green
|
||||
self.style.configure("Danger.TButton", foreground="#C62828") # Darker Red
|
||||
self.style.configure(
|
||||
"Success.TButton", foreground="#2E7D32") # Darker Green
|
||||
self.style.configure(
|
||||
"Danger.TButton", foreground="#C62828") # Darker Red
|
||||
|
||||
# Custom LabelFrame style for Mount frame
|
||||
self.style.configure("Mount.TLabelFrame", bordercolor="#0078D7")
|
||||
self.style.configure("Mount.TLabelFrame.Label", foreground="#0078D7")
|
||||
|
||||
# Custom button styles for BackupContentFrame
|
||||
self.style.configure("Success.TButton", foreground="#2E7D32") # Darker Green
|
||||
self.style.configure("Danger.TButton", foreground="#C62828") # Darker Red
|
||||
self.style.configure(
|
||||
"Success.TButton", foreground="#2E7D32") # Darker Green
|
||||
self.style.configure(
|
||||
"Danger.TButton", foreground="#C62828") # Darker Red
|
||||
|
||||
self.style.configure("Switch2.TCheckbutton",
|
||||
background="#2b3e4f", foreground="white")
|
||||
@@ -435,7 +439,7 @@ class MainApplication(tk.Tk):
|
||||
restore_dest_folder)
|
||||
self._process_queue()
|
||||
self._update_sync_mode_display() # Call after loading state
|
||||
self.update_backup_options_from_config() # Apply defaults on startup
|
||||
self.update_backup_options_from_config() # Apply defaults on startup
|
||||
|
||||
def _setup_log_window(self):
|
||||
self.log_frame = ttk.Frame(self.content_frame)
|
||||
@@ -452,8 +456,8 @@ class MainApplication(tk.Tk):
|
||||
self.scheduler_frame.grid_remove()
|
||||
|
||||
def _setup_settings_frame(self):
|
||||
self.settings_frame = SettingsFrame(
|
||||
self.content_frame, self.navigation, self.actions, self.backup_manager.encryption_manager, self.image_manager, self.config_manager, padding=(0, 10))
|
||||
self.settings_frame = SettingsFrame(self.content_frame, self.navigation, self.actions,
|
||||
self.backup_manager.encryption_manager, self.image_manager, self.config_manager, padding=(0, 10))
|
||||
self.settings_frame.grid(row=2, column=0, sticky="nsew")
|
||||
self.settings_frame.grid_remove()
|
||||
|
||||
@@ -552,13 +556,10 @@ class MainApplication(tk.Tk):
|
||||
self.start_cancel_button.grid(row=0, column=2, rowspan=2, padx=5)
|
||||
|
||||
def on_closing(self):
|
||||
self.config_manager.set_setting(
|
||||
"refresh_log", self.refresh_log_var.get())
|
||||
|
||||
# Attempt to unmount all encrypted drives
|
||||
# First, always attempt to unmount all encrypted drives
|
||||
unmount_success = self.backup_manager.encryption_manager.unmount_all()
|
||||
|
||||
# Check if any drives are still mounted after the attempt
|
||||
# If unmounting fails, show an error and prevent the app from closing
|
||||
if not unmount_success and self.backup_manager.encryption_manager.mounted_destinations:
|
||||
app_logger.log(
|
||||
"WARNING: Not all encrypted drives could be unmounted. Preventing application closure.")
|
||||
@@ -566,6 +567,8 @@ class MainApplication(tk.Tk):
|
||||
message_type="error", title=Msg.STR["unmount_failed_title"], text=Msg.STR["unmount_failed_message"]).show()
|
||||
return # Prevent application from closing
|
||||
|
||||
self.config_manager.set_setting(
|
||||
"refresh_log", self.refresh_log_var.get())
|
||||
self.config_manager.set_setting("last_mode", self.mode)
|
||||
|
||||
if self.backup_left_canvas_data.get('path_display'):
|
||||
@@ -592,6 +595,7 @@ class MainApplication(tk.Tk):
|
||||
else:
|
||||
self.config_manager.set_setting("restore_source_path", None)
|
||||
|
||||
# Finally, clean up UI resources and destroy the window
|
||||
if self.left_canvas_animation:
|
||||
self.left_canvas_animation.stop()
|
||||
self.left_canvas_animation.destroy()
|
||||
|
@@ -77,7 +77,7 @@ class Actions:
|
||||
self.app.compressed_cb.config(state="normal")
|
||||
self.app.encrypted_cb.config(state="normal")
|
||||
self.app.test_run_cb.config(state="normal")
|
||||
|
||||
|
||||
# Apply mutual exclusion rules for Option A
|
||||
if self.app.compressed_var.get():
|
||||
self.app.incremental_var.set(False)
|
||||
@@ -85,7 +85,7 @@ class Actions:
|
||||
self.app.incremental_cb.config(state="disabled")
|
||||
self.app.encrypted_var.set(False)
|
||||
self.app.encrypted_cb.config(state="disabled")
|
||||
|
||||
|
||||
if self.app.incremental_var.get() or self.app.encrypted_var.get():
|
||||
self.app.compressed_var.set(False)
|
||||
self.app.compressed_cb.config(state="disabled")
|
||||
@@ -397,47 +397,6 @@ class Actions:
|
||||
MessageDialog(message_type="error",
|
||||
title=Msg.STR["error"], text=Msg.STR["path_not_found"].format(path=path)).show()
|
||||
|
||||
def reset_to_default_settings(self):
|
||||
|
||||
self.app.config_manager.set_setting("backup_destination_path", None)
|
||||
self.app.config_manager.set_setting("restore_source_path", None)
|
||||
self.app.config_manager.set_setting("restore_destination_path", None)
|
||||
|
||||
self.app.config_manager.remove_setting("backup_animation_type")
|
||||
self.app.config_manager.remove_setting("calculation_animation_type")
|
||||
self.app.config_manager.remove_setting("force_full_backup")
|
||||
self.app.config_manager.remove_setting("force_incremental_backup")
|
||||
self.app.config_manager.remove_setting("force_compression")
|
||||
self.app.config_manager.remove_setting("force_encryption")
|
||||
|
||||
self.app.update_backup_options_from_config()
|
||||
|
||||
AppConfig.generate_and_write_final_exclude_list()
|
||||
app_logger.log("Settings have been reset to default values.")
|
||||
|
||||
settings_frame = self.app.settings_frame
|
||||
if settings_frame:
|
||||
settings_frame.load_and_display_excludes()
|
||||
settings_frame._load_hidden_files()
|
||||
self.app.destination_path = None
|
||||
self.app.start_cancel_button.config(state="disabled")
|
||||
|
||||
self.app.backup_left_canvas_data.clear()
|
||||
self.app.backup_right_canvas_data.clear()
|
||||
self.app.restore_left_canvas_data.clear()
|
||||
self.app.restore_right_canvas_data.clear()
|
||||
|
||||
current_source = self.app.left_canvas_data.get('folder')
|
||||
if current_source:
|
||||
self.on_sidebar_button_click(current_source)
|
||||
|
||||
self.app.backup_content_frame.system_backups_frame._load_backup_content()
|
||||
self.app.backup_content_frame.user_backups_frame._load_backup_content()
|
||||
|
||||
with message_box_animation(self.app.animated_icon):
|
||||
MessageDialog(message_type="info",
|
||||
title=Msg.STR["settings_reset_title"], text=Msg.STR["settings_reset_text"])
|
||||
|
||||
def _parse_size_string_to_bytes(self, size_str: str) -> int:
|
||||
if not size_str or size_str == Msg.STR["calculating_size"]:
|
||||
return 0
|
||||
@@ -524,7 +483,8 @@ class Actions:
|
||||
|
||||
self.app.task_progress.stop()
|
||||
self.app.task_progress.config(mode="determinate", value=0)
|
||||
self.app._update_info_label(Msg.STR["incremental_calc_cancelled"], color="#E8740C")
|
||||
self.app._update_info_label(
|
||||
Msg.STR["incremental_calc_cancelled"], color="#E8740C")
|
||||
self.app.start_cancel_button.config(text=Msg.STR["start"])
|
||||
self._set_ui_state(True)
|
||||
return
|
||||
@@ -541,12 +501,14 @@ class Actions:
|
||||
delete_path)
|
||||
app_logger.log(
|
||||
Msg.STR["backup_cancelled_and_deleted_msg"])
|
||||
self.app._update_info_label(Msg.STR["backup_cancelled_and_deleted_msg"])
|
||||
self.app._update_info_label(
|
||||
Msg.STR["backup_cancelled_and_deleted_msg"])
|
||||
else:
|
||||
self.app.backup_manager.cancel_backup()
|
||||
app_logger.log(
|
||||
"Backup cancelled, but directory could not be deleted (path unknown).")
|
||||
self.app._update_info_label("Backup cancelled, but directory could not be deleted (path unknown).")
|
||||
self.app._update_info_label(
|
||||
"Backup cancelled, but directory could not be deleted (path unknown).")
|
||||
else:
|
||||
self.app.backup_manager.cancel_backup()
|
||||
if delete_path:
|
||||
@@ -557,14 +519,17 @@ class Actions:
|
||||
shutil.rmtree(delete_path)
|
||||
app_logger.log(
|
||||
Msg.STR["backup_cancelled_and_deleted_msg"])
|
||||
self.app._update_info_label(Msg.STR["backup_cancelled_and_deleted_msg"])
|
||||
self.app._update_info_label(
|
||||
Msg.STR["backup_cancelled_and_deleted_msg"])
|
||||
except Exception as e:
|
||||
app_logger.log(f"Error deleting backup directory: {e}")
|
||||
self.app._update_info_label(f"Error deleting backup directory: {e}")
|
||||
self.app._update_info_label(
|
||||
f"Error deleting backup directory: {e}")
|
||||
else:
|
||||
app_logger.log(
|
||||
"Backup cancelled, but no path found to delete.")
|
||||
self.app._update_info_label("Backup cancelled, but no path found to delete.")
|
||||
self.app._update_info_label(
|
||||
"Backup cancelled, but no path found to delete.")
|
||||
|
||||
if hasattr(self.app, 'current_backup_path'):
|
||||
self.app.current_backup_path = None
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import os
|
||||
import glob
|
||||
from pathlib import Path
|
||||
|
||||
from core.pbp_app_config import AppConfig, Msg
|
||||
@@ -17,6 +18,7 @@ class AdvancedSettingsFrame(ttk.Frame):
|
||||
self.config_manager = config_manager
|
||||
self.app_instance = app_instance
|
||||
self.current_view_index = 0
|
||||
self.keyfile_profile_widgets = {}
|
||||
|
||||
nav_frame = ttk.Frame(self)
|
||||
nav_frame.pack(fill=tk.X, padx=10, pady=(0, 5))
|
||||
@@ -166,28 +168,21 @@ class AdvancedSettingsFrame(ttk.Frame):
|
||||
self.backup_defaults_frame, text=Msg.STR["encryption_note_system_backup"], wraplength=750, justify="left")
|
||||
encryption_note.pack(anchor=tk.W, pady=5)
|
||||
|
||||
# --- Keyfile Settings Frame ---
|
||||
self.keyfile_settings_frame = ttk.LabelFrame(
|
||||
view_container, text=Msg.STR["automation_settings_title"], padding=10)
|
||||
|
||||
keyfile_info_label = ttk.Label(
|
||||
self.keyfile_settings_frame, text=Msg.STR["keyfile_automation_info"], justify="left", wraplength=750)
|
||||
keyfile_info_label.pack(fill=tk.X, pady=(5, 15))
|
||||
|
||||
key_file_button = ttk.Button(
|
||||
self.keyfile_settings_frame, text=Msg.STR["create_add_key_file"], command=self._create_key_file)
|
||||
key_file_button.grid(row=0, column=0, padx=5, pady=5)
|
||||
scan_button = ttk.Button(self.keyfile_settings_frame, text="Scan Destination for Encrypted Profiles", command=self._populate_keyfile_profiles_view)
|
||||
scan_button.pack(pady=5)
|
||||
|
||||
self.key_file_status_var = tk.StringVar(
|
||||
value=Msg.STR["key_file_not_created"])
|
||||
key_file_status_label = ttk.Label(
|
||||
self.keyfile_settings_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 = Msg.STR["sudoers_info_text"]
|
||||
sudoers_info_label = ttk.Label(
|
||||
self.keyfile_settings_frame, text=sudoers_info_text, justify="left")
|
||||
|
||||
sudoers_info_label.grid(
|
||||
row=1, column=0, columnspan=2, sticky="w", padx=5, pady=5)
|
||||
|
||||
self.keyfile_settings_frame.columnconfigure(1, weight=1)
|
||||
self.keyfile_profiles_frame = ttk.Frame(self.keyfile_settings_frame)
|
||||
self.keyfile_profiles_frame.pack(fill=tk.BOTH, expand=True, pady=10)
|
||||
|
||||
# --- Bottom Buttons (for most views) ---
|
||||
self.bottom_button_frame = ttk.Frame(self)
|
||||
self.bottom_button_frame.pack(pady=10)
|
||||
|
||||
@@ -206,7 +201,6 @@ class AdvancedSettingsFrame(ttk.Frame):
|
||||
self._load_animation_settings()
|
||||
self._load_backup_defaults()
|
||||
self._load_manual_excludes()
|
||||
self._update_key_file_status()
|
||||
|
||||
self._switch_view(self.current_view_index)
|
||||
|
||||
@@ -243,61 +237,140 @@ class AdvancedSettingsFrame(ttk.Frame):
|
||||
self.config_manager.set_setting("use_trash_bin", use_trash)
|
||||
self.config_manager.set_setting("no_trash_bin", no_trash)
|
||||
|
||||
def _create_key_file(self):
|
||||
if not self.app_instance.destination_path:
|
||||
MessageDialog(message_type="error", title="Error",
|
||||
text="Please select a backup destination first.")
|
||||
def _populate_keyfile_profiles_view(self):
|
||||
# Clear existing widgets
|
||||
for widget in self.keyfile_profiles_frame.winfo_children():
|
||||
widget.destroy()
|
||||
self.keyfile_profile_widgets.clear()
|
||||
|
||||
destination = self.app_instance.destination_path
|
||||
if not destination:
|
||||
MessageDialog(message_type="error", title=Msg.STR["error"],
|
||||
text=Msg.STR["select_destination_first"]).show()
|
||||
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(message_type="error", title="Error",
|
||||
text="No encrypted container found at the destination.")
|
||||
pybackup_dir = os.path.join(destination, "pybackup")
|
||||
if not os.path.isdir(pybackup_dir):
|
||||
ttk.Label(self.keyfile_profiles_frame, text="No 'pybackup' directory found at destination.").pack()
|
||||
return
|
||||
|
||||
search_pattern = os.path.join(pybackup_dir, "pybackup_encrypted_*.luks")
|
||||
found_containers = glob.glob(search_pattern)
|
||||
|
||||
if not found_containers:
|
||||
ttk.Label(self.keyfile_profiles_frame, text="No encrypted profiles found at destination.").pack()
|
||||
return
|
||||
|
||||
profiles = []
|
||||
for container in found_containers:
|
||||
filename = os.path.basename(container)
|
||||
profile_name = filename.replace("pybackup_encrypted_", "").replace(".luks", "")
|
||||
if profile_name:
|
||||
profiles.append(profile_name)
|
||||
|
||||
# Master "All" checkbox
|
||||
all_var = tk.BooleanVar()
|
||||
all_check = ttk.Checkbutton(self.keyfile_profiles_frame, text="All Profiles", variable=all_var, style="Switch.TCheckbutton",
|
||||
command=lambda v=all_var: self._on_master_keyfile_toggle(v, profiles))
|
||||
all_check.pack(anchor="w", padx=5, pady=5)
|
||||
ttk.Separator(self.keyfile_profiles_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=5)
|
||||
|
||||
# Individual profile checkboxes
|
||||
for profile_name in sorted(profiles):
|
||||
profile_frame = ttk.Frame(self.keyfile_profiles_frame)
|
||||
profile_frame.pack(fill=tk.X, padx=5, pady=2)
|
||||
|
||||
var = tk.BooleanVar()
|
||||
check = ttk.Checkbutton(profile_frame, text=profile_name, variable=var, style="Switch.TCheckbutton")
|
||||
check.pack(side=tk.LEFT)
|
||||
# Use a lambda with default arguments to capture the current values
|
||||
check.config(command=lambda p=profile_name, v=var: self._on_keyfile_checkbox_toggle(p, v))
|
||||
|
||||
status_label = ttk.Label(profile_frame, text="", foreground="gray")
|
||||
status_label.pack(side=tk.LEFT, padx=10)
|
||||
|
||||
self.keyfile_profile_widgets[profile_name] = {"var": var, "status_label": status_label}
|
||||
self._update_single_keyfile_status(profile_name)
|
||||
|
||||
def _on_master_keyfile_toggle(self, var, profiles):
|
||||
if not var.get():
|
||||
return # Do nothing on uncheck for safety
|
||||
|
||||
password_dialog = PasswordDialog(
|
||||
self, title="Enter Existing Password", confirm=False, translations=Msg.STR)
|
||||
self, title=Msg.STR["enter_existing_password_title"], confirm=False, translations=Msg.STR)
|
||||
password, _ = password_dialog.get_password()
|
||||
|
||||
if not password:
|
||||
var.set(False) # Uncheck the master switch
|
||||
return
|
||||
|
||||
for profile_name in profiles:
|
||||
self._create_key_file_for_profile(profile_name, password)
|
||||
|
||||
def _on_keyfile_checkbox_toggle(self, profile_name, var):
|
||||
if var.get(): # If checked, create key
|
||||
self._create_key_file_for_profile(profile_name)
|
||||
else: # If unchecked, delete key
|
||||
self._delete_key_file_for_profile(profile_name)
|
||||
|
||||
def _create_key_file_for_profile(self, profile_name, password=None):
|
||||
header = self.app_instance.header_frame
|
||||
destination = self.app_instance.destination_path
|
||||
|
||||
if not password:
|
||||
password_dialog = PasswordDialog(
|
||||
self, title=f"{Msg.STR['enter_existing_password_title']} for {profile_name}", confirm=False, translations=Msg.STR)
|
||||
password, _ = password_dialog.get_password()
|
||||
if not password:
|
||||
self._update_single_keyfile_status(profile_name) # Revert checkbox state
|
||||
return
|
||||
|
||||
key_file_path = self.app_instance.backup_manager.encryption_manager.create_and_add_key_file(
|
||||
self.app_instance.destination_path, password)
|
||||
destination, profile_name, password)
|
||||
|
||||
if key_file_path:
|
||||
MessageDialog(message_type="info", title="Success",
|
||||
text=f"Key file created and added successfully!\nPath: {key_file_path}")
|
||||
header.show_temporary_message(Msg.STR["keyfile_creation_success"])
|
||||
else:
|
||||
MessageDialog(message_type="error", title="Error",
|
||||
text="Failed to create or add key file. See log for details.")
|
||||
header.show_temporary_message(Msg.STR["keyfile_creation_failed"])
|
||||
|
||||
self._update_single_keyfile_status(profile_name)
|
||||
|
||||
self._update_key_file_status()
|
||||
def _delete_key_file_for_profile(self, profile_name):
|
||||
header = self.app_instance.header_frame
|
||||
destination = self.app_instance.destination_path
|
||||
|
||||
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).")
|
||||
password_dialog = PasswordDialog(
|
||||
self, title=f"Password to delete key for {profile_name}", confirm=False, translations=Msg.STR)
|
||||
password, _ = password_dialog.get_password()
|
||||
if not password:
|
||||
self._update_single_keyfile_status(profile_name) # Revert checkbox state
|
||||
return
|
||||
|
||||
# Determine the profile_name based on the current source folder
|
||||
source_name = self.app_instance.left_canvas_data.get('folder')
|
||||
if not source_name:
|
||||
# Fallback or handle case where no source is selected, perhaps default to "system" or log an error
|
||||
# For now, let's assume "system" if no specific folder is selected for keyfile status
|
||||
profile_name = "system"
|
||||
else:
|
||||
profile_name = "system" if source_name == "Computer" else source_name
|
||||
success = self.app_instance.backup_manager.encryption_manager.delete_key_file(
|
||||
destination, profile_name, password)
|
||||
|
||||
key_file_path = self.app_instance.backup_manager.encryption_manager.get_key_file_path(
|
||||
self.app_instance.destination_path, profile_name)
|
||||
if os.path.exists(key_file_path):
|
||||
self.key_file_status_var.set(f"Key file exists: {key_file_path}")
|
||||
if success:
|
||||
header.show_temporary_message("Keyfile successfully deleted.")
|
||||
else:
|
||||
self.key_file_status_var.set(
|
||||
"Key file has not been created for this destination.")
|
||||
header.show_temporary_message("Failed to delete keyfile.")
|
||||
|
||||
self._update_single_keyfile_status(profile_name)
|
||||
|
||||
def _update_single_keyfile_status(self, profile_name):
|
||||
widgets = self.keyfile_profile_widgets.get(profile_name)
|
||||
if not widgets:
|
||||
return
|
||||
|
||||
destination = self.app_instance.destination_path
|
||||
key_file_path = self.app_instance.backup_manager.encryption_manager.get_key_file_path(
|
||||
destination, profile_name)
|
||||
|
||||
if os.path.exists(key_file_path):
|
||||
widgets["var"].set(True)
|
||||
widgets["status_label"].config(text="Keyfile exists")
|
||||
else:
|
||||
widgets["var"].set(False)
|
||||
widgets["status_label"].config(text="")
|
||||
|
||||
def _switch_view(self, index):
|
||||
self.current_view_index = index
|
||||
@@ -309,8 +382,7 @@ class AdvancedSettingsFrame(ttk.Frame):
|
||||
self.animation_settings_frame.pack_forget()
|
||||
self.backup_defaults_frame.pack_forget()
|
||||
|
||||
# Show/hide the main action buttons based on the view
|
||||
if index == 1: # Manual Excludes view
|
||||
if index in [1, 2]:
|
||||
self.bottom_button_frame.pack_forget()
|
||||
else:
|
||||
self.bottom_button_frame.pack(pady=10)
|
||||
@@ -326,7 +398,7 @@ class AdvancedSettingsFrame(ttk.Frame):
|
||||
elif index == 2:
|
||||
self.keyfile_settings_frame.pack(fill=tk.BOTH, expand=True)
|
||||
self.info_label.config(text="")
|
||||
self._update_key_file_status()
|
||||
self._populate_keyfile_profiles_view()
|
||||
elif index == 3:
|
||||
self.animation_settings_frame.pack(fill=tk.BOTH, expand=True)
|
||||
self.info_label.config(text="")
|
||||
@@ -357,8 +429,11 @@ class AdvancedSettingsFrame(ttk.Frame):
|
||||
|
||||
def _delete_manual_exclude(self):
|
||||
selected_indices = self.manual_excludes_listbox.curselection()
|
||||
if not selected_indices:
|
||||
return
|
||||
for i in reversed(selected_indices):
|
||||
self.manual_excludes_listbox.delete(i)
|
||||
self.app_instance.header_frame.show_temporary_message(Msg.STR["delete_success"])
|
||||
self._apply_changes()
|
||||
|
||||
def _reset_animation_settings(self):
|
||||
@@ -550,13 +625,7 @@ class AdvancedSettingsFrame(ttk.Frame):
|
||||
f.write(f"{item}\n")
|
||||
|
||||
# Show temporary success message in header
|
||||
self.app_instance.header_frame.show_temporary_message("Settings saved successfully!")
|
||||
|
||||
if self.app_instance:
|
||||
current_source = self.app_instance.left_canvas_data.get('folder')
|
||||
if current_source:
|
||||
self.app_instance.actions.on_sidebar_button_click(
|
||||
current_source)
|
||||
self.app_instance.header_frame.show_temporary_message(Msg.STR["settings_saved"])
|
||||
|
||||
def _cancel_changes(self):
|
||||
self.pack_forget()
|
||||
|
@@ -290,7 +290,21 @@ class Drawing:
|
||||
elif self.app.is_first_backup:
|
||||
info_messages.append(Msg.STR["ready_for_first_backup"])
|
||||
elif self.app.mode == "backup":
|
||||
# Base message is always shown after calculation in backup mode
|
||||
info_messages.append(Msg.STR["backup_mode_info"])
|
||||
|
||||
# Additionally, show a warning if encryption is on but the drive is not mounted
|
||||
if self.app.encrypted_var.get():
|
||||
dest_path = self.app.destination_path
|
||||
profile_name = self.app.left_canvas_data.get('folder')
|
||||
|
||||
is_mounted = False
|
||||
if dest_path and profile_name:
|
||||
is_mounted = self.app.backup_manager.encryption_manager.is_mounted(
|
||||
dest_path, profile_name)
|
||||
|
||||
if not is_mounted:
|
||||
info_messages.append(Msg.STR["encrypted_unmounted_warning"])
|
||||
else:
|
||||
info_messages.append(Msg.STR["restore_mode_info"])
|
||||
|
||||
|
@@ -72,18 +72,32 @@ class HeaderFrame(tk.Frame):
|
||||
|
||||
self.refresh_status()
|
||||
|
||||
def show_temporary_message(self, message: str, duration_ms: int = 4000):
|
||||
"""Displays a temporary message in the subtitle area."""
|
||||
# Cancel any previous after job to prevent conflicts
|
||||
if self._temp_message_after_id:
|
||||
self.after_cancel(self._temp_message_after_id)
|
||||
def show_temporary_message(self, message: str, duration_ms: int = 3000):
|
||||
"""Displays a large, centered, temporary message over the header."""
|
||||
# If a message is already shown, cancel its removal and destroy it
|
||||
if hasattr(self, '_temp_message_label') and self._temp_message_label.winfo_exists():
|
||||
if self._temp_message_after_id:
|
||||
self.after_cancel(self._temp_message_after_id)
|
||||
self._temp_message_label.destroy()
|
||||
|
||||
self.subtitle_label.config(text=message, fg="#2ECC71") # Green color for success
|
||||
# Create a new label for the message
|
||||
self._temp_message_label = tk.Label(
|
||||
self, # Place it directly in the HeaderFrame
|
||||
text=message,
|
||||
font=("Helvetica", 16, "bold"),
|
||||
fg="#2ECC71", # Green color for success
|
||||
bg="#455A64" # Same background as header
|
||||
)
|
||||
# Place it in the center of the header frame
|
||||
self._temp_message_label.place(relx=0.5, rely=0.5, anchor="center")
|
||||
|
||||
# Schedule its destruction
|
||||
self._temp_message_after_id = self.after(duration_ms, self._restore_subtitle)
|
||||
|
||||
def _restore_subtitle(self):
|
||||
"""Restores the original subtitle text and color."""
|
||||
self.subtitle_label.config(text=self.original_subtitle_text, fg="#bdc3c7")
|
||||
"""Destroys the temporary message label."""
|
||||
if hasattr(self, '_temp_message_label') and self._temp_message_label.winfo_exists():
|
||||
self._temp_message_label.destroy()
|
||||
self._temp_message_after_id = None
|
||||
|
||||
def refresh_status(self):
|
||||
|
@@ -9,6 +9,7 @@ from core.pbp_app_config import AppConfig, Msg
|
||||
from pyimage_ui.advanced_settings_frame import AdvancedSettingsFrame
|
||||
from shared_libs.custom_file_dialog import CustomFileDialog
|
||||
from shared_libs.message import MessageDialog, PasswordDialog
|
||||
from shared_libs.logger import app_logger
|
||||
|
||||
|
||||
class SettingsFrame(ttk.Frame):
|
||||
@@ -45,19 +46,12 @@ class SettingsFrame(ttk.Frame):
|
||||
self.button_frame, text=Msg.STR["add_to_exclude_list"], command=self._add_to_exclude_list, style="Gray.Toolbutton")
|
||||
add_to_exclude_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack(
|
||||
side=tk.LEFT, ipady=15, padx=5)
|
||||
|
||||
reset_button = ttk.Button(
|
||||
self.button_frame, text=Msg.STR["default_settings"], command=self.actions.reset_to_default_settings, style="Gray.Toolbutton")
|
||||
reset_button.pack(side=tk.LEFT)
|
||||
|
||||
ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack(
|
||||
side=tk.LEFT, ipady=15, padx=5)
|
||||
|
||||
# Right-aligned buttons
|
||||
hard_reset_button = ttk.Button(
|
||||
self.button_frame, text=Msg.STR["hard_reset"], command=self._toggle_hard_reset_view, style="Gray.Toolbutton")
|
||||
self.button_frame, text=Msg.STR["reset"], command=self._toggle_hard_reset_view, style="Gray.Toolbutton")
|
||||
hard_reset_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack(
|
||||
@@ -79,10 +73,6 @@ class SettingsFrame(ttk.Frame):
|
||||
self.bottom_button_frame, text=Msg.STR["apply"], command=self._apply_changes)
|
||||
apply_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
cancel_button = ttk.Button(self.bottom_button_frame, text=Msg.STR["cancel"],
|
||||
command=lambda: self.navigation.toggle_mode("backup", 0))
|
||||
cancel_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# --- Treeview for file/folder exclusion ---
|
||||
self.tree_frame = ttk.LabelFrame(
|
||||
self.trees_container, text=Msg.STR["user_defined_folder_settings"], padding=10)
|
||||
@@ -121,50 +111,122 @@ class SettingsFrame(ttk.Frame):
|
||||
self.hidden_tree.bind("<Button-1>", self._toggle_include_status_hidden)
|
||||
self.hidden_tree_frame.pack_forget() # Initially hidden
|
||||
|
||||
# --- Default Reset Frame (initially hidden) ---
|
||||
self.default_reset_frame = ttk.LabelFrame(
|
||||
self, text=Msg.STR["default_config_settings"], padding=10)
|
||||
|
||||
default_reset_label = ttk.Label(
|
||||
self.default_reset_frame, text=Msg.STR["default_reset_info"], wraplength=400, justify=tk.CENTER)
|
||||
default_reset_label.pack(pady=10, expand=True)
|
||||
|
||||
# Frame to center the button
|
||||
default_reset_button_frame = ttk.Frame(self.default_reset_frame)
|
||||
default_reset_button_frame.pack(pady=5)
|
||||
|
||||
reset_button = ttk.Button(
|
||||
default_reset_button_frame, text=Msg.STR["default_settings"], command=self._reset_to_default_settings)
|
||||
reset_button.pack() # Centered by default in its own frame
|
||||
|
||||
# --- Hard Reset Frame (initially hidden) ---
|
||||
self.hard_reset_frame = ttk.LabelFrame(
|
||||
self, text=Msg.STR["full_delete_config_settings"], padding=10)
|
||||
|
||||
hard_reset_label = ttk.Label(
|
||||
self.hard_reset_frame, text=Msg.STR["hard_reset_warning"], wraplength=400, justify=tk.LEFT)
|
||||
hard_reset_label.pack(pady=10)
|
||||
self.hard_reset_frame, text=Msg.STR["hard_reset_warning"], wraplength=400, justify=tk.CENTER)
|
||||
hard_reset_label.pack(pady=10, expand=True)
|
||||
|
||||
hard_reset_button_frame = ttk.Frame(self.hard_reset_frame)
|
||||
hard_reset_button_frame.pack(pady=10)
|
||||
hard_reset_button_frame.pack(pady=5)
|
||||
|
||||
delete_now_button = ttk.Button(
|
||||
self.delete_now_button = ttk.Button(
|
||||
hard_reset_button_frame, text=Msg.STR["delete_now"], command=self._perform_hard_reset)
|
||||
delete_now_button.pack(side=tk.LEFT, padx=5)
|
||||
self.delete_now_button.pack() # Centered by default
|
||||
|
||||
cancel_hard_reset_button = ttk.Button(
|
||||
hard_reset_button_frame, text=Msg.STR["cancel"], command=self._toggle_hard_reset_view)
|
||||
cancel_hard_reset_button.pack(side=tk.LEFT, padx=5)
|
||||
# --- Bottom Cancel Button for Reset View (initially hidden) ---
|
||||
self.reset_view_cancel_frame = ttk.Frame(self)
|
||||
self.reset_view_cancel_button = ttk.Button(
|
||||
self.reset_view_cancel_frame, text=Msg.STR["cancel"], command=self._toggle_hard_reset_view)
|
||||
self.reset_view_cancel_button.pack(pady=10)
|
||||
|
||||
self.hidden_files_visible = False
|
||||
self.hard_reset_visible = False
|
||||
# To hold the instance of AdvancedSettingsFrame
|
||||
self.advanced_settings_frame_instance = None
|
||||
|
||||
def _perform_hard_reset(self):
|
||||
if self.encryption_manager.mounted_destinations:
|
||||
dialog = PasswordDialog(
|
||||
self, title=Msg.STR["unlock_backup"], confirm=False, translations=Msg.STR)
|
||||
password, _ = dialog.get_password()
|
||||
if not password:
|
||||
return
|
||||
|
||||
success, message = self.encryption_manager.unmount_all_encrypted_drives(
|
||||
password)
|
||||
if not success:
|
||||
MessageDialog(message_type="error", text=message).show()
|
||||
return
|
||||
|
||||
def _reset_to_default_settings(self):
|
||||
app_instance = self.master.master.master
|
||||
header = app_instance.header_frame
|
||||
try:
|
||||
shutil.rmtree(AppConfig.APP_DIR)
|
||||
# Restart the application
|
||||
os.execl(sys.executable, sys.executable, *sys.argv)
|
||||
self.config_manager.set_setting("backup_destination_path", None)
|
||||
self.config_manager.set_setting("restore_source_path", None)
|
||||
self.config_manager.set_setting("restore_destination_path", None)
|
||||
|
||||
self.config_manager.remove_setting("backup_animation_type")
|
||||
self.config_manager.remove_setting("calculation_animation_type")
|
||||
self.config_manager.remove_setting("force_full_backup")
|
||||
self.config_manager.remove_setting("force_incremental_backup")
|
||||
self.config_manager.remove_setting("force_compression")
|
||||
self.config_manager.remove_setting("force_encryption")
|
||||
|
||||
app_instance.update_backup_options_from_config()
|
||||
|
||||
AppConfig.generate_and_write_final_exclude_list()
|
||||
app_logger.log("Settings have been reset to default values.")
|
||||
|
||||
self.load_and_display_excludes()
|
||||
if self.hidden_files_visible:
|
||||
self._load_hidden_files()
|
||||
|
||||
app_instance.destination_path = None
|
||||
app_instance.start_cancel_button.config(state="disabled")
|
||||
|
||||
app_instance.backup_left_canvas_data.clear()
|
||||
app_instance.backup_right_canvas_data.clear()
|
||||
app_instance.restore_left_canvas_data.clear()
|
||||
app_instance.restore_right_canvas_data.clear()
|
||||
|
||||
current_source = app_instance.left_canvas_data.get('folder')
|
||||
if current_source:
|
||||
self.actions.on_sidebar_button_click(current_source)
|
||||
|
||||
app_instance.backup_content_frame.system_backups_frame._load_backup_content()
|
||||
app_instance.backup_content_frame.user_backups_frame._load_backup_content()
|
||||
|
||||
header.show_temporary_message(Msg.STR["default_reset_success"])
|
||||
|
||||
except Exception as e:
|
||||
MessageDialog(message_type="error", text=str(e)).show()
|
||||
app_logger.log(f"Error resetting settings: {e}")
|
||||
header.show_temporary_message(Msg.STR["default_reset_failed"])
|
||||
|
||||
def _perform_hard_reset(self):
|
||||
try:
|
||||
# First, always attempt to unmount all encrypted drives
|
||||
unmount_success = self.encryption_manager.unmount_all()
|
||||
|
||||
# If unmounting fails, show an error and prevent the app from closing
|
||||
if not unmount_success and self.encryption_manager.mounted_destinations:
|
||||
app_logger.log(
|
||||
"WARNING: Not all encrypted drives could be unmounted. Preventing application closure.")
|
||||
MessageDialog(
|
||||
message_type="error", title=Msg.STR["unmount_failed_title"], text=Msg.STR["unmount_failed_message"])
|
||||
return # Prevent application from closing
|
||||
|
||||
# Try to delete the config directory without elevated rights
|
||||
|
||||
if AppConfig.APP_DIR.exists():
|
||||
shutil.rmtree(AppConfig.APP_DIR)
|
||||
|
||||
app_instance = self.master.master.master
|
||||
header = app_instance.header_frame
|
||||
header.show_temporary_message(Msg.STR["hard_reset_success"])
|
||||
AppConfig.ensure_directories()
|
||||
|
||||
self.after(2000, lambda: os.execl(
|
||||
sys.executable, sys.executable, *sys.argv))
|
||||
|
||||
except Exception as e:
|
||||
MessageDialog(message_type="error",
|
||||
text=f"Hard reset failed: {e}").show()
|
||||
|
||||
def _toggle_hard_reset_view(self):
|
||||
self.hard_reset_visible = not self.hard_reset_visible
|
||||
@@ -172,10 +234,16 @@ class SettingsFrame(ttk.Frame):
|
||||
self.trees_container.pack_forget()
|
||||
self.button_frame.pack_forget()
|
||||
self.bottom_button_frame.pack_forget()
|
||||
self.default_reset_frame.pack(
|
||||
fill=tk.X, expand=True, padx=10, pady=5)
|
||||
self.hard_reset_frame.pack(
|
||||
fill=tk.BOTH, expand=True, padx=10, pady=5)
|
||||
fill=tk.X, expand=True, padx=10, pady=5)
|
||||
self.reset_view_cancel_frame.pack(fill=tk.X, side=tk.BOTTOM)
|
||||
|
||||
else:
|
||||
self.default_reset_frame.pack_forget()
|
||||
self.hard_reset_frame.pack_forget()
|
||||
self.reset_view_cancel_frame.pack_forget()
|
||||
self.button_frame.pack(fill=tk.X, padx=10)
|
||||
self.trees_container.pack(
|
||||
fill=tk.BOTH, expand=True, padx=10, pady=5)
|
||||
@@ -338,6 +406,10 @@ class SettingsFrame(ttk.Frame):
|
||||
if self.hidden_files_visible:
|
||||
self._load_hidden_files()
|
||||
|
||||
# Show success message and stay on the settings page
|
||||
self.master.master.master.header_frame.show_temporary_message(
|
||||
Msg.STR["settings_saved"])
|
||||
|
||||
def _open_advanced_settings(self):
|
||||
# Hide main settings UI elements
|
||||
self.trees_container.pack_forget() # Hide the container for treeviews
|
||||
|
Reference in New Issue
Block a user