refactor: Rework encrypted backup UI and logic

- Centralizes backup content view logic into a single BackupContentFrame.
- Removes the separate, now obsolete EncryptedBackupContentFrame.
- Adds a toggle button within the BackupContentFrame to switch between viewing normal and encrypted backups.
- Centralizes the Restore, Delete, and Edit Comment buttons into a single button bar in BackupContentFrame.
- Corrects the path resolution logic to find backups and encrypted containers within the /pybackup subdirectory.
- Fixes UI bugs where action buttons would disappear when switching tabs.
This commit is contained in:
2025-09-04 23:22:12 +02:00
parent 0b9c58410f
commit 2d685e1d97
10 changed files with 336 additions and 480 deletions

View File

@@ -582,54 +582,16 @@ set -e
backups.append(item) backups.append(item)
return sorted(backups, reverse=True) return sorted(backups, reverse=True)
def list_system_backups(self, base_backup_path: str) -> List[Dict[str, str]]: def list_system_backups(self, scan_path: str, is_encrypted_and_mounted: bool = False) -> List[Dict[str, str]]:
"""Lists all system backups found in the pybackup subdirectory.""" """Lists all system backups, handling encrypted containers and sorting into groups."""
system_backups = [] if not os.path.isdir(scan_path):
pybackup_path = os.path.join(base_backup_path, "pybackup") return []
if not os.path.isdir(pybackup_path): all_backups = []
return system_backups
# Check for a single encrypted container first
encrypted_container_path = os.path.join(pybackup_path, "pybackup_encrypted.luks")
if os.path.exists(encrypted_container_path):
try:
stat_info = os.stat(encrypted_container_path)
m_time = datetime.datetime.fromtimestamp(stat_info.st_mtime)
file_size = stat_info.st_size
# Format size
power = 1024
n = 0
power_labels = {0: 'B', 1: 'KB', 2: 'MB', 3: 'GB', 4: 'TB'}
display_size = file_size
while display_size >= power and n < len(power_labels) - 1:
display_size /= power
n += 1
size_str = f"{display_size:.2f} {power_labels[n]}"
system_backups.append({
"date": m_time.strftime('%d-%m-%Y'),
"time": m_time.strftime('%H:%M:%S'),
"type": "Encrypted Container",
"size": size_str,
"folder_name": "pybackup_encrypted.luks",
"full_path": encrypted_container_path,
"comment": "Container for all encrypted backups",
"is_compressed": False,
"is_encrypted": True,
"backup_type_base": "Full" # Treat container as a single full entity
})
return system_backups # Return immediately
except Exception as e:
self.logger.log(f"Could not stat encrypted container file: {e}")
# Fall through to normal processing if stat fails
# Proceed with individual folder scan if no container is found
name_regex = re.compile( name_regex = re.compile(
r"^(\d{2}-\d{2}-\d{4})_(\d{2}:\d{2}:\d{2})_system_(full|incremental)(\.tar\.gz)?$", re.IGNORECASE) r"^(\d{2}-\d{2}-\d{4})_(\d{2}:\d{2}:\d{2})_system_(full|incremental)(\.tar\.gz)?$", re.IGNORECASE)
for item in os.listdir(pybackup_path): for item in os.listdir(scan_path):
if item.endswith('.txt') or item.endswith('.luks'): if item.endswith('.txt') or item.endswith('.luks'):
continue continue
@@ -637,39 +599,29 @@ set -e
if not match: if not match:
continue continue
full_path = os.path.join(pybackup_path, item) full_path = os.path.join(scan_path, item)
date_str = match.group(1) date_str, time_str, backup_type_base, extension = match.groups()
time_str = match.group(2)
backup_type_base = match.group(3).capitalize()
extension = match.group(4)
is_compressed = (extension == ".tar.gz") is_compressed = (extension == ".tar.gz")
is_encrypted = False # Individual folders are not encrypted in this logic
backup_type = backup_type_base.capitalize()
backup_type = backup_type_base
if is_compressed: if is_compressed:
backup_type += " (Compressed)" backup_type += " (Compressed)"
backup_size = "N/A" backup_size = "N/A"
comment = "" comment = ""
info_file_path = os.path.join(scan_path, f"{item}.txt")
info_file_path = os.path.join(pybackup_path, f"{item}.txt")
if os.path.exists(info_file_path): if os.path.exists(info_file_path):
try: try:
with open(info_file_path, 'r') as f: with open(info_file_path, 'r') as f:
for line in f: for line in f:
if line.strip().lower().startswith("originalgröße:"): if line.strip().lower().startswith("originalgröße:"):
size_part = line.split(":", 1)[1].strip() backup_size = line.split(":", 1)[1].strip().split('(')[0].strip()
if '(' in size_part:
backup_size = size_part.split('(')[0].strip()
else:
backup_size = size_part
elif line.strip().lower().startswith("kommentar:"): elif line.strip().lower().startswith("kommentar:"):
comment = line.split(":", 1)[1].strip() comment = line.split(":", 1)[1].strip()
except Exception as e: except Exception as e:
self.logger.log( self.logger.log(f"Could not read info file {info_file_path}: {e}")
f"Could not read info file {info_file_path}: {e}")
system_backups.append({ all_backups.append({
"date": date_str, "date": date_str,
"time": time_str, "time": time_str,
"type": backup_type, "type": backup_type,
@@ -678,20 +630,35 @@ set -e
"full_path": full_path, "full_path": full_path,
"comment": comment, "comment": comment,
"is_compressed": is_compressed, "is_compressed": is_compressed,
"is_encrypted": is_encrypted, "is_encrypted": is_encrypted_and_mounted,
"backup_type_base": backup_type_base "backup_type_base": backup_type_base.capitalize(),
"datetime": datetime.datetime.strptime(f"{date_str} {time_str}", '%d-%m-%Y %H:%M:%S')
}) })
try: # Sort all backups chronologically to make grouping easier
# Sort chronologically, oldest first all_backups.sort(key=lambda x: x['datetime'])
system_backups.sort(key=lambda x: datetime.datetime.strptime(
f"{x['date']} {x['time']}", '%d-%m-%Y %H:%M:%S'), reverse=False)
except ValueError:
self.logger.log(
"Could not sort backups by date and time due to format mismatch.")
system_backups.sort(key=lambda x: x['folder_name'], reverse=False)
return system_backups # Group backups: each group starts with a Full backup
grouped_backups = []
current_group = []
for backup in all_backups:
if backup['backup_type_base'] == 'Full':
if current_group:
grouped_backups.append(current_group)
current_group = [backup]
else: # Incremental
if current_group: # Add to the current group if it exists
current_group.append(backup)
if current_group:
grouped_backups.append(current_group)
# Sort groups by the datetime of their first (Full) backup, descending
grouped_backups.sort(key=lambda g: g[0]['datetime'], reverse=True)
# Flatten the list of groups into the final sorted list
final_sorted_list = [item for group in grouped_backups for item in group]
return final_sorted_list
def list_user_backups(self, base_backup_path: str) -> List[Dict[str, str]]: def list_user_backups(self, base_backup_path: str) -> List[Dict[str, str]]:
"""Lists all user backups found in the base backup path.""" """Lists all user backups found in the base backup path."""

View File

