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.
192 lines
7.9 KiB
Python
192 lines
7.9 KiB
Python
import tkinter as tk
|
|
from tkinter import ttk
|
|
import os
|
|
from shared_libs.animated_icon import AnimatedIcon
|
|
from core.pbp_app_config import Msg
|
|
from pyimage_ui.system_backup_content_frame import SystemBackupContentFrame
|
|
from pyimage_ui.user_backup_content_frame import UserBackupContentFrame
|
|
from shared_libs.logger import app_logger
|
|
from shared_libs.message import MessageDialog
|
|
|
|
|
|
class BackupContentFrame(ttk.Frame):
|
|
def __init__(self, master, backup_manager, actions, app, **kwargs):
|
|
super().__init__(master, **kwargs)
|
|
app_logger.log("BackupContentFrame: __init__ called")
|
|
self.backup_manager = backup_manager
|
|
self.actions = actions
|
|
self.app = app
|
|
|
|
self.base_backup_path = None
|
|
self.current_view_index = 0
|
|
self.viewing_encrypted = False
|
|
|
|
self.grid_rowconfigure(1, weight=1)
|
|
self.grid_columnconfigure(0, weight=1)
|
|
|
|
header_frame = ttk.Frame(self)
|
|
header_frame.grid(row=0, column=0, sticky=tk.NSEW)
|
|
|
|
top_nav_frame = ttk.Frame(header_frame)
|
|
top_nav_frame.pack(side=tk.LEFT)
|
|
|
|
self.nav_buttons_defs = [
|
|
(Msg.STR["system_backup_info"], lambda: self._switch_view(0)),
|
|
(Msg.STR["user_backup_info"], lambda: self._switch_view(1)),
|
|
]
|
|
|
|
self.nav_buttons = []
|
|
self.nav_progress_bars = []
|
|
|
|
for i, (text, command) in enumerate(self.nav_buttons_defs):
|
|
button_frame = ttk.Frame(top_nav_frame)
|
|
button_frame.pack(side=tk.LEFT, padx=5)
|
|
button = ttk.Button(button_frame, text=text,
|
|
command=command, style="TButton.Borderless.Round")
|
|
button.pack(side=tk.TOP)
|
|
self.nav_buttons.append(button)
|
|
progress_bar = ttk.Progressbar(
|
|
button_frame, orient="horizontal", length=50, mode="determinate", style="Small.Horizontal.TProgressbar")
|
|
progress_bar.pack_forget()
|
|
self.nav_progress_bars.append(progress_bar)
|
|
|
|
if i < len(self.nav_buttons_defs) - 1:
|
|
ttk.Separator(top_nav_frame, orient=tk.VERTICAL).pack(
|
|
side=tk.LEFT, fill=tk.Y, padx=2)
|
|
|
|
# Deletion Status UI
|
|
self.deletion_status_frame = ttk.Frame(header_frame)
|
|
self.deletion_status_frame.pack(side=tk.LEFT, padx=15)
|
|
|
|
bg_color = self.winfo_toplevel().style.lookup('TFrame', 'background')
|
|
self.deletion_animated_icon = AnimatedIcon(
|
|
self.deletion_status_frame, width=20, height=20, use_pillow=True, bg=bg_color, animation_type="blink")
|
|
self.deletion_animated_icon.pack(side=tk.LEFT, padx=5)
|
|
self.deletion_animated_icon.stop("DISABLE")
|
|
|
|
self.deletion_status_label = ttk.Label(
|
|
self.deletion_status_frame, text="", font=("Ubuntu", 10, "bold"))
|
|
self.deletion_status_label.pack(side=tk.LEFT, padx=5)
|
|
|
|
content_container = ttk.Frame(self)
|
|
content_container.grid(row=1, column=0, sticky="nsew")
|
|
content_container.grid_rowconfigure(0, weight=1)
|
|
content_container.grid_columnconfigure(0, weight=1)
|
|
|
|
self.system_backups_frame = SystemBackupContentFrame(
|
|
content_container, backup_manager, actions, parent_view=self)
|
|
self.user_backups_frame = UserBackupContentFrame(
|
|
content_container, backup_manager, actions, parent_view=self)
|
|
self.system_backups_frame.grid(row=0, column=0, sticky=tk.NSEW)
|
|
self.user_backups_frame.grid(row=0, column=0, sticky=tk.NSEW)
|
|
|
|
action_button_frame = ttk.Frame(self, padding=10)
|
|
action_button_frame.grid(row=2, column=0, sticky="ew")
|
|
|
|
self.restore_button = ttk.Button(
|
|
action_button_frame, text=Msg.STR["restore"], command=self._restore_selected, state="disabled")
|
|
self.restore_button.pack(side=tk.LEFT, padx=5)
|
|
|
|
self.delete_button = ttk.Button(
|
|
action_button_frame, text=Msg.STR["delete"], command=self._delete_selected, state="disabled")
|
|
self.delete_button.pack(side=tk.LEFT, padx=5)
|
|
|
|
self.edit_comment_button = ttk.Button(
|
|
action_button_frame, text=Msg.STR["comment"], command=self._edit_comment, state="disabled")
|
|
self.edit_comment_button.pack(side=tk.LEFT, padx=5)
|
|
|
|
self._switch_view(0)
|
|
|
|
def update_button_state(self, is_selected):
|
|
self.restore_button.config(
|
|
state="normal" if is_selected else "disabled")
|
|
self.delete_button.config(
|
|
state="normal" if is_selected else "disabled")
|
|
self.edit_comment_button.config(
|
|
state="normal" if is_selected else "disabled")
|
|
|
|
def _get_active_subframe(self):
|
|
return self.system_backups_frame if self.current_view_index == 0 else self.user_backups_frame
|
|
|
|
def _restore_selected(self):
|
|
self._get_active_subframe()._restore_selected()
|
|
|
|
def _delete_selected(self):
|
|
self._get_active_subframe()._delete_selected()
|
|
|
|
def _edit_comment(self):
|
|
self._get_active_subframe()._edit_comment()
|
|
|
|
def _switch_view(self, index):
|
|
self.current_view_index = index
|
|
config_key = "last_encrypted_backup_content_view" if self.viewing_encrypted else "last_backup_content_view"
|
|
self.app.config_manager.set_setting(config_key, index)
|
|
self.update_nav_buttons(index)
|
|
|
|
if index == 0:
|
|
self.system_backups_frame.grid()
|
|
self.user_backups_frame.grid_remove()
|
|
else:
|
|
self.user_backups_frame.grid()
|
|
self.system_backups_frame.grid_remove()
|
|
self.update_button_state(False)
|
|
|
|
def update_nav_buttons(self, active_index):
|
|
for i, button in enumerate(self.nav_buttons):
|
|
if i == active_index:
|
|
button.configure(style="Toolbutton")
|
|
self.nav_progress_bars[i].pack(side=tk.BOTTOM, fill=tk.X)
|
|
self.nav_progress_bars[i]['value'] = 100
|
|
else:
|
|
button.configure(style="Gray.Toolbutton")
|
|
self.nav_progress_bars[i].pack_forget()
|
|
|
|
def show(self, backup_path, initial_tab_index=0):
|
|
app_logger.log(
|
|
f"BackupContentFrame: show called with path {backup_path}")
|
|
self.grid(row=2, column=0, sticky="nsew")
|
|
|
|
self.base_backup_path = backup_path
|
|
|
|
# Check if the destination is encrypted and trigger mount if necessary
|
|
is_encrypted = self.backup_manager.encryption_manager.is_encrypted(
|
|
backup_path)
|
|
self.viewing_encrypted = is_encrypted # Set this flag for remembering the view
|
|
|
|
pybackup_dir = os.path.join(backup_path, "pybackup")
|
|
|
|
if not os.path.isdir(pybackup_dir):
|
|
app_logger.log(
|
|
f"Backup path {pybackup_dir} does not exist or is not a directory.")
|
|
# Clear views if path is invalid
|
|
self.system_backups_frame.show(backup_path, [])
|
|
self.user_backups_frame.show(backup_path, [])
|
|
return
|
|
|
|
all_backups = self.backup_manager.list_all_backups(backup_path)
|
|
if all_backups:
|
|
system_backups, user_backups = all_backups
|
|
self.system_backups_frame.show(backup_path, system_backups)
|
|
self.user_backups_frame.show(backup_path, user_backups)
|
|
else:
|
|
# Handle case where inspection returns None (e.g. encrypted and mount_if_needed=False)
|
|
self.system_backups_frame.show(backup_path, [])
|
|
self.user_backups_frame.show(backup_path, [])
|
|
|
|
# Use the passed index to switch to the correct view
|
|
self._switch_view(initial_tab_index)
|
|
|
|
def hide(self):
|
|
self.grid_remove()
|
|
|
|
def show_deletion_status(self, text: str):
|
|
app_logger.log(f"Showing deletion status: {text}")
|
|
self.deletion_status_label.config(text=text)
|
|
self.deletion_animated_icon.start()
|
|
self.deletion_status_frame.pack(side=tk.LEFT, padx=15)
|
|
|
|
def hide_deletion_status(self):
|
|
app_logger.log("Hiding deletion status text.")
|
|
self.deletion_status_label.config(text="")
|
|
self.deletion_animated_icon.stop("DISABLE")
|