Files
Py-Backup/pyimage_ui/system_backup_content_frame.py
Désiré Werner Menrath 452a56b813 feat: Implement auto-scaling encrypted containers and fix UI workflow
Refactors the encryption mechanism to use a flexible LVM-on-a-loop-device backend instead of a fixed-size file. This resolves issues with containers running out of space.

- Implements auto-resizing of the container when a backup fails due to lack of space.
- Implements transparent inspection of encrypted containers, allowing the UI to display their contents (full/incremental backups) just like unencrypted ones.
- Fixes deletion of encrypted backups by ensuring the container is unlocked before deletion.
- Fixes a bug where deleting unencrypted user backups incorrectly required root privileges.
- Fixes a UI freeze caused by calling a password dialog from a non-UI thread during deletion.
- Simplifies the UI by removing the now-obsolete "Show Encrypted Backups" button.
- Changes the default directory for encrypted user backups to `user_encrypt`.
2025-09-06 12:46:36 +02:00

160 lines
6.0 KiB
Python

import tkinter as tk
from tkinter import ttk
import os
from core.pbp_app_config import Msg
from pyimage_ui.comment_editor_dialog import CommentEditorDialog
class SystemBackupContentFrame(ttk.Frame):
def __init__(self, master, backup_manager, actions, parent_view, **kwargs):
super().__init__(master, **kwargs)
self.backup_manager = backup_manager
self.actions = actions
self.parent_view = parent_view
self.system_backups_list = []
self.backup_path = None
self.tag_colors = [
("full_blue", "#0078D7", "inc_blue", "#50E6FF"),
("full_orange", "#E8740C", "inc_orange", "#FFB366"),
("full_green", "#107C10", "inc_green", "#50E680"),
("full_purple", "#8B107C", "inc_purple", "#D46EE5"),
]
columns = ("date", "time", "type", "size", "comment")
self.content_tree = ttk.Treeview(
self, columns=columns, show="headings")
self.content_tree.heading("date", text=Msg.STR["date"])
self.content_tree.heading("time", text=Msg.STR["time"])
self.content_tree.heading("type", text=Msg.STR["type"])
self.content_tree.heading("size", text=Msg.STR["size"])
self.content_tree.heading("comment", text=Msg.STR["comment"])
self.content_tree.column("date", width=100, anchor="w")
self.content_tree.column("time", width=80, anchor="center")
self.content_tree.column("type", width=120, anchor="center")
self.content_tree.column("size", width=100, anchor="e")
self.content_tree.column("comment", width=300, anchor="w")
self.content_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.content_tree.bind("<<TreeviewSelect>>", self._on_item_select)
def show(self, backup_path):
self.backup_path = backup_path
self._load_backup_content()
def _load_backup_content(self):
for i in self.content_tree.get_children():
self.content_tree.delete(i)
if not self.backup_path or not os.path.isdir(self.backup_path):
return
self.system_backups_list = self.backup_manager.list_system_backups(
self.backup_path)
color_index = -1
for i, backup_info in enumerate(self.system_backups_list):
if backup_info.get("backup_type_base") == "Full":
color_index = (color_index + 1) % len(self.tag_colors)
full_tag, full_color, inc_tag, inc_color = self.tag_colors[color_index]
self.content_tree.tag_configure(
full_tag, foreground=full_color)
self.content_tree.tag_configure(
inc_tag, foreground=inc_color, font=("Helvetica", 10, "bold"))
current_tag = full_tag
else:
_, _, inc_tag, _ = self.tag_colors[color_index]
current_tag = inc_tag
self.content_tree.insert("", "end", values=(
backup_info.get("date", "N/A"),
backup_info.get("time", "N/A"),
backup_info.get("type", "N/A"),
backup_info.get("size", "N/A"),
backup_info.get("comment", ""),
), tags=(current_tag,), iid=backup_info.get("folder_name"))
self._on_item_select(None)
def _on_item_select(self, event):
is_selected = True if self.content_tree.focus() else False
self.parent_view.update_button_state(is_selected)
def _edit_comment(self):
selected_item_id = self.content_tree.focus()
if not selected_item_id:
return
info_file_path = os.path.join(
self.backup_path, "pybackup", f"{selected_item_id}.txt")
if not os.path.exists(info_file_path):
self.backup_manager.update_comment(info_file_path, "")
CommentEditorDialog(self, info_file_path, self.backup_manager)
self._load_backup_content()
def _restore_selected(self):
selected_item_id = self.content_tree.focus()
if not selected_item_id:
return
selected_backup = next((b for b in self.system_backups_list if b.get(
"folder_name") == selected_item_id), None)
if not selected_backup:
return
main_app = self.winfo_toplevel()
restore_dest_path = main_app.config_manager.get_setting(
"restore_destination_path", "/")
if not restore_dest_path:
return
self.backup_manager.start_restore(
source_path=selected_backup['full_path'],
dest_path=restore_dest_path,
is_compressed=selected_backup['is_compressed']
)
def _delete_selected(self):
selected_item_id = self.content_tree.focus()
if not selected_item_id:
return
selected_backup = next((b for b in self.system_backups_list if b.get(
"folder_name") == selected_item_id), None)
if not selected_backup:
return
folder_to_delete = selected_backup['full_path']
is_encrypted = selected_backup['is_encrypted']
password = None
if is_encrypted:
# Get password in the UI thread before starting the background task
password = self.backup_manager.encryption_manager.get_password("root", confirm=True)
if not password:
self.actions.logger.log("Password entry cancelled, aborting deletion.")
return
info_file_to_delete = os.path.join(
self.backup_path, "pybackup", f"{selected_item_id}{'_encrypted' if is_encrypted else ''}.txt")
self.actions._set_ui_state(False)
self.parent_view.show_deletion_status(Msg.STR["deleting_backup_in_progress"])
self.backup_manager.start_delete_backup(
path_to_delete=folder_to_delete,
info_file_path=info_file_to_delete,
is_encrypted=is_encrypted,
is_system=True,
base_dest_path=self.backup_path,
password=password,
queue=self.winfo_toplevel().queue
)