@@ -25,12 +25,20 @@ class EncryptionManager:
try: try:
return keyring.get_password(self.service_id, username) return keyring.get_password(self.service_id, username)
except keyring.errors.InitError as e: except keyring.errors.InitError as e:
logger.log(f"Could not initialize keyring. Keyring is not available on this system or is not configured correctly. Error: {e}") self.logger.log(f"Could not initialize keyring. Keyring is not available on this system or is not configured correctly. Error: {e}")
return None return None
except Exception as e: except Exception as e:
logger.log(f"Could not get password from keyring: {e}") self.logger.log(f"Could not get password from keyring: {e}")
return None return None
def is_key_in_keyring(self, username: str) -> bool:
"""Checks if a password for the given username exists in the keyring."""
try:
return self.get_password_from_keyring(username) is not None
except Exception as e:
self.logger.log(f"Could not check password in keyring: {e}")
return False
def set_password_in_keyring(self, username: str, password: str) -> bool: def set_password_in_keyring(self, username: str, password: str) -> bool:
try: try:
keyring.set_password(self.service_id, username, password) keyring.set_password(self.service_id, username, password)
@@ -69,6 +77,43 @@ class EncryptionManager:
return password return password
def unlock_container(self, base_path: str, password: str) -> Optional[str]:
"""Unlocks and mounts an existing LUKS container."""
self.logger.log(f"Attempting to unlock encrypted container in {base_path}")
container_file = "pybackup_encrypted.luks"
container_path = os.path.join(base_path, container_file)
if not os.path.exists(container_path):
self.logger.log(f"Encrypted container not found at {container_path}")
return None
mapper_name = f"pybackup_{os.path.basename(base_path.rstrip('/'))}"
mount_point = f"/mnt/{mapper_name}"
if os.path.ismount(mount_point):
self.logger.log(f"Container already mounted at {mount_point}")
return mount_point
script = f"""
mkdir -p {mount_point}
echo -n '{password}' | cryptsetup luksOpen {container_path} {mapper_name} -
mount /dev/mapper/{mapper_name} {mount_point}
"""
if not self._execute_as_root(script):
self.logger.log("Failed to unlock existing encrypted container. Check password or permissions.")
# Clean up failed mount attempt
self.cleanup_encrypted_backup(mapper_name, mount_point)
return None
self.logger.log(f"Encrypted container unlocked and mounted at {mount_point}")
return mount_point
def lock_container(self, base_path: str):
"""Unmounts and closes the LUKS container for a given base path."""
mapper_name = f"pybackup_{os.path.basename(base_path.rstrip('/'))}"
mount_point = f"/mnt/{mapper_name}"
self.cleanup_encrypted_backup(mapper_name, mount_point)
def setup_encrypted_backup(self, queue, base_path: str, size_gb: int, password: str) -> Optional[str]: def setup_encrypted_backup(self, queue, base_path: str, size_gb: int, password: str) -> Optional[str]:
"""Sets up a persistent LUKS encrypted container for the backup destination.""" """Sets up a persistent LUKS encrypted container for the backup destination."""
self.logger.log(f"Setting up encrypted container at {base_path}") self.logger.log(f"Setting up encrypted container at {base_path}")
@@ -80,7 +125,7 @@ class EncryptionManager:
container_file = "pybackup_encrypted.luks" container_file = "pybackup_encrypted.luks"
container_path = os.path.join(base_path, container_file) container_path = os.path.join(base_path, container_file)
mapper_name = f"pybackup_{os.path.basename(base_path)}" mapper_name = f"pybackup_{os.path.basename(base_path.rstrip('/'))}"
mount_point = f"/mnt/{mapper_name}" mount_point = f"/mnt/{mapper_name}"
if not password: if not password:
@@ -95,17 +140,7 @@ class EncryptionManager:
if os.path.exists(container_path): if os.path.exists(container_path):
self.logger.log(f"Encrypted container {container_path} already exists. Attempting to unlock.") self.logger.log(f"Encrypted container {container_path} already exists. Attempting to unlock.")
script = f""" return self.unlock_container(base_path, password)
mkdir -p {mount_point}
echo -n '{password}' | cryptsetup luksOpen {container_path} {mapper_name} -
mount /dev/mapper/{mapper_name} {mount_point}
"""
if not self._execute_as_root(script):
self.logger.log("Failed to unlock existing encrypted container. Check password or permissions.")
queue.put(('error', "Failed to unlock existing encrypted container."))
# Clean up failed mount attempt
self.cleanup_encrypted_backup(mapper_name, mount_point)
return None
else: else:
self.logger.log(f"Creating new encrypted container: {container_path}") self.logger.log(f"Creating new encrypted container: {container_path}")
script = f""" script = f"""
@@ -119,10 +154,11 @@ class EncryptionManager:
if not self._execute_as_root(script): if not self._execute_as_root(script):
self.logger.log("Failed to create and setup encrypted container.") self.logger.log("Failed to create and setup encrypted container.")
self._cleanup_encrypted_backup(mapper_name, mount_point) self.cleanup_encrypted_backup(mapper_name, mount_point)
# Also remove the failed container file # Also remove the failed container file
if os.path.exists(container_path): if os.path.exists(container_path):
os.remove(container_path) # This should be done with pkexec as well # This should be done with pkexec as well for safety
self._execute_as_root(f"rm -f {container_path}")
queue.put(('error', "Failed to setup encrypted container.")) queue.put(('error', "Failed to setup encrypted container."))
return None return None

View File

