Files
Py-Backup/pyimage_ui/backup_content_frame.py
Désiré Werner Menrath 73e6e42485 Refactor: Encrypted backups to use direct LUKS
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.
2025-09-07 15:58:28 +02:00

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")