Replaced the LVM-on-a-file implementation with a more robust, industry-standard LUKS-on-a-file approach. This change was motivated by persistent and hard-to-debug errors related to LVM state management and duplicate loop device detection during repeated mount/unmount cycles. The new implementation provides several key benefits: - **Robustness:** Eliminates the entire LVM layer, which was the root cause of the mount/unmount failures. - **Improved UX:** Drastically reduces the number of password prompts for encrypted user backups. By changing ownership of the mountpoint, rsync can run with user privileges. - **Enhanced Security:** The file transfer process (rsync) for user backups no longer runs with root privileges. - **Better Usability:** Encrypted containers are now left mounted during the application's lifecycle and are only unmounted on exit, improving workflow for consecutive operations.
307 lines
13 KiB
Python
307 lines
13 KiB
Python
# pyimage_ui/navigation.py
|
|
import os
|
|
import shutil
|
|
from shared_libs.message import MessageDialog
|
|
from core.pbp_app_config import Msg
|
|
|
|
|
|
class Navigation:
|
|
def __init__(self, app):
|
|
self.app = app
|
|
|
|
def _cancel_calculation(self):
|
|
|
|
if self.app.calculation_thread and self.app.calculation_thread.is_alive():
|
|
self.app.calculation_stop_event.set()
|
|
if self.app.right_calculation_thread and self.app.right_calculation_thread.is_alive():
|
|
self.app.right_calculation_stop_event.set()
|
|
|
|
# Stop all calculation animations and update UI
|
|
self.app.drawing._stop_calculating_animation() # Stops both left and right
|
|
if self.app.left_canvas_data:
|
|
self.app.left_canvas_data['calculating'] = False
|
|
if self.app.right_canvas_data:
|
|
self.app.right_canvas_data['calculating'] = False
|
|
self.app.drawing.redraw_left_canvas()
|
|
self.app.drawing.redraw_right_canvas()
|
|
|
|
def initialize_ui_for_mode(self, mode):
|
|
"""Resets and initializes the UI components for the given mode."""
|
|
self.app.mode = mode
|
|
self._cancel_calculation()
|
|
|
|
if mode == "backup":
|
|
self.app.left_canvas_data = self.app.backup_left_canvas_data
|
|
self.app.right_canvas_data = self.app.backup_right_canvas_data
|
|
active_index = 0
|
|
|
|
# Restore backup destination disk metrics
|
|
backup_dest_path = self.app.backup_right_canvas_data.get(
|
|
'path_display')
|
|
if backup_dest_path and os.path.isdir(backup_dest_path):
|
|
try:
|
|
total, used, free = shutil.disk_usage(backup_dest_path)
|
|
self.app.destination_total_bytes = total
|
|
self.app.destination_used_bytes = used
|
|
except FileNotFoundError:
|
|
self.app.destination_total_bytes = 0
|
|
self.app.destination_used_bytes = 0
|
|
else:
|
|
self.app.destination_total_bytes = 0
|
|
self.app.destination_used_bytes = 0
|
|
|
|
else: # restore
|
|
self.app.left_canvas_data = self.app.restore_left_canvas_data
|
|
self.app.right_canvas_data = self.app.restore_right_canvas_data
|
|
active_index = 1
|
|
|
|
# Restore restore destination disk metrics
|
|
restore_dest_path = self.app.restore_left_canvas_data.get(
|
|
'path_display')
|
|
if restore_dest_path and os.path.isdir(restore_dest_path):
|
|
try:
|
|
total, used, free = shutil.disk_usage(restore_dest_path)
|
|
self.app.destination_total_bytes = total
|
|
self.app.destination_used_bytes = used
|
|
except FileNotFoundError:
|
|
self.app.destination_total_bytes = 0
|
|
self.app.destination_used_bytes = 0
|
|
else:
|
|
self.app.destination_total_bytes = 0
|
|
self.app.destination_used_bytes = 0
|
|
|
|
# Set default content if the dictionaries are empty
|
|
if not self.app.left_canvas_data:
|
|
if mode == "backup":
|
|
# In backup mode, left is source, default to Computer
|
|
self.app.left_canvas_data.update({
|
|
'icon': 'computer_extralarge',
|
|
'folder': 'Computer',
|
|
'path_display': '',
|
|
'size': ''
|
|
})
|
|
else: # In restore mode, left is destination
|
|
self.app.left_canvas_data.update({
|
|
'icon': 'computer_extralarge',
|
|
'folder': 'Computer',
|
|
'path_display': '',
|
|
'size': ''
|
|
})
|
|
|
|
if not self.app.right_canvas_data:
|
|
# Right canvas is always the destination/HDD view conceptually
|
|
self.app.right_canvas_data.update({
|
|
'icon': 'hdd_extralarge',
|
|
'folder': Msg.STR["select_destination"] if mode == 'backup' else Msg.STR["select_source"],
|
|
'path_display': '',
|
|
'size': ''
|
|
})
|
|
|
|
# Update UI elements
|
|
if mode == "backup":
|
|
self.app.source_size_frame.grid()
|
|
self.app.target_size_frame.grid()
|
|
self.app.restore_size_frame_before.grid_remove()
|
|
self.app.restore_size_frame_after.grid_remove()
|
|
self.app.mode_button_icon = self.app.image_manager.get_icon(
|
|
"forward_extralarge")
|
|
self.app.info_label.config(text=Msg.STR["backup_mode_info"])
|
|
self.app.full_backup_cb.config(state="normal")
|
|
self.app.incremental_cb.config(state="normal")
|
|
self.app.compressed_cb.config(state="normal")
|
|
self.app.encrypted_cb.config(state="normal")
|
|
self.app.bypass_security_cb.config(
|
|
state='disabled') # This one is mode-dependent
|
|
# Let the central config function handle the state of these checkboxes
|
|
self.app.update_backup_options_from_config()
|
|
else: # restore
|
|
self.app.source_size_frame.grid_remove()
|
|
self.app.target_size_frame.grid_remove()
|
|
self.app.restore_size_frame_before.grid()
|
|
self.app.restore_size_frame_after.grid()
|
|
self.app.mode_button_icon = self.app.image_manager.get_icon(
|
|
"back_extralarge")
|
|
self.app.info_label.config(text=Msg.STR["restore_mode_info"])
|
|
self.app.full_backup_cb.config(state='disabled')
|
|
self.app.incremental_cb.config(state='disabled')
|
|
self.app.compressed_cb.config(state='disabled')
|
|
self.app.encrypted_cb.config(state='disabled')
|
|
self.app.bypass_security_cb.config(
|
|
state='normal') # This one is mode-dependent
|
|
|
|
self.app.mode_button.config(image=self.app.mode_button_icon)
|
|
self.app.drawing.update_nav_buttons(active_index)
|
|
self.app.drawing.redraw_left_canvas()
|
|
self.app.drawing.redraw_right_canvas()
|
|
|
|
def toggle_mode(self, mode=None, active_index=None, trigger_calculation=True):
|
|
if self.app.backup_is_running:
|
|
# If a backup is running, we only want to switch the view to the main backup screen.
|
|
# We don't reset anything.
|
|
self.app.log_frame.grid_remove()
|
|
self.app.scheduler_frame.hide()
|
|
self.app.settings_frame.hide()
|
|
self.app.backup_content_frame.grid_remove()
|
|
|
|
|
|
# Show the main content frames
|
|
self.app.canvas_frame.grid()
|
|
self.app.top_bar.grid()
|
|
# Ensures action_frame is visible
|
|
self._update_task_bar_visibility("backup")
|
|
|
|
# Restore visibility of size frames based on the current mode
|
|
if self.app.mode == 'backup':
|
|
self.app.source_size_frame.grid()
|
|
self.app.target_size_frame.grid()
|
|
else: # restore
|
|
self.app.restore_size_frame_before.grid()
|
|
self.app.restore_size_frame_after.grid()
|
|
|
|
# Update the top nav button highlight
|
|
if active_index is not None:
|
|
self.app.drawing.update_nav_buttons(active_index)
|
|
return
|
|
|
|
# --- Original logic if no backup is running ---
|
|
self.app.drawing.reset_projection_canvases()
|
|
self._cancel_calculation()
|
|
|
|
# Save current state before switching
|
|
if self.app.mode == 'backup':
|
|
self.app.backup_left_canvas_data = self.app.left_canvas_data
|
|
self.app.backup_right_canvas_data = self.app.right_canvas_data
|
|
else:
|
|
self.app.restore_left_canvas_data = self.app.left_canvas_data
|
|
self.app.restore_right_canvas_data = self.app.right_canvas_data
|
|
|
|
if mode is None: # Clicked the middle arrow button
|
|
new_mode = "restore" if self.app.mode == "backup" else "backup"
|
|
else:
|
|
new_mode = mode
|
|
|
|
self.initialize_ui_for_mode(new_mode)
|
|
|
|
# Hide/show frames
|
|
self.app.log_frame.grid_remove()
|
|
self.app.scheduler_frame.hide()
|
|
self.app.settings_frame.hide()
|
|
self.app.backup_content_frame.grid_remove()
|
|
|
|
self.app.canvas_frame.grid()
|
|
self.app.source_size_frame.grid()
|
|
self.app.target_size_frame.grid()
|
|
self._update_task_bar_visibility(self.app.mode)
|
|
|
|
if trigger_calculation:
|
|
# Always trigger the calculation for the left canvas, which is controlled by the sidebar
|
|
current_folder_on_left = self.app.left_canvas_data.get('folder')
|
|
if current_folder_on_left and current_folder_on_left in self.app.buttons_map:
|
|
self.app.actions.on_sidebar_button_click(
|
|
current_folder_on_left)
|
|
|
|
# Additionally, in restore mode, trigger calculation for the right canvas if a source is set
|
|
if self.app.mode == 'restore':
|
|
if self.app.right_canvas_data.get('path_display'):
|
|
self.app.drawing.calculate_restore_folder_size()
|
|
|
|
def _update_task_bar_visibility(self, mode):
|
|
if mode in ["backup", "restore"]:
|
|
self.app.info_checkbox_frame.grid()
|
|
self.app.action_frame.grid()
|
|
elif mode == "log":
|
|
self.app.info_checkbox_frame.grid_remove()
|
|
self.app.action_frame.grid()
|
|
elif mode in ["scheduler", "settings"]:
|
|
self.app.info_checkbox_frame.grid_remove()
|
|
self.app.action_frame.grid_remove()
|
|
|
|
def toggle_log_window(self, active_index=None):
|
|
self._cancel_calculation()
|
|
if active_index is not None:
|
|
self.app.drawing.update_nav_buttons(active_index)
|
|
|
|
self.app.canvas_frame.grid_remove()
|
|
self.app.scheduler_frame.hide()
|
|
self.app.settings_frame.hide()
|
|
self.app.backup_content_frame.grid_remove()
|
|
self.app.source_size_frame.grid_remove()
|
|
self.app.target_size_frame.grid_remove()
|
|
self.app.restore_size_frame_before.grid_remove()
|
|
self.app.restore_size_frame_after.grid_remove()
|
|
self.app.log_frame.grid()
|
|
self.app.top_bar.grid()
|
|
self._update_task_bar_visibility("log")
|
|
|
|
def toggle_scheduler_frame(self, active_index=None):
|
|
self._cancel_calculation()
|
|
if active_index is not None:
|
|
self.app.drawing.update_nav_buttons(active_index)
|
|
|
|
self.app.canvas_frame.grid_remove()
|
|
self.app.log_frame.grid_remove()
|
|
self.app.settings_frame.hide()
|
|
self.app.backup_content_frame.grid_remove()
|
|
|
|
self.app.source_size_frame.grid_remove()
|
|
self.app.target_size_frame.grid_remove()
|
|
self.app.restore_size_frame_before.grid_remove()
|
|
self.app.restore_size_frame_after.grid_remove()
|
|
self.app.scheduler_frame.show()
|
|
self.app.top_bar.grid()
|
|
self._update_task_bar_visibility("scheduler")
|
|
|
|
def toggle_settings_frame(self, active_index=None):
|
|
self._cancel_calculation()
|
|
if active_index is not None:
|
|
self.app.drawing.update_nav_buttons(active_index)
|
|
|
|
self.app.canvas_frame.grid_remove()
|
|
self.app.log_frame.grid_remove()
|
|
self.app.backup_content_frame.grid_remove()
|
|
self.app.scheduler_frame.hide()
|
|
|
|
self.app.source_size_frame.grid_remove()
|
|
self.app.target_size_frame.grid_remove()
|
|
self.app.restore_size_frame_before.grid_remove()
|
|
self.app.restore_size_frame_after.grid_remove()
|
|
self.app.settings_frame.show()
|
|
self.app.top_bar.grid()
|
|
self._update_task_bar_visibility("settings")
|
|
|
|
def toggle_backup_content_frame(self, initial_tab_index=0):
|
|
self._cancel_calculation()
|
|
self.app.drawing.update_nav_buttons(2) # Index 2 for Backup Content
|
|
|
|
if not self.app.destination_path:
|
|
MessageDialog(master=self.app, message_type="info",
|
|
title=Msg.STR["info_menu"], text=Msg.STR["err_no_dest_folder"])
|
|
self.toggle_mode("backup", 0)
|
|
return
|
|
|
|
# Mount the destination if it is encrypted and not already mounted
|
|
if self.app.backup_manager.encryption_manager.is_encrypted(self.app.destination_path):
|
|
if not self.app.backup_manager.encryption_manager.is_mounted(self.app.destination_path):
|
|
is_system = (initial_tab_index == 0)
|
|
mount_point = self.app.backup_manager.encryption_manager.prepare_encrypted_destination(
|
|
self.app.destination_path, is_system=is_system, source_size=0, queue=self.app.queue)
|
|
if not mount_point:
|
|
MessageDialog(master=self.app, message_type="error",
|
|
title=Msg.STR["error"], text=Msg.STR.get("err_mount_failed", "Mounting failed."))
|
|
self.toggle_mode("backup", 0)
|
|
return
|
|
|
|
self.app.header_frame.refresh_status()
|
|
|
|
self.app.canvas_frame.grid_remove()
|
|
self.app.log_frame.grid_remove()
|
|
self.app.scheduler_frame.hide()
|
|
self.app.settings_frame.hide()
|
|
|
|
self.app.source_size_frame.grid_remove()
|
|
self.app.target_size_frame.grid_remove()
|
|
self.app.restore_size_frame_before.grid_remove()
|
|
self.app.restore_size_frame_after.grid_remove()
|
|
self.app.backup_content_frame.show(self.app.destination_path, initial_tab_index)
|
|
self.app.top_bar.grid()
|
|
self._update_task_bar_visibility("scheduler") |