@@ -4,6 +4,7 @@ from tkinter import ttk
import os import os
import datetime import datetime
from queue import Queue, Empty from queue import Queue, Empty
import shutil
from shared_libs.log_window import LogWindow from shared_libs.log_window import LogWindow
from shared_libs.logger import app_logger from shared_libs.logger import app_logger
@@ -146,9 +147,7 @@ class MainApplication(tk.Tk):
self.sidebar_buttons_frame, text=Msg.STR["settings"], command=lambda: self.navigation.toggle_settings_frame(4), style="Sidebar.TButton") self.sidebar_buttons_frame, text=Msg.STR["settings"], command=lambda: self.navigation.toggle_settings_frame(4), style="Sidebar.TButton")
self.settings_button.pack(fill=tk.X, pady=10) self.settings_button.pack(fill=tk.X, pady=10)
self.header_frame = HeaderFrame(self.content_frame, self.image_manager, self.backup_manager.encryption_manager, self)
self.header_frame = HeaderFrame(self.content_frame, self.image_manager)
self.header_frame.grid(row=0, column=0, sticky="nsew") self.header_frame.grid(row=0, column=0, sticky="nsew")
self.top_bar = ttk.Frame(self.content_frame) self.top_bar = ttk.Frame(self.content_frame)
@@ -223,7 +222,6 @@ class MainApplication(tk.Tk):
self._setup_settings_frame() self._setup_settings_frame()
self._setup_backup_content_frame() self._setup_backup_content_frame()
self._setup_task_bar() self._setup_task_bar()
self.source_size_frame = ttk.LabelFrame( self.source_size_frame = ttk.LabelFrame(
@@ -299,15 +297,13 @@ class MainApplication(tk.Tk):
self.restore_size_frame_after.grid_remove() self.restore_size_frame_after.grid_remove()
self._load_state_and_initialize() self._load_state_and_initialize()
self.update_backup_options_from_config() # Add this call self.update_backup_options_from_config()
self.protocol("WM_DELETE_WINDOW", self.on_closing) self.protocol("WM_DELETE_WINDOW", self.on_closing)
def _load_state_and_initialize(self): def _load_state_and_initialize(self):
self.log_window.clear_log() self.log_window.clear_log()
"""Loads saved state from config and initializes the UI."""
last_mode = self.config_manager.get_setting("last_mode", "backup") last_mode = self.config_manager.get_setting("last_mode", "backup")
# Pre-load data from config before initializing the UI
backup_source_path = self.config_manager.get_setting( backup_source_path = self.config_manager.get_setting(
"backup_source_path") "backup_source_path")
if backup_source_path and os.path.isdir(backup_source_path): if backup_source_path and os.path.isdir(backup_source_path):
@@ -317,9 +313,8 @@ class MainApplication(tk.Tk):
if folder_name: if folder_name:
icon_name = self.buttons_map[folder_name]['icon'] icon_name = self.buttons_map[folder_name]['icon']
else: else:
# Handle custom folder path
folder_name = os.path.basename(backup_source_path.rstrip('/')) folder_name = os.path.basename(backup_source_path.rstrip('/'))
icon_name = 'folder_extralarge' # A generic folder icon icon_name = 'folder_extralarge'
self.backup_left_canvas_data.update({ self.backup_left_canvas_data.update({
'icon': icon_name, 'icon': icon_name,
@@ -330,7 +325,7 @@ class MainApplication(tk.Tk):
backup_dest_path = self.config_manager.get_setting( backup_dest_path = self.config_manager.get_setting(
"backup_destination_path") "backup_destination_path")
if backup_dest_path and os.path.isdir(backup_dest_path): if backup_dest_path and os.path.isdir(backup_dest_path):
self.destination_path = backup_dest_path # Still needed for some logic self.destination_path = backup_dest_path
total, used, free = shutil.disk_usage(backup_dest_path) total, used, free = shutil.disk_usage(backup_dest_path)
self.backup_right_canvas_data.update({ self.backup_right_canvas_data.update({
'folder': os.path.basename(backup_dest_path.rstrip('/')), 'folder': os.path.basename(backup_dest_path.rstrip('/')),
@@ -340,7 +335,18 @@ class MainApplication(tk.Tk):
self.destination_total_bytes = total self.destination_total_bytes = total
self.destination_used_bytes = used self.destination_used_bytes = used
if hasattr(self, 'header_frame'):
self.header_frame.refresh_status()
container_path = os.path.join(backup_dest_path, "pybackup_encrypted.luks")
if os.path.exists(container_path):
username = os.path.basename(backup_dest_path.rstrip('/'))
password = self.backup_manager.encryption_manager.get_password_from_keyring(username)
if password:
self.backup_manager.encryption_manager.unlock_container(backup_dest_path, password)
app_logger.log("Automatically unlocked encrypted container.")
if hasattr(self, 'header_frame'):
self.header_frame.refresh_status()
restore_src_path = self.config_manager.get_setting( restore_src_path = self.config_manager.get_setting(
"restore_source_path") "restore_source_path")
@@ -353,7 +359,6 @@ class MainApplication(tk.Tk):
restore_dest_path = self.config_manager.get_setting( restore_dest_path = self.config_manager.get_setting(
"restore_destination_path") "restore_destination_path")
if restore_dest_path and os.path.isdir(restore_dest_path): if restore_dest_path and os.path.isdir(restore_dest_path):
# Find the corresponding button_text for the path
folder_name = "" folder_name = ""
for name, path_obj in AppConfig.FOLDER_PATHS.items(): for name, path_obj in AppConfig.FOLDER_PATHS.items():
if str(path_obj) == restore_dest_path: if str(path_obj) == restore_dest_path:
@@ -366,18 +371,14 @@ class MainApplication(tk.Tk):
'path_display': restore_dest_path, 'path_display': restore_dest_path,
}) })
# Initialize UI for the last active mode
self.navigation.initialize_ui_for_mode(last_mode) self.navigation.initialize_ui_for_mode(last_mode)
# Trigger calculations if needed
if last_mode == 'backup': if last_mode == 'backup':
self.after(100, self.actions.on_sidebar_button_click, self.after(100, self.actions.on_sidebar_button_click,
self.backup_left_canvas_data.get('folder', 'Computer')) self.backup_left_canvas_data.get('folder', 'Computer'))
elif last_mode == 'restore': elif last_mode == 'restore':
# Trigger calculation for the right canvas (source) if a path is set
if restore_src_path: if restore_src_path:
self.drawing.calculate_restore_folder_size() self.drawing.calculate_restore_folder_size()
# Trigger calculation for the left canvas (destination) based on last selection
restore_dest_folder = self.restore_left_canvas_data.get( restore_dest_folder = self.restore_left_canvas_data.get(
'folder', 'Computer') 'folder', 'Computer')
self.after(100, self.actions.on_sidebar_button_click, self.after(100, self.actions.on_sidebar_button_click,
@@ -406,14 +407,11 @@ class MainApplication(tk.Tk):
def _setup_backup_content_frame(self): def _setup_backup_content_frame(self):
self.backup_content_frame = BackupContentFrame( self.backup_content_frame = BackupContentFrame(
self.content_frame, self.backup_manager, self.actions, padding=10) self.content_frame, self.backup_manager, self.actions, self, padding=10)
self.backup_content_frame.grid(row=2, column=0, sticky="nsew") self.backup_content_frame.grid(row=2, column=0, sticky="nsew")
self.backup_content_frame.grid_remove() self.backup_content_frame.grid_remove()
def _setup_task_bar(self): def _setup_task_bar(self):
# Define all boolean vars at the top to ensure they exist before use.
self.vollbackup_var = tk.BooleanVar() self.vollbackup_var = tk.BooleanVar()
self.inkrementell_var = tk.BooleanVar() self.inkrementell_var = tk.BooleanVar()
self.genaue_berechnung_var = tk.BooleanVar() self.genaue_berechnung_var = tk.BooleanVar()
@@ -429,7 +427,6 @@ class MainApplication(tk.Tk):
self.info_checkbox_frame, text=Msg.STR["info_text_placeholder"]) self.info_checkbox_frame, text=Msg.STR["info_text_placeholder"])
self.info_label.pack(anchor=tk.W, fill=tk.X, pady=5) self.info_label.pack(anchor=tk.W, fill=tk.X, pady=5)
# Frame for time info
self.time_info_frame = ttk.Frame(self.info_checkbox_frame) self.time_info_frame = ttk.Frame(self.info_checkbox_frame)
self.time_info_frame.pack(anchor=tk.W, fill=tk.X, pady=5) self.time_info_frame.pack(anchor=tk.W, fill=tk.X, pady=5)
@@ -445,7 +442,6 @@ class MainApplication(tk.Tk):
self.time_info_frame, text="Ende: --:--:--") self.time_info_frame, text="Ende: --:--:--")
self.end_time_label.pack(side=tk.LEFT, padx=5) self.end_time_label.pack(side=tk.LEFT, padx=5)
# --- Accurate Size Calculation Frame (on the right) ---
accurate_size_frame = ttk.Frame(self.time_info_frame) accurate_size_frame = ttk.Frame(self.time_info_frame)
accurate_size_frame.pack(side=tk.LEFT, padx=20) accurate_size_frame.pack(side=tk.LEFT, padx=20)
@@ -513,13 +509,12 @@ class MainApplication(tk.Tk):
self.action_frame, text=Msg.STR["start"], command=self.actions.toggle_start_cancel, state="disabled") self.action_frame, text=Msg.STR["start"], command=self.actions.toggle_start_cancel, state="disabled")
self.start_pause_button.grid(row=0, column=2, rowspan=2, padx=5) self.start_pause_button.grid(row=0, column=2, rowspan=2, padx=5)
def on_closing(self): def on_closing(self):
"""Handles window closing events and saves the app state.""" if self.destination_path:
self.backup_manager.encryption_manager.lock_container(self.destination_path)
self.config_manager.set_setting("last_mode", self.mode) self.config_manager.set_setting("last_mode", self.mode)
# Save paths from the data dictionaries
if self.backup_left_canvas_data.get('path_display'): if self.backup_left_canvas_data.get('path_display'):
self.config_manager.set_setting( self.config_manager.set_setting(
"backup_source_path", self.backup_left_canvas_data['path_display']) "backup_source_path", self.backup_left_canvas_data['path_display'])
@@ -544,7 +539,6 @@ class MainApplication(tk.Tk):
else: else:
self.config_manager.set_setting("restore_source_path", None) self.config_manager.set_setting("restore_source_path", None)
# Stop any ongoing animations before destroying the application
if self.left_canvas_animation: if self.left_canvas_animation:
self.left_canvas_animation.stop() self.left_canvas_animation.stop()
self.left_canvas_animation.destroy() self.left_canvas_animation.destroy()
@@ -562,21 +556,15 @@ class MainApplication(tk.Tk):
self.destroy() self.destroy()
def _process_queue(self): def _process_queue(self):
"""
Processes all messages from background threads to update the UI safely.
This is the single, consolidated queue processing loop for the entire application.
It processes messages in batches to avoid freezing the UI.
"""
try: try:
for _ in range(100): # Process up to 100 messages at a time for _ in range(100):
message = self.queue.get_nowait() message = self.queue.get_nowait()
# --- Size Calculation Message Handling (from data_processing) ---
if isinstance(message, tuple) and len(message) in [3, 5]: if isinstance(message, tuple) and len(message) in [3, 5]:
calc_type, status = None, None calc_type, status = None, None
if len(message) == 5: if len(message) == 5:
button_text, folder_size, mode_when_started, calc_type, status = message button_text, folder_size, mode_when_started, calc_type, status = message
else: # len == 3 else:
button_text, folder_size, mode_when_started = message button_text, folder_size, mode_when_started = message
if mode_when_started != self.mode: if mode_when_started != self.mode:
@@ -641,7 +629,6 @@ class MainApplication(tk.Tk):
text=Msg.STR["accurate_size_failed"], foreground="#D32F2F") text=Msg.STR["accurate_size_failed"], foreground="#D32F2F")
self.current_file_label.config(text="") self.current_file_label.config(text="")
# --- Backup/Deletion Message Handling (from main_app) ---
elif isinstance(message, tuple) and len(message) == 2: elif isinstance(message, tuple) and len(message) == 2:
message_type, value = message message_type, value = message
@@ -707,15 +694,13 @@ class MainApplication(tk.Tk):
self.actions._set_ui_state(True) self.actions._set_ui_state(True)
self.backup_content_frame.system_backups_frame._load_backup_content() self.backup_content_frame.system_backups_frame._load_backup_content()
elif message_type == 'completion_accurate': elif message_type == 'completion_accurate':
# This is now handled by the len=5 case above
pass pass
else: else:
app_logger.log(f"Unknown message in queue: {message}") app_logger.log(f"Unknown message in queue: {message}")
except Empty: except Empty:
pass # The queue is empty, do nothing. pass
finally: finally:
# Always schedule the next check.
self.after(100, self._process_queue) self.after(100, self._process_queue)
def _update_duration(self): def _update_duration(self):
@@ -732,11 +717,6 @@ class MainApplication(tk.Tk):
self.on_closing() self.on_closing()
def update_backup_options_from_config(self): def update_backup_options_from_config(self):
"""
Reads the 'force' settings from the config and updates the main UI checkboxes.
A 'force' setting overrides the user's selection and disables the control.
"""
# Full/Incremental Logic
force_full = self.config_manager.get_setting( force_full = self.config_manager.get_setting(
"force_full_backup", False) "force_full_backup", False)
force_incremental = self.config_manager.get_setting( force_incremental = self.config_manager.get_setting(
@@ -753,14 +733,12 @@ class MainApplication(tk.Tk):
self.full_backup_cb.config(state="disabled") self.full_backup_cb.config(state="disabled")
self.incremental_cb.config(state="disabled") self.incremental_cb.config(state="disabled")
# Compression Logic
force_compression = self.config_manager.get_setting( force_compression = self.config_manager.get_setting(
"force_compression", False) "force_compression", False)
if force_compression: if force_compression:
self.compressed_var.set(True) self.compressed_var.set(True)
self.compressed_cb.config(state="disabled") self.compressed_cb.config(state="disabled")
# Encryption Logic
force_encryption = self.config_manager.get_setting( force_encryption = self.config_manager.get_setting(
"force_encryption", False) "force_encryption", False)
if force_encryption: if force_encryption:
@@ -773,7 +751,6 @@ class MainApplication(tk.Tk):
if __name__ == "__main__": if __name__ == "__main__":
import argparse import argparse
import sys import sys
import shutil
parser = argparse.ArgumentParser(description="Py-Backup Application.") parser = argparse.ArgumentParser(description="Py-Backup Application.")
parser.add_argument( parser.add_argument(

View File

@@ -299,6 +299,8 @@ class Msg:
"cat_documents": _("Documents"), "cat_documents": _("Documents"),
"cat_music": _("Music"), "cat_music": _("Music"),
"cat_videos": _("Videos"), "cat_videos": _("Videos"),
"show_encrypted_backups": _("Show Encrypted Backups"),
"show_normal_backups": _("Show Normal Backups"),
# Browser View # Browser View
"backup_location": _("Backup Location"), "backup_location": _("Backup Location"),
@@ -315,6 +317,8 @@ class Msg:
"err_no_dest_folder": _("Please select a destination folder."), "err_no_dest_folder": _("Please select a destination folder."),
"err_no_source_folder": _("Please select at least one source folder."), "err_no_source_folder": _("Please select at least one source folder."),
"err_no_backup_selected": _("Please select a backup from the list."), "err_no_backup_selected": _("Please select a backup from the list."),
"err_unlock_failed": _("Failed to unlock the container. Please check the password and try again."),
"err_encrypted_not_mounted": _("Encrypted container is not unlocked. Please unlock it first from the header bar."),
"confirm_user_restore_title": _("Confirm User Data Restore"), "confirm_user_restore_title": _("Confirm User Data Restore"),
"confirm_user_restore_msg": _("Do you really want to restore the backup of '{backup_name}' to its original location? Any newer files may be overwritten."), "confirm_user_restore_msg": _("Do you really want to restore the backup of '{backup_name}' to its original location? Any newer files may be overwritten."),
"confirm_delete_title": _("Confirm Deletion"), "confirm_delete_title": _("Confirm Deletion"),
@@ -339,6 +343,7 @@ class Msg:
"projected_usage_label": _("Projected usage after backup"), "projected_usage_label": _("Projected usage after backup"),
"header_title": _("Lx Tools Py-Backup"), "header_title": _("Lx Tools Py-Backup"),
"header_subtitle": _("Simple GUI for rsync"), "header_subtitle": _("Simple GUI for rsync"),
"encrypted_backup_content": _("Encrypted Backups"),
"compressed": _("Compressed"), "compressed": _("Compressed"),
"encrypted": _("Encrypted"), "encrypted": _("Encrypted"),
"bypass_security": _("Bypass security"), "bypass_security": _("Bypass security"),

View File

@@ -345,6 +345,7 @@ class Actions:
}) })
self.app.config_manager.set_setting( self.app.config_manager.set_setting(
"backup_destination_path", path) "backup_destination_path", path)
self.app.header_frame.refresh_status() # Refresh keyring status
self.app.drawing.redraw_right_canvas() self.app.drawing.redraw_right_canvas()
self.app.drawing.update_target_projection() self.app.drawing.update_target_projection()

