Implements a new feature for creating compressed full backups and restoring from them. - Backups can now be created as compressed .tar.gz archives. - This option is only available for full backups to maintain the efficiency of incremental backups. - The UI now forces a full backup when compression is selected. - The backup list correctly identifies and labels compressed backups. - The restore process can handle both compressed and uncompressed backups. fix: Improve backup process feedback and reliability - Fixes a critical bug where backups could be overwritten if created on the same day. Backup names now include a timestamp to ensure uniqueness. - Improves UI feedback during compressed backups by showing distinct stages (transfer, compress) and using an indeterminate progress bar during the compression phase. - Disables the cancel button during the non-cancellable compression stage. - Fixes a bug where the incremental backup size was written to the info file for full backups. The correct total size is now used.
174 lines
6.9 KiB
Python
174 lines
6.9 KiB
Python
import tkinter as tk
|
|
from tkinter import ttk
|
|
import os
|
|
|
|
from pbp_app_config import Msg
|
|
from pyimage_ui.comment_editor_dialog import CommentEditorDialog
|
|
|
|
|
|
class SystemBackupContentFrame(ttk.Frame):
|
|
def __init__(self, master, backup_manager, **kwargs):
|
|
super().__init__(master, **kwargs)
|
|
self.backup_manager = backup_manager
|
|
self.system_backups_list = []
|
|
|
|
self.backup_path = None
|
|
|
|
# --- Backup Content List View ---
|
|
self.content_frame = ttk.LabelFrame(
|
|
self, text=Msg.STR["backup_content"], padding=10)
|
|
self.content_frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
columns = ("date", "type", "size", "comment", "folder_name")
|
|
self.content_tree = ttk.Treeview(
|
|
self.content_frame, columns=columns, show="headings")
|
|
self.content_tree.heading(
|
|
"date", text=Msg.STR["date"])
|
|
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.heading(
|
|
"folder_name", text=Msg.STR["folder"])
|
|
|
|
self.content_tree.column("date", width=120, anchor="w")
|
|
self.content_tree.column("type", width=80, anchor="center")
|
|
self.content_tree.column("size", width=100, anchor="e")
|
|
self.content_tree.column("comment", width=200, anchor="w")
|
|
self.content_tree.column("folder_name", width=250, anchor="w")
|
|
|
|
self.content_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
|
|
self.content_tree.bind("<<TreeviewSelect>>", self._on_item_select)
|
|
|
|
list_button_frame = ttk.Frame(self.content_frame)
|
|
list_button_frame.pack(pady=10)
|
|
|
|
self.restore_button = ttk.Button(list_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(list_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(list_button_frame, text="Kommentar bearbeiten",
|
|
command=self._edit_comment, state="disabled")
|
|
self.edit_comment_button.pack(side=tk.LEFT, padx=5)
|
|
|
|
def show(self, backup_path):
|
|
if backup_path and self.backup_path != backup_path:
|
|
self.backup_path = backup_path
|
|
self._load_backup_content()
|
|
|
|
def hide(self):
|
|
self.grid_remove()
|
|
|
|
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
|
|
|
|
# Use the new method to get structured system backup data
|
|
system_backups = self.backup_manager.list_system_backups(
|
|
self.backup_path)
|
|
|
|
for backup_info in system_backups:
|
|
self.content_tree.insert("", "end", values=(
|
|
backup_info.get("date", "N/A"),
|
|
backup_info.get("type", "N/A"),
|
|
backup_info.get("size", "N/A"),
|
|
backup_info.get("comment", ""),
|
|
backup_info.get("folder_name", "N/A")
|
|
))
|
|
self._on_item_select(None) # Disable buttons initially
|
|
|
|
def _on_item_select(self, event):
|
|
selected_item = self.content_tree.focus()
|
|
is_selected = True if selected_item else False
|
|
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 _edit_comment(self):
|
|
selected_item = self.content_tree.focus()
|
|
if not selected_item:
|
|
return
|
|
|
|
item_values = self.content_tree.item(selected_item)["values"]
|
|
folder_name = item_values[4] # Assuming folder_name is the 5th value
|
|
|
|
# Construct the path to the info file
|
|
pybackup_path = os.path.join(self.backup_path, "pybackup")
|
|
info_file_path = os.path.join(pybackup_path, f"{folder_name}.txt")
|
|
|
|
# The file should exist, but we can handle cases where it might not.
|
|
if not os.path.exists(info_file_path):
|
|
# If for some reason the info file is missing, we can create an empty one.
|
|
self.backup_manager.update_comment(info_file_path, "")
|
|
|
|
CommentEditorDialog(self, info_file_path, self.backup_manager)
|
|
self._load_backup_content() # Refresh list to show new comment
|
|
|
|
def _restore_selected(self):
|
|
selected_item = self.content_tree.focus()
|
|
if not selected_item:
|
|
return
|
|
|
|
item_values = self.content_tree.item(selected_item)["values"]
|
|
folder_name = item_values[4] # 5th column is folder_name
|
|
|
|
selected_backup = None
|
|
for backup in self.system_backups_list:
|
|
if backup.get("folder_name") == folder_name:
|
|
selected_backup = backup
|
|
break
|
|
|
|
if not selected_backup:
|
|
print(f"Error: Could not find backup info for {folder_name}")
|
|
return
|
|
|
|
# We need to get the restore destination from the main app
|
|
# This is a bit tricky as this frame is isolated.
|
|
# We assume the main app has a way to provide this.
|
|
# Let's get it from the config manager, which should be accessible via the backup_manager's app instance.
|
|
try:
|
|
# Accessing the app instance through the master hierarchy
|
|
main_app = self.winfo_toplevel()
|
|
restore_dest_path = main_app.config_manager.get_setting("restore_destination_path", "/")
|
|
|
|
if not restore_dest_path:
|
|
print("Error: Restore destination not set.")
|
|
# Optionally, show a message box to the user
|
|
return
|
|
|
|
self.backup_manager.start_restore(
|
|
source_path=selected_backup['full_path'],
|
|
dest_path=restore_dest_path,
|
|
is_compressed=selected_backup['is_compressed']
|
|
)
|
|
except AttributeError:
|
|
print("Could not access main application instance to get restore path.")
|
|
|
|
def _delete_selected(self):
|
|
selected_item = self.content_tree.focus()
|
|
if not selected_item:
|
|
return
|
|
|
|
item_values = self.content_tree.item(selected_item)["values"]
|
|
folder_name = item_values[4] # Assuming folder_name is the 5th value
|
|
|
|
# Construct the full path to the backup folder
|
|
pybackup_path = os.path.join(self.backup_path, "pybackup")
|
|
folder_to_delete = os.path.join(pybackup_path, folder_name)
|
|
|
|
self.backup_manager.delete_privileged_path(folder_to_delete)
|
|
self._load_backup_content()
|