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:
@@ -582,54 +582,16 @@ set -e
|
||||
backups.append(item)
|
||||
return sorted(backups, reverse=True)
|
||||
|
||||
def list_system_backups(self, base_backup_path: str) -> List[Dict[str, str]]:
|
||||
"""Lists all system backups found in the pybackup subdirectory."""
|
||||
system_backups = []
|
||||
pybackup_path = os.path.join(base_backup_path, "pybackup")
|
||||
def list_system_backups(self, scan_path: str, is_encrypted_and_mounted: bool = False) -> List[Dict[str, str]]:
|
||||
"""Lists all system backups, handling encrypted containers and sorting into groups."""
|
||||
if not os.path.isdir(scan_path):
|
||||
return []
|
||||
|
||||
if not os.path.isdir(pybackup_path):
|
||||
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
|
||||
all_backups = []
|
||||
name_regex = re.compile(
|
||||
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'):
|
||||
continue
|
||||
|
||||
@@ -637,39 +599,29 @@ set -e
|
||||
if not match:
|
||||
continue
|
||||
|
||||
full_path = os.path.join(pybackup_path, item)
|
||||
date_str = match.group(1)
|
||||
time_str = match.group(2)
|
||||
backup_type_base = match.group(3).capitalize()
|
||||
extension = match.group(4)
|
||||
full_path = os.path.join(scan_path, item)
|
||||
date_str, time_str, backup_type_base, extension = match.groups()
|
||||
is_compressed = (extension == ".tar.gz")
|
||||
is_encrypted = False # Individual folders are not encrypted in this logic
|
||||
|
||||
backup_type = backup_type_base
|
||||
|
||||
backup_type = backup_type_base.capitalize()
|
||||
if is_compressed:
|
||||
backup_type += " (Compressed)"
|
||||
|
||||
backup_size = "N/A"
|
||||
comment = ""
|
||||
|
||||
info_file_path = os.path.join(pybackup_path, f"{item}.txt")
|
||||
info_file_path = os.path.join(scan_path, f"{item}.txt")
|
||||
if os.path.exists(info_file_path):
|
||||
try:
|
||||
with open(info_file_path, 'r') as f:
|
||||
for line in f:
|
||||
if line.strip().lower().startswith("originalgröße:"):
|
||||
size_part = line.split(":", 1)[1].strip()
|
||||
if '(' in size_part:
|
||||
backup_size = size_part.split('(')[0].strip()
|
||||
else:
|
||||
backup_size = size_part
|
||||
backup_size = line.split(":", 1)[1].strip().split('(')[0].strip()
|
||||
elif line.strip().lower().startswith("kommentar:"):
|
||||
comment = line.split(":", 1)[1].strip()
|
||||
except Exception as e:
|
||||
self.logger.log(
|
||||
f"Could not read info file {info_file_path}: {e}")
|
||||
self.logger.log(f"Could not read info file {info_file_path}: {e}")
|
||||
|
||||
system_backups.append({
|
||||
all_backups.append({
|
||||
"date": date_str,
|
||||
"time": time_str,
|
||||
"type": backup_type,
|
||||
@@ -678,20 +630,35 @@ set -e
|
||||
"full_path": full_path,
|
||||
"comment": comment,
|
||||
"is_compressed": is_compressed,
|
||||
"is_encrypted": is_encrypted,
|
||||
"backup_type_base": backup_type_base
|
||||
"is_encrypted": is_encrypted_and_mounted,
|
||||
"backup_type_base": backup_type_base.capitalize(),
|
||||
"datetime": datetime.datetime.strptime(f"{date_str} {time_str}", '%d-%m-%Y %H:%M:%S')
|
||||
})
|
||||
|
||||
try:
|
||||
# Sort chronologically, oldest first
|
||||
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)
|
||||
# Sort all backups chronologically to make grouping easier
|
||||
all_backups.sort(key=lambda x: x['datetime'])
|
||||
|
||||
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]]:
|
||||
"""Lists all user backups found in the base backup path."""
|
||||
|
||||
@@ -25,12 +25,20 @@ class EncryptionManager:
|
||||
try:
|
||||
return keyring.get_password(self.service_id, username)
|
||||
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
|
||||
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
|
||||
|
||||
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:
|
||||
try:
|
||||
keyring.set_password(self.service_id, username, password)
|
||||
@@ -69,6 +77,43 @@ class EncryptionManager:
|
||||
|
||||
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]:
|
||||
"""Sets up a persistent LUKS encrypted container for the backup destination."""
|
||||
self.logger.log(f"Setting up encrypted container at {base_path}")
|
||||
@@ -80,7 +125,7 @@ class EncryptionManager:
|
||||
|
||||
container_file = "pybackup_encrypted.luks"
|
||||
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}"
|
||||
|
||||
if not password:
|
||||
@@ -95,17 +140,7 @@ class EncryptionManager:
|
||||
|
||||
if os.path.exists(container_path):
|
||||
self.logger.log(f"Encrypted container {container_path} already exists. Attempting to unlock.")
|
||||
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.")
|
||||
queue.put(('error', "Failed to unlock existing encrypted container."))
|
||||
# Clean up failed mount attempt
|
||||
self.cleanup_encrypted_backup(mapper_name, mount_point)
|
||||
return None
|
||||
return self.unlock_container(base_path, password)
|
||||
else:
|
||||
self.logger.log(f"Creating new encrypted container: {container_path}")
|
||||
script = f"""
|
||||
@@ -119,10 +154,11 @@ class EncryptionManager:
|
||||
|
||||
if not self._execute_as_root(script):
|
||||
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
|
||||
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."))
|
||||
return None
|
||||
|
||||
|
||||
71
main_app.py
71
main_app.py
@@ -4,6 +4,7 @@ from tkinter import ttk
|
||||
import os
|
||||
import datetime
|
||||
from queue import Queue, Empty
|
||||
import shutil
|
||||
|
||||
from shared_libs.log_window import LogWindow
|
||||
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.settings_button.pack(fill=tk.X, pady=10)
|
||||
|
||||
|
||||
|
||||
self.header_frame = HeaderFrame(self.content_frame, self.image_manager)
|
||||
self.header_frame = HeaderFrame(self.content_frame, self.image_manager, self.backup_manager.encryption_manager, self)
|
||||
self.header_frame.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
self.top_bar = ttk.Frame(self.content_frame)
|
||||
@@ -223,7 +222,6 @@ class MainApplication(tk.Tk):
|
||||
self._setup_settings_frame()
|
||||
self._setup_backup_content_frame()
|
||||
|
||||
|
||||
self._setup_task_bar()
|
||||
|
||||
self.source_size_frame = ttk.LabelFrame(
|
||||
@@ -299,15 +297,13 @@ class MainApplication(tk.Tk):
|
||||
self.restore_size_frame_after.grid_remove()
|
||||
|
||||
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)
|
||||
|
||||
def _load_state_and_initialize(self):
|
||||
self.log_window.clear_log()
|
||||
"""Loads saved state from config and initializes the UI."""
|
||||
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")
|
||||
if backup_source_path and os.path.isdir(backup_source_path):
|
||||
@@ -317,9 +313,8 @@ class MainApplication(tk.Tk):
|
||||
if folder_name:
|
||||
icon_name = self.buttons_map[folder_name]['icon']
|
||||
else:
|
||||
# Handle custom folder path
|
||||
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({
|
||||
'icon': icon_name,
|
||||
@@ -330,7 +325,7 @@ class MainApplication(tk.Tk):
|
||||
backup_dest_path = self.config_manager.get_setting(
|
||||
"backup_destination_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)
|
||||
self.backup_right_canvas_data.update({
|
||||
'folder': os.path.basename(backup_dest_path.rstrip('/')),
|
||||
@@ -340,7 +335,18 @@ class MainApplication(tk.Tk):
|
||||
self.destination_total_bytes = total
|
||||
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_source_path")
|
||||
@@ -353,7 +359,6 @@ class MainApplication(tk.Tk):
|
||||
restore_dest_path = self.config_manager.get_setting(
|
||||
"restore_destination_path")
|
||||
if restore_dest_path and os.path.isdir(restore_dest_path):
|
||||
# Find the corresponding button_text for the path
|
||||
folder_name = ""
|
||||
for name, path_obj in AppConfig.FOLDER_PATHS.items():
|
||||
if str(path_obj) == restore_dest_path:
|
||||
@@ -366,18 +371,14 @@ class MainApplication(tk.Tk):
|
||||
'path_display': restore_dest_path,
|
||||
})
|
||||
|
||||
# Initialize UI for the last active mode
|
||||
self.navigation.initialize_ui_for_mode(last_mode)
|
||||
|
||||
# Trigger calculations if needed
|
||||
if last_mode == 'backup':
|
||||
self.after(100, self.actions.on_sidebar_button_click,
|
||||
self.backup_left_canvas_data.get('folder', 'Computer'))
|
||||
elif last_mode == 'restore':
|
||||
# Trigger calculation for the right canvas (source) if a path is set
|
||||
if restore_src_path:
|
||||
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(
|
||||
'folder', 'Computer')
|
||||
self.after(100, self.actions.on_sidebar_button_click,
|
||||
@@ -406,14 +407,11 @@ class MainApplication(tk.Tk):
|
||||
|
||||
def _setup_backup_content_frame(self):
|
||||
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_remove()
|
||||
|
||||
|
||||
|
||||
def _setup_task_bar(self):
|
||||
# Define all boolean vars at the top to ensure they exist before use.
|
||||
self.vollbackup_var = tk.BooleanVar()
|
||||
self.inkrementell_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_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.pack(anchor=tk.W, fill=tk.X, pady=5)
|
||||
|
||||
@@ -445,7 +442,6 @@ class MainApplication(tk.Tk):
|
||||
self.time_info_frame, text="Ende: --:--:--")
|
||||
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.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.start_pause_button.grid(row=0, column=2, rowspan=2, padx=5)
|
||||
|
||||
|
||||
|
||||
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)
|
||||
|
||||
# Save paths from the data dictionaries
|
||||
if self.backup_left_canvas_data.get('path_display'):
|
||||
self.config_manager.set_setting(
|
||||
"backup_source_path", self.backup_left_canvas_data['path_display'])
|
||||
@@ -544,7 +539,6 @@ class MainApplication(tk.Tk):
|
||||
else:
|
||||
self.config_manager.set_setting("restore_source_path", None)
|
||||
|
||||
# Stop any ongoing animations before destroying the application
|
||||
if self.left_canvas_animation:
|
||||
self.left_canvas_animation.stop()
|
||||
self.left_canvas_animation.destroy()
|
||||
@@ -562,21 +556,15 @@ class MainApplication(tk.Tk):
|
||||
self.destroy()
|
||||
|
||||
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:
|
||||
for _ in range(100): # Process up to 100 messages at a time
|
||||
for _ in range(100):
|
||||
message = self.queue.get_nowait()
|
||||
|
||||
# --- Size Calculation Message Handling (from data_processing) ---
|
||||
if isinstance(message, tuple) and len(message) in [3, 5]:
|
||||
calc_type, status = None, None
|
||||
if len(message) == 5:
|
||||
button_text, folder_size, mode_when_started, calc_type, status = message
|
||||
else: # len == 3
|
||||
else:
|
||||
button_text, folder_size, mode_when_started = message
|
||||
|
||||
if mode_when_started != self.mode:
|
||||
@@ -641,7 +629,6 @@ class MainApplication(tk.Tk):
|
||||
text=Msg.STR["accurate_size_failed"], foreground="#D32F2F")
|
||||
self.current_file_label.config(text="")
|
||||
|
||||
# --- Backup/Deletion Message Handling (from main_app) ---
|
||||
elif isinstance(message, tuple) and len(message) == 2:
|
||||
message_type, value = message
|
||||
|
||||
@@ -707,15 +694,13 @@ class MainApplication(tk.Tk):
|
||||
self.actions._set_ui_state(True)
|
||||
self.backup_content_frame.system_backups_frame._load_backup_content()
|
||||
elif message_type == 'completion_accurate':
|
||||
# This is now handled by the len=5 case above
|
||||
pass
|
||||
else:
|
||||
app_logger.log(f"Unknown message in queue: {message}")
|
||||
|
||||
except Empty:
|
||||
pass # The queue is empty, do nothing.
|
||||
pass
|
||||
finally:
|
||||
# Always schedule the next check.
|
||||
self.after(100, self._process_queue)
|
||||
|
||||
def _update_duration(self):
|
||||
@@ -732,11 +717,6 @@ class MainApplication(tk.Tk):
|
||||
self.on_closing()
|
||||
|
||||
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_backup", False)
|
||||
force_incremental = self.config_manager.get_setting(
|
||||
@@ -753,14 +733,12 @@ class MainApplication(tk.Tk):
|
||||
self.full_backup_cb.config(state="disabled")
|
||||
self.incremental_cb.config(state="disabled")
|
||||
|
||||
# Compression Logic
|
||||
force_compression = self.config_manager.get_setting(
|
||||
"force_compression", False)
|
||||
if force_compression:
|
||||
self.compressed_var.set(True)
|
||||
self.compressed_cb.config(state="disabled")
|
||||
|
||||
# Encryption Logic
|
||||
force_encryption = self.config_manager.get_setting(
|
||||
"force_encryption", False)
|
||||
if force_encryption:
|
||||
@@ -773,7 +751,6 @@ class MainApplication(tk.Tk):
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
parser = argparse.ArgumentParser(description="Py-Backup Application.")
|
||||
parser.add_argument(
|
||||
|
||||
@@ -299,6 +299,8 @@ class Msg:
|
||||
"cat_documents": _("Documents"),
|
||||
"cat_music": _("Music"),
|
||||
"cat_videos": _("Videos"),
|
||||
"show_encrypted_backups": _("Show Encrypted Backups"),
|
||||
"show_normal_backups": _("Show Normal Backups"),
|
||||
|
||||
# Browser View
|
||||
"backup_location": _("Backup Location"),
|
||||
@@ -315,6 +317,8 @@ class Msg:
|
||||
"err_no_dest_folder": _("Please select a destination folder."),
|
||||
"err_no_source_folder": _("Please select at least one source folder."),
|
||||
"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_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"),
|
||||
@@ -339,6 +343,7 @@ class Msg:
|
||||
"projected_usage_label": _("Projected usage after backup"),
|
||||
"header_title": _("Lx Tools Py-Backup"),
|
||||
"header_subtitle": _("Simple GUI for rsync"),
|
||||
"encrypted_backup_content": _("Encrypted Backups"),
|
||||
"compressed": _("Compressed"),
|
||||
"encrypted": _("Encrypted"),
|
||||
"bypass_security": _("Bypass security"),
|
||||
|
||||
@@ -345,6 +345,7 @@ class Actions:
|
||||
})
|
||||
self.app.config_manager.set_setting(
|
||||
"backup_destination_path", path)
|
||||
self.app.header_frame.refresh_status() # Refresh keyring status
|
||||
self.app.drawing.redraw_right_canvas()
|
||||
self.app.drawing.update_target_projection()
|
||||
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import os
|
||||
|
||||
from 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.animated_icon import AnimatedIcon
|
||||
from shared_libs.logger import app_logger
|
||||
|
||||
from shared_libs.message import MessageDialog
|
||||
|
||||
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)
|
||||
app_logger.log("BackupContentFrame: __init__ called")
|
||||
self.backup_manager = backup_manager
|
||||
self.actions = actions
|
||||
self.master = master
|
||||
self.app = app
|
||||
|
||||
self.backup_path = None
|
||||
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)
|
||||
self.grid_rowconfigure(1, weight=1)
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
|
||||
top_nav_frame = ttk.Frame(header_frame)
|
||||
top_nav_frame.pack(side=tk.LEFT)
|
||||
@@ -50,32 +53,82 @@ class BackupContentFrame(ttk.Frame):
|
||||
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)
|
||||
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)
|
||||
|
||||
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="counter_arc")
|
||||
self.deletion_animated_icon.pack(side=tk.LEFT, padx=5)
|
||||
self.deletion_animated_icon.stop("DISABLE")
|
||||
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)
|
||||
|
||||
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)
|
||||
action_button_frame = ttk.Frame(self, padding=10)
|
||||
action_button_frame.grid(row=2, column=0, sticky="ew")
|
||||
|
||||
# --- Content Frames ---
|
||||
self.system_backups_frame = SystemBackupContentFrame(
|
||||
self, backup_manager, actions)
|
||||
self.user_backups_frame = UserBackupContentFrame(self, backup_manager, actions)
|
||||
self.toggle_encrypted_button = ttk.Button(action_button_frame, text=Msg.STR["show_encrypted_backups"], command=self._toggle_encrypted_view)
|
||||
self.toggle_encrypted_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.system_backups_frame.grid(row=1, column=0, sticky=tk.NSEW)
|
||||
self.user_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.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):
|
||||
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:
|
||||
@@ -84,6 +137,7 @@ class BackupContentFrame(ttk.Frame):
|
||||
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):
|
||||
@@ -96,26 +150,26 @@ class BackupContentFrame(ttk.Frame):
|
||||
self.nav_progress_bars[i].pack_forget()
|
||||
|
||||
def show(self, backup_path):
|
||||
app_logger.log(f"BackupContentFrame: show called with path {backup_path}")
|
||||
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._switch_view(self.current_view_index)
|
||||
self.base_backup_path = backup_path
|
||||
|
||||
if self.viewing_encrypted:
|
||||
actual_backup_path = backup_path
|
||||
else:
|
||||
actual_backup_path = os.path.join(backup_path, "pybackup")
|
||||
|
||||
def hide(self):
|
||||
self.grid_remove()
|
||||
if not os.path.isdir(actual_backup_path):
|
||||
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):
|
||||
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.")
|
||||
self.deletion_animated_icon.stop("DISABLE")
|
||||
self.deletion_status_frame.pack_forget()
|
||||
self.system_backups_frame.show(actual_backup_path)
|
||||
self.user_backups_frame.show(actual_backup_path)
|
||||
|
||||
config_key = "last_encrypted_backup_content_view" if self.viewing_encrypted else "last_backup_content_view"
|
||||
last_view = self.app.config_manager.get_setting(config_key, 0)
|
||||
self._switch_view(last_view)
|
||||
@@ -1,14 +1,16 @@
|
||||
import tkinter as tk
|
||||
import os
|
||||
|
||||
from pbp_app_config import Msg
|
||||
from shared_libs.common_tools import IconManager
|
||||
|
||||
|
||||
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)
|
||||
|
||||
self.image_manager = image_manager
|
||||
self.encryption_manager = encryption_manager
|
||||
self.app = app
|
||||
|
||||
# Configure grid weights for internal layout
|
||||
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",
|
||||
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.grid(row=0, column=2, rowspan=2, sticky="nsew")
|
||||
right_frame.columnconfigure(0, weight=1)
|
||||
right_frame.rowconfigure(0, weight=1)
|
||||
|
||||
# Example of content for the right side (can be removed or replaced)
|
||||
# info_label = tk.Label(
|
||||
# right_frame,
|
||||
# text="Some Info Here",
|
||||
# font=("Helvetica", 10),
|
||||
# fg="#ecf0f1",
|
||||
# bg="#455A64",
|
||||
# )
|
||||
# info_label.grid(row=0, column=0, sticky="ne", padx=(10, 10), pady=(10, 0))
|
||||
self.keyring_status_label = tk.Label(
|
||||
right_frame,
|
||||
text="",
|
||||
font=("Helvetica", 10, "bold"),
|
||||
bg="#455A64",
|
||||
)
|
||||
self.keyring_status_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
|
||||
)
|
||||
|
||||
@@ -141,7 +141,7 @@ class Navigation:
|
||||
self.app.log_frame.grid_remove()
|
||||
self.app.scheduler_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
|
||||
@@ -186,7 +186,7 @@ class Navigation:
|
||||
self.app.log_frame.grid_remove()
|
||||
self.app.scheduler_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.source_size_frame.grid()
|
||||
@@ -224,7 +224,7 @@ class Navigation:
|
||||
self.app.canvas_frame.grid_remove()
|
||||
self.app.scheduler_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.target_size_frame.grid_remove()
|
||||
self.app.restore_size_frame_before.grid_remove()
|
||||
@@ -241,7 +241,7 @@ class Navigation:
|
||||
self.app.canvas_frame.grid_remove()
|
||||
self.app.log_frame.grid_remove()
|
||||
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.target_size_frame.grid_remove()
|
||||
@@ -258,7 +258,7 @@ class Navigation:
|
||||
|
||||
self.app.canvas_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.source_size_frame.grid_remove()
|
||||
|
||||
@@ -5,17 +5,15 @@ 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, actions, **kwargs):
|
||||
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
|
||||
|
||||
# --- Color Tags ---
|
||||
self.tag_colors = [
|
||||
("full_blue", "#0078D7", "inc_blue", "#50E6FF"),
|
||||
("full_orange", "#E8740C", "inc_orange", "#FFB366"),
|
||||
@@ -23,24 +21,13 @@ class SystemBackupContentFrame(ttk.Frame):
|
||||
("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")
|
||||
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(
|
||||
"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 = 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")
|
||||
@@ -49,31 +36,11 @@ class SystemBackupContentFrame(ttk.Frame):
|
||||
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)
|
||||
|
||||
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()
|
||||
self.backup_path = backup_path
|
||||
self._load_backup_content()
|
||||
|
||||
def _load_backup_content(self):
|
||||
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):
|
||||
return
|
||||
|
||||
system_backups = self.backup_manager.list_system_backups(self.backup_path)
|
||||
self.system_backups_list = system_backups # Store for later use
|
||||
self.system_backups_list = self.backup_manager.list_system_backups(self.backup_path)
|
||||
|
||||
color_index = 0
|
||||
for i, backup_info in enumerate(system_backups):
|
||||
# Determine the color tag for the group
|
||||
color_index = -1
|
||||
for i, backup_info in enumerate(self.system_backups_list):
|
||||
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)
|
||||
|
||||
tag_name, color, _, _ = self.tag_colors[color_index]
|
||||
self.content_tree.tag_configure(tag_name, foreground=color)
|
||||
current_tag = tag_name
|
||||
else: # Incremental
|
||||
_, _, tag_name, color = self.tag_colors[color_index]
|
||||
self.content_tree.tag_configure(tag_name, foreground=color)
|
||||
current_tag = tag_name
|
||||
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"),
|
||||
@@ -108,79 +71,57 @@ class SystemBackupContentFrame(ttk.Frame):
|
||||
backup_info.get("comment", ""),
|
||||
), 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):
|
||||
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")
|
||||
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
|
||||
|
||||
# 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"{selected_item_id}.txt")
|
||||
info_file_path = os.path.join(self.backup_path, 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() # Refresh list to show new comment
|
||||
self._load_backup_content()
|
||||
|
||||
def _restore_selected(self):
|
||||
selected_item_id = self.content_tree.focus()
|
||||
if not selected_item_id:
|
||||
return
|
||||
|
||||
selected_backup = None
|
||||
for backup in self.system_backups_list:
|
||||
if backup.get("folder_name") == selected_item_id:
|
||||
selected_backup = backup
|
||||
break
|
||||
selected_backup = next((b for b in self.system_backups_list if b.get("folder_name") == selected_item_id), None)
|
||||
|
||||
if not selected_backup:
|
||||
print(f"Error: Could not find backup info for {selected_item_id}")
|
||||
return
|
||||
|
||||
try:
|
||||
main_app = self.winfo_toplevel()
|
||||
restore_dest_path = main_app.config_manager.get_setting(
|
||||
"restore_destination_path", "/")
|
||||
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.")
|
||||
return
|
||||
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']
|
||||
)
|
||||
except AttributeError:
|
||||
print("Could not access main application instance to get restore path.")
|
||||
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
|
||||
|
||||
# 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, selected_item_id)
|
||||
folder_to_delete = os.path.join(self.backup_path, selected_item_id)
|
||||
|
||||
# Lock UI and show status
|
||||
self.actions._set_ui_state(False) # Lock UI
|
||||
self.master.show_deletion_status(
|
||||
Msg.STR["deleting_backup_in_progress"])
|
||||
self.actions._set_ui_state(False)
|
||||
# This needs to be adapted, as the deletion status is now in the parent view
|
||||
# self.master.show_deletion_status(Msg.STR["deleting_backup_in_progress"])
|
||||
|
||||
# Start deletion in background
|
||||
self.backup_manager.start_delete_system_backup(
|
||||
folder_to_delete, self.winfo_toplevel().queue)
|
||||
folder_to_delete, self.winfo_toplevel().queue)
|
||||
@@ -1,87 +1,28 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from pbp_app_config import Msg
|
||||
from pyimage_ui.comment_editor_dialog import CommentEditorDialog
|
||||
|
||||
from shared_libs.message import MessageDialog
|
||||
|
||||
class UserBackupContentFrame(ttk.Frame):
|
||||
def __init__(self, master, backup_manager, actions, **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):
|
||||
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.user_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", "time", "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(
|
||||
"time", text=Msg.STR["time"])
|
||||
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 = 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("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("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.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()
|
||||
self.backup_path = backup_path
|
||||
self._load_backup_content()
|
||||
|
||||
def _load_backup_content(self):
|
||||
for i in self.content_tree.get_children():
|
||||
@@ -133,17 +54,11 @@ class UserBackupContentFrame(ttk.Frame):
|
||||
backup_info.get("comment", ""),
|
||||
backup_info.get("folder_name", "N/A")
|
||||
), 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):
|
||||
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")
|
||||
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()
|
||||
@@ -156,109 +71,37 @@ class UserBackupContentFrame(ttk.Frame):
|
||||
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
|
||||
self._load_backup_content()
|
||||
|
||||
def _restore_selected(self):
|
||||
# This functionality needs to be implemented based on app requirements
|
||||
pass
|
||||
selected_item_id = self.content_tree.focus()
|
||||
if not selected_item_id:
|
||||
return
|
||||
|
||||
MessageDialog(master=self, message_type="info", title="Info", text="User restore not implemented yet.")
|
||||
|
||||
def _delete_selected(self):
|
||||
# This functionality needs to be implemented based on app requirements
|
||||
pass
|
||||
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):
|
||||
selected_item_id = self.content_tree.focus()
|
||||
if not selected_item_id:
|
||||
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",
|
||||
title=Msg.STR["confirm_delete_title"],
|
||||
text=Msg.STR["confirm_delete_text"].format(
|
||||
folder_name=folder_name),
|
||||
folder_name=selected_item_id),
|
||||
buttons=["ok_cancel"])
|
||||
if dialog.get_result() != "ok":
|
||||
return
|
||||
|
||||
try:
|
||||
import shutil
|
||||
if os.path.isdir(folder_to_delete):
|
||||
shutil.rmtree(folder_to_delete)
|
||||
if os.path.exists(info_file_to_delete):
|
||||
os.remove(info_file_to_delete)
|
||||
self._load_backup_content()
|
||||
except Exception as e:
|
||||
MessageDialog(master=self, message_type="error",
|
||||
title=Msg.STR["error"], text=str(e))
|
||||
|
||||
self._load_backup_content()
|
||||
|
||||
Reference in New Issue
Block a user