View File

@@ -1,27 +1,30 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
import os
from pbp_app_config import Msg from pbp_app_config import Msg
from pyimage_ui.system_backup_content_frame import SystemBackupContentFrame from pyimage_ui.system_backup_content_frame import SystemBackupContentFrame
from pyimage_ui.user_backup_content_frame import UserBackupContentFrame from pyimage_ui.user_backup_content_frame import UserBackupContentFrame
from shared_libs.animated_icon import AnimatedIcon
from shared_libs.logger import app_logger from shared_libs.logger import app_logger
from shared_libs.message import MessageDialog
class BackupContentFrame(ttk.Frame): class BackupContentFrame(ttk.Frame):
def __init__(self, master, backup_manager, actions, **kwargs): def __init__(self, master, backup_manager, actions, app, **kwargs):
super().__init__(master, **kwargs) super().__init__(master, **kwargs)
app_logger.log("BackupContentFrame: __init__ called")
self.backup_manager = backup_manager self.backup_manager = backup_manager
self.actions = actions self.actions = actions
self.master = master self.app = app
self.backup_path = None self.base_backup_path = None
self.current_view_index = 0 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 = ttk.Frame(self)
header_frame.grid(row=0, column=0, sticky=tk.NSEW) header_frame.grid(row=0, column=0, sticky=tk.NSEW)
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)
top_nav_frame = ttk.Frame(header_frame) top_nav_frame = ttk.Frame(header_frame)
top_nav_frame.pack(side=tk.LEFT) top_nav_frame.pack(side=tk.LEFT)
@@ -50,32 +53,82 @@ class BackupContentFrame(ttk.Frame):
ttk.Separator(top_nav_frame, orient=tk.VERTICAL).pack( ttk.Separator(top_nav_frame, orient=tk.VERTICAL).pack(
side=tk.LEFT, fill=tk.Y, padx=2) side=tk.LEFT, fill=tk.Y, padx=2)
# Deletion Status UI content_container = ttk.Frame(self)
self.deletion_status_frame = ttk.Frame(header_frame) content_container.grid(row=1, column=0, sticky="nsew")
self.deletion_status_frame.pack(side=tk.LEFT, padx=15) content_container.grid_rowconfigure(0, weight=1)
content_container.grid_columnconfigure(0, weight=1)
bg_color = self.winfo_toplevel().style.lookup('TFrame', 'background') self.system_backups_frame = SystemBackupContentFrame(content_container, backup_manager, actions, parent_view=self)
self.deletion_animated_icon = AnimatedIcon( self.user_backups_frame = UserBackupContentFrame(content_container, backup_manager, actions, parent_view=self)
self.deletion_status_frame, width=20, height=20, use_pillow=True, bg=bg_color, animation_type="counter_arc") self.system_backups_frame.grid(row=0, column=0, sticky=tk.NSEW)
self.deletion_animated_icon.pack(side=tk.LEFT, padx=5) self.user_backups_frame.grid(row=0, column=0, sticky=tk.NSEW)
self.deletion_animated_icon.stop("DISABLE")
self.deletion_status_label = ttk.Label( action_button_frame = ttk.Frame(self, padding=10)
self.deletion_status_frame, text="", font=("Ubuntu", 10, "bold")) action_button_frame.grid(row=2, column=0, sticky="ew")
self.deletion_status_label.pack(side=tk.LEFT, padx=5)
# --- Content Frames --- self.toggle_encrypted_button = ttk.Button(action_button_frame, text=Msg.STR["show_encrypted_backups"], command=self._toggle_encrypted_view)
self.system_backups_frame = SystemBackupContentFrame( self.toggle_encrypted_button.pack(side=tk.LEFT, padx=5)
self, backup_manager, actions)
self.user_backups_frame = UserBackupContentFrame(self, backup_manager, actions)
self.system_backups_frame.grid(row=1, column=0, sticky=tk.NSEW) self.restore_button = ttk.Button(action_button_frame, text=Msg.STR["restore"], command=self._restore_selected, state="disabled")
self.user_backups_frame.grid(row=1, column=0, sticky=tk.NSEW) self.restore_button.pack(side=tk.LEFT, padx=5)
self._switch_view(self.current_view_index) 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 _toggle_encrypted_view(self):
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"])
return
pybackup_dir = os.path.join(self.app.destination_path, "pybackup")
if not self.viewing_encrypted:
username = os.path.basename(self.app.destination_path.rstrip('/'))
password = self.app.backup_manager.encryption_manager.get_password(username, confirm=False)
if not password:
return
mount_point = self.app.backup_manager.encryption_manager.unlock_container(pybackup_dir, password)
if mount_point:
self.viewing_encrypted = True
self.toggle_encrypted_button.config(text=Msg.STR["show_normal_backups"])
self.show(mount_point)
self.app.header_frame.refresh_status()
else:
MessageDialog(master=self.app, message_type="error", title=Msg.STR["error"], text=Msg.STR["err_unlock_failed"])
else:
self.app.backup_manager.encryption_manager.lock_container(pybackup_dir)
self.viewing_encrypted = False
self.toggle_encrypted_button.config(text=Msg.STR["show_encrypted_backups"])
self.show(self.app.destination_path)
self.app.header_frame.refresh_status()
def _switch_view(self, index): def _switch_view(self, index):
self.current_view_index = 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) self.update_nav_buttons(index)
if index == 0: if index == 0:
@@ -84,6 +137,7 @@ class BackupContentFrame(ttk.Frame):
else: else:
self.user_backups_frame.grid() self.user_backups_frame.grid()
self.system_backups_frame.grid_remove() self.system_backups_frame.grid_remove()
self.update_button_state(False)
def update_nav_buttons(self, active_index): def update_nav_buttons(self, active_index):
for i, button in enumerate(self.nav_buttons): for i, button in enumerate(self.nav_buttons):
@@ -96,26 +150,26 @@ class BackupContentFrame(ttk.Frame):
self.nav_progress_bars[i].pack_forget() self.nav_progress_bars[i].pack_forget()
def show(self, backup_path): def show(self, backup_path):
app_logger.log(f"BackupContentFrame: show called with path {backup_path}")
self.grid(row=2, column=0, sticky="nsew") self.grid(row=2, column=0, sticky="nsew")
if backup_path and self.backup_path != backup_path:
self.backup_path = backup_path
self.system_backups_frame.show(backup_path)
self.user_backups_frame.show(backup_path)
# Ensure the correct view is shown upon revealing the frame self.base_backup_path = backup_path
self._switch_view(self.current_view_index)
if self.viewing_encrypted:
actual_backup_path = backup_path
else:
actual_backup_path = os.path.join(backup_path, "pybackup")
def hide(self): if not os.path.isdir(actual_backup_path):
self.grid_remove() app_logger.log(f"Backup path {actual_backup_path} does not exist or is not a directory.")
# Clear views if path is invalid
self.system_backups_frame.show(None)
self.user_backups_frame.show(None)
return
def show_deletion_status(self, text: str): self.system_backups_frame.show(actual_backup_path)
app_logger.log(f"Showing deletion status: {text}") self.user_backups_frame.show(actual_backup_path)
self.deletion_status_label.config(text=text)
self.deletion_animated_icon.start() config_key = "last_encrypted_backup_content_view" if self.viewing_encrypted else "last_backup_content_view"
self.deletion_status_frame.pack(side=tk.LEFT, padx=15) last_view = self.app.config_manager.get_setting(config_key, 0)
self._switch_view(last_view)
def hide_deletion_status(self):
app_logger.log("Hiding deletion status.")
self.deletion_animated_icon.stop("DISABLE")
self.deletion_status_frame.pack_forget()

View File

@@ -1,14 +1,16 @@
import tkinter as tk import tkinter as tk
import os
from pbp_app_config import Msg from pbp_app_config import Msg
from shared_libs.common_tools import IconManager from shared_libs.common_tools import IconManager
class HeaderFrame(tk.Frame): class HeaderFrame(tk.Frame):
def __init__(self, container, image_manager, **kwargs): def __init__(self, container, image_manager, encryption_manager, app, **kwargs):
super().__init__(container, bg="#455A64", **kwargs) super().__init__(container, bg="#455A64", **kwargs)
self.image_manager = image_manager self.image_manager = image_manager
self.encryption_manager = encryption_manager
self.app = app
# Configure grid weights for internal layout # Configure grid weights for internal layout
self.columnconfigure(1, weight=1) # Make the middle column expand self.columnconfigure(1, weight=1) # Make the middle column expand
@@ -48,18 +50,48 @@ class HeaderFrame(tk.Frame):
subtitle_label.grid(row=1, column=1, sticky="w", subtitle_label.grid(row=1, column=1, sticky="w",
padx=(5, 20), pady=(0, 10)) padx=(5, 20), pady=(0, 10))
# Right side: Placeholder for future info or buttons # Right side: Keyring status
right_frame = tk.Frame(self, bg="#455A64") right_frame = tk.Frame(self, bg="#455A64")
right_frame.grid(row=0, column=2, rowspan=2, sticky="nsew") right_frame.grid(row=0, column=2, rowspan=2, sticky="nsew")
right_frame.columnconfigure(0, weight=1) right_frame.columnconfigure(0, weight=1)
right_frame.rowconfigure(0, weight=1) right_frame.rowconfigure(0, weight=1)
# Example of content for the right side (can be removed or replaced) self.keyring_status_label = tk.Label(
# info_label = tk.Label( right_frame,
# right_frame, text="",
# text="Some Info Here", font=("Helvetica", 10, "bold"),
# font=("Helvetica", 10), bg="#455A64",
# fg="#ecf0f1", )
# bg="#455A64", self.keyring_status_label.grid(row=0, column=0, sticky="ne", padx=(10, 10), pady=(10, 0))
# )
# info_label.grid(row=0, column=0, sticky="ne", padx=(10, 10), pady=(10, 0)) self.refresh_status()
def refresh_status(self):
"""Checks the keyring status based on the current destination and updates the label."""
dest_path = self.app.destination_path
if not dest_path:
self.keyring_status_label.config(
text="Keyring: N/A",
fg="#A9A9A9" # DarkGray
)
return
username = os.path.basename(dest_path.rstrip('/'))
mapper_name = f"pybackup_{username}"
mount_point = f"/mnt/{mapper_name}"
if os.path.ismount(mount_point):
self.keyring_status_label.config(
text="Keyring: In Use",
fg="#2E8B57" # SeaGreen
)
elif self.encryption_manager.is_key_in_keyring(username):
self.keyring_status_label.config(
text="Keyring: Available",
fg="#FFD700" # Gold
)
else:
self.keyring_status_label.config(
text="Keyring: Not in Use",
fg="#A9A9A9" # DarkGray
)

View File

@@ -141,7 +141,7 @@ class Navigation:
self.app.log_frame.grid_remove() self.app.log_frame.grid_remove()
self.app.scheduler_frame.hide() self.app.scheduler_frame.hide()
self.app.settings_frame.hide() self.app.settings_frame.hide()
self.app.backup_content_frame.hide() self.app.backup_content_frame.grid_remove()
# Show the main content frames # Show the main content frames
@@ -186,7 +186,7 @@ class Navigation:
self.app.log_frame.grid_remove() self.app.log_frame.grid_remove()
self.app.scheduler_frame.hide() self.app.scheduler_frame.hide()
self.app.settings_frame.hide() self.app.settings_frame.hide()
self.app.backup_content_frame.hide() self.app.backup_content_frame.grid_remove()
self.app.canvas_frame.grid() self.app.canvas_frame.grid()
self.app.source_size_frame.grid() self.app.source_size_frame.grid()
@@ -224,7 +224,7 @@ class Navigation:
self.app.canvas_frame.grid_remove() self.app.canvas_frame.grid_remove()
self.app.scheduler_frame.hide() self.app.scheduler_frame.hide()
self.app.settings_frame.hide() self.app.settings_frame.hide()
self.app.backup_content_frame.hide() self.app.backup_content_frame.grid_remove()
self.app.source_size_frame.grid_remove() self.app.source_size_frame.grid_remove()
self.app.target_size_frame.grid_remove() self.app.target_size_frame.grid_remove()
self.app.restore_size_frame_before.grid_remove() self.app.restore_size_frame_before.grid_remove()
@@ -241,7 +241,7 @@ class Navigation:
self.app.canvas_frame.grid_remove() self.app.canvas_frame.grid_remove()
self.app.log_frame.grid_remove() self.app.log_frame.grid_remove()
self.app.settings_frame.hide() self.app.settings_frame.hide()
self.app.backup_content_frame.hide() self.app.backup_content_frame.grid_remove()
self.app.source_size_frame.grid_remove() self.app.source_size_frame.grid_remove()
self.app.target_size_frame.grid_remove() self.app.target_size_frame.grid_remove()
@@ -258,7 +258,7 @@ class Navigation:
self.app.canvas_frame.grid_remove() self.app.canvas_frame.grid_remove()
self.app.log_frame.grid_remove() self.app.log_frame.grid_remove()
self.app.backup_content_frame.hide() self.app.backup_content_frame.grid_remove()
self.app.scheduler_frame.hide() self.app.scheduler_frame.hide()
self.app.source_size_frame.grid_remove() self.app.source_size_frame.grid_remove()

View File

@@ -5,17 +5,15 @@ import os
from pbp_app_config import Msg from pbp_app_config import Msg
from pyimage_ui.comment_editor_dialog import CommentEditorDialog from pyimage_ui.comment_editor_dialog import CommentEditorDialog
class SystemBackupContentFrame(ttk.Frame): class SystemBackupContentFrame(ttk.Frame):
def __init__(self, master, backup_manager, actions, **kwargs): def __init__(self, master, backup_manager, actions, parent_view, **kwargs):
super().__init__(master, **kwargs) super().__init__(master, **kwargs)
self.backup_manager = backup_manager self.backup_manager = backup_manager
self.actions = actions self.actions = actions
self.parent_view = parent_view
self.system_backups_list = [] self.system_backups_list = []
self.backup_path = None self.backup_path = None
# --- Color Tags ---
self.tag_colors = [ self.tag_colors = [
("full_blue", "#0078D7", "inc_blue", "#50E6FF"), ("full_blue", "#0078D7", "inc_blue", "#50E6FF"),
("full_orange", "#E8740C", "inc_orange", "#FFB366"), ("full_orange", "#E8740C", "inc_orange", "#FFB366"),
@@ -23,24 +21,13 @@ class SystemBackupContentFrame(ttk.Frame):
("full_purple", "#8B107C", "inc_purple", "#D46EE5"), ("full_purple", "#8B107C", "inc_purple", "#D46EE5"),
] ]
# --- 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", "time", "type", "size", "comment") columns = ("date", "time", "type", "size", "comment")
self.content_tree = ttk.Treeview( self.content_tree = ttk.Treeview(self, columns=columns, show="headings")
self.content_frame, columns=columns, show="headings") self.content_tree.heading("date", text=Msg.STR["date"])
self.content_tree.heading( self.content_tree.heading("time", text=Msg.STR["time"])
"date", text=Msg.STR["date"]) self.content_tree.heading("type", text=Msg.STR["type"])
self.content_tree.heading( self.content_tree.heading("size", text=Msg.STR["size"])
"time", text=Msg.STR["time"]) self.content_tree.heading("comment", text=Msg.STR["comment"])
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("date", width=100, anchor="w")
self.content_tree.column("time", width=80, anchor="center") self.content_tree.column("time", width=80, anchor="center")
@@ -49,31 +36,11 @@ class SystemBackupContentFrame(ttk.Frame):
self.content_tree.column("comment", width=300, anchor="w") 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.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.content_tree.bind("<<TreeviewSelect>>", self._on_item_select) 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): def show(self, backup_path):
if backup_path and self.backup_path != backup_path: self.backup_path = backup_path
self.backup_path = backup_path self._load_backup_content()
self._load_backup_content()
def hide(self):
self.grid_remove()
def _load_backup_content(self): def _load_backup_content(self):
for i in self.content_tree.get_children(): for i in self.content_tree.get_children():
@@ -82,23 +49,19 @@ class SystemBackupContentFrame(ttk.Frame):
if not self.backup_path or not os.path.isdir(self.backup_path): if not self.backup_path or not os.path.isdir(self.backup_path):
return return
system_backups = self.backup_manager.list_system_backups(self.backup_path) self.system_backups_list = self.backup_manager.list_system_backups(self.backup_path)
self.system_backups_list = system_backups # Store for later use
color_index = 0 color_index = -1
for i, backup_info in enumerate(system_backups): for i, backup_info in enumerate(self.system_backups_list):
# Determine the color tag for the group
if backup_info.get("backup_type_base") == "Full": if backup_info.get("backup_type_base") == "Full":
if i > 0: # Not the very first backup color_index = (color_index + 1) % len(self.tag_colors)
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)
tag_name, color, _, _ = self.tag_colors[color_index] self.content_tree.tag_configure(inc_tag, foreground=inc_color, font=("Helvetica", 10, "bold"))
self.content_tree.tag_configure(tag_name, foreground=color) current_tag = full_tag
current_tag = tag_name else:
else: # Incremental _, _, inc_tag, _ = self.tag_colors[color_index]
_, _, tag_name, color = self.tag_colors[color_index] current_tag = inc_tag
self.content_tree.tag_configure(tag_name, foreground=color)
current_tag = tag_name
self.content_tree.insert("", "end", values=( self.content_tree.insert("", "end", values=(
backup_info.get("date", "N/A"), backup_info.get("date", "N/A"),
@@ -108,79 +71,57 @@ class SystemBackupContentFrame(ttk.Frame):
backup_info.get("comment", ""), backup_info.get("comment", ""),
), tags=(current_tag,), iid=backup_info.get("folder_name")) ), tags=(current_tag,), iid=backup_info.get("folder_name"))
self._on_item_select(None) # Disable buttons initially self._on_item_select(None)
def _on_item_select(self, event): def _on_item_select(self, event):
selected_item = self.content_tree.focus() is_selected = True if self.content_tree.focus() else False
is_selected = True if selected_item else False self.parent_view.update_button_state(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 _edit_comment(self): def _edit_comment(self):
selected_item_id = self.content_tree.focus() selected_item_id = self.content_tree.focus()
if not selected_item_id: if not selected_item_id:
return return
# Construct the path to the info file info_file_path = os.path.join(self.backup_path, f"{selected_item_id}.txt")
pybackup_path = os.path.join(self.backup_path, "pybackup")
info_file_path = os.path.join(pybackup_path, f"{selected_item_id}.txt")
if not os.path.exists(info_file_path): if not os.path.exists(info_file_path):
self.backup_manager.update_comment(info_file_path, "") self.backup_manager.update_comment(info_file_path, "")
CommentEditorDialog(self, info_file_path, self.backup_manager) CommentEditorDialog(self, info_file_path, self.backup_manager)
self._load_backup_content() # Refresh list to show new comment self._load_backup_content()
def _restore_selected(self): def _restore_selected(self):
selected_item_id = self.content_tree.focus() selected_item_id = self.content_tree.focus()
if not selected_item_id: if not selected_item_id:
return return
selected_backup = None selected_backup = next((b for b in self.system_backups_list if b.get("folder_name") == selected_item_id), None)
for backup in self.system_backups_list:
if backup.get("folder_name") == selected_item_id:
selected_backup = backup
break
if not selected_backup: if not selected_backup:
print(f"Error: Could not find backup info for {selected_item_id}")
return return
try: main_app = self.winfo_toplevel()
main_app = self.winfo_toplevel() restore_dest_path = main_app.config_manager.get_setting("restore_destination_path", "/")
restore_dest_path = main_app.config_manager.get_setting(
"restore_destination_path", "/")
if not restore_dest_path: if not restore_dest_path:
print("Error: Restore destination not set.") return
return
self.backup_manager.start_restore( self.backup_manager.start_restore(
source_path=selected_backup['full_path'], source_path=selected_backup['full_path'],
dest_path=restore_dest_path, dest_path=restore_dest_path,
is_compressed=selected_backup['is_compressed'] is_compressed=selected_backup['is_compressed']
) )
except AttributeError:
print("Could not access main application instance to get restore path.")
def _delete_selected(self): def _delete_selected(self):
selected_item_id = self.content_tree.focus() selected_item_id = self.content_tree.focus()
if not selected_item_id: if not selected_item_id:
return return
# Construct the full path to the backup folder folder_to_delete = os.path.join(self.backup_path, selected_item_id)
pybackup_path = os.path.join(self.backup_path, "pybackup")
folder_to_delete = os.path.join(pybackup_path, selected_item_id)
# Lock UI and show status self.actions._set_ui_state(False)
self.actions._set_ui_state(False) # Lock UI # This needs to be adapted, as the deletion status is now in the parent view
self.master.show_deletion_status( # self.master.show_deletion_status(Msg.STR["deleting_backup_in_progress"])
Msg.STR["deleting_backup_in_progress"])
# Start deletion in background
self.backup_manager.start_delete_system_backup( self.backup_manager.start_delete_system_backup(
folder_to_delete, self.winfo_toplevel().queue) folder_to_delete, self.winfo_toplevel().queue)

View File

@@ -1,87 +1,28 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
import os import os
import shutil
from pbp_app_config import Msg from pbp_app_config import Msg
from pyimage_ui.comment_editor_dialog import CommentEditorDialog from pyimage_ui.comment_editor_dialog import CommentEditorDialog
from shared_libs.message import MessageDialog
class UserBackupContentFrame(ttk.Frame): class UserBackupContentFrame(ttk.Frame):
def __init__(self, master, backup_manager, actions, **kwargs): def __init__(self, master, backup_manager, actions, parent_view, **kwargs):
super().__init__(master, **kwargs)
self.backup_manager = backup_manager
self.backup_path = None
self.actions = actions # Store actions object
# --- 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", "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(
"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("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)
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 UserBackupContentFrame(ttk.Frame):
def __init__(self, master, backup_manager, actions, **kwargs):
super().__init__(master, **kwargs) super().__init__(master, **kwargs)
self.backup_manager = backup_manager self.backup_manager = backup_manager
self.actions = actions self.actions = actions
self.parent_view = parent_view
self.user_backups_list = [] self.user_backups_list = []
self.backup_path = None 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", "time", "size", "comment", "folder_name") columns = ("date", "time", "size", "comment", "folder_name")
self.content_tree = ttk.Treeview( self.content_tree = ttk.Treeview(self, columns=columns, show="headings")
self.content_frame, columns=columns, show="headings") self.content_tree.heading("date", text=Msg.STR["date"])
self.content_tree.heading( self.content_tree.heading("time", text=Msg.STR["time"])
"date", text=Msg.STR["date"]) self.content_tree.heading("size", text=Msg.STR["size"])
self.content_tree.heading( self.content_tree.heading("comment", text=Msg.STR["comment"])
"time", text=Msg.STR["time"]) self.content_tree.heading("folder_name", text=Msg.STR["folder"])
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=100, anchor="w") self.content_tree.column("date", width=100, anchor="w")
self.content_tree.column("time", width=80, anchor="center") self.content_tree.column("time", width=80, anchor="center")
@@ -90,31 +31,11 @@ class UserBackupContentFrame(ttk.Frame):
self.content_tree.column("folder_name", width=200, anchor="w") self.content_tree.column("folder_name", width=200, anchor="w")
self.content_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.content_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.content_tree.bind("<<TreeviewSelect>>", self._on_item_select) 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): def show(self, backup_path):
if backup_path and self.backup_path != backup_path: self.backup_path = backup_path
self.backup_path = backup_path self._load_backup_content()
self._load_backup_content()
def hide(self):
self.grid_remove()
def _load_backup_content(self): def _load_backup_content(self):
for i in self.content_tree.get_children(): for i in self.content_tree.get_children():
@@ -133,17 +54,11 @@ class UserBackupContentFrame(ttk.Frame):
backup_info.get("comment", ""), backup_info.get("comment", ""),
backup_info.get("folder_name", "N/A") backup_info.get("folder_name", "N/A")
), iid=backup_info.get("folder_name")) ), iid=backup_info.get("folder_name"))
self._on_item_select(None) # Disable buttons initially self._on_item_select(None)
def _on_item_select(self, event): def _on_item_select(self, event):
selected_item = self.content_tree.focus() is_selected = True if self.content_tree.focus() else False
is_selected = True if selected_item else False self.parent_view.update_button_state(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 _edit_comment(self): def _edit_comment(self):
selected_item_id = self.content_tree.focus() selected_item_id = self.content_tree.focus()
@@ -156,109 +71,37 @@ class UserBackupContentFrame(ttk.Frame):
self.backup_manager.update_comment(info_file_path, "") self.backup_manager.update_comment(info_file_path, "")
CommentEditorDialog(self, info_file_path, self.backup_manager) CommentEditorDialog(self, info_file_path, self.backup_manager)
self._load_backup_content() # Refresh list to show new comment self._load_backup_content()
def _restore_selected(self): def _restore_selected(self):
# This functionality needs to be implemented based on app requirements selected_item_id = self.content_tree.focus()
pass if not selected_item_id:
return
MessageDialog(master=self, message_type="info", title="Info", text="User restore not implemented yet.")
def _delete_selected(self): def _delete_selected(self):
# This functionality needs to be implemented based on app requirements selected_item_id = self.content_tree.focus()
pass if not selected_item_id:
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 return
user_backups = self.backup_manager.list_user_backups(self.backup_path) folder_to_delete = os.path.join(self.backup_path, selected_item_id)
info_file_to_delete = os.path.join(self.backup_path, f"{selected_item_id}.txt")
for backup_info in user_backups:
self.content_tree.insert("", "end", values=(
backup_info.get("date", "N/A"),
backup_info.get("size", "N/A"),
backup_info.get("comment", ""),
backup_info.get("folder_name", "N/A")
))
self._on_item_select(None)
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[3] # Assuming folder_name is the 4th value
# Construct the path to the info file
info_file_path = os.path.join(self.backup_path, f"{folder_name}.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() # Refresh list to show new comment
def _restore_selected(self):
# Placeholder for restore logic
selected_item = self.content_tree.focus()
if not selected_item:
return
backup_name = self.content_tree.item(selected_item)["values"][0]
print(f"Restoring {backup_name}...")
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[3] # Assuming folder_name is the 4th value
# Construct the full path to the backup folder
folder_to_delete = os.path.join(self.backup_path, folder_name)
info_file_to_delete = os.path.join(
self.backup_path, f"{folder_name}.txt")
# Ask for confirmation
from shared_libs.message import MessageDialog
dialog = MessageDialog(master=self, message_type="warning", dialog = MessageDialog(master=self, message_type="warning",
title=Msg.STR["confirm_delete_title"], title=Msg.STR["confirm_delete_title"],
text=Msg.STR["confirm_delete_text"].format( text=Msg.STR["confirm_delete_text"].format(
folder_name=folder_name), folder_name=selected_item_id),
buttons=["ok_cancel"]) buttons=["ok_cancel"])
if dialog.get_result() != "ok": if dialog.get_result() != "ok":
return return
try: try:
import shutil
if os.path.isdir(folder_to_delete): if os.path.isdir(folder_to_delete):
shutil.rmtree(folder_to_delete) shutil.rmtree(folder_to_delete)
if os.path.exists(info_file_to_delete): if os.path.exists(info_file_to_delete):
os.remove(info_file_to_delete) os.remove(info_file_to_delete)
self._load_backup_content()
except Exception as e: except Exception as e:
MessageDialog(master=self, message_type="error", MessageDialog(master=self, message_type="error",
title=Msg.STR["error"], text=str(e)) title=Msg.STR["error"], text=str(e))
self._load_backup_content()