diff --git a/backup_manager.py b/backup_manager.py index 9789ce1..5cb9b70 100644 --- a/backup_manager.py +++ b/backup_manager.py @@ -326,7 +326,7 @@ set -e size_str = f"{display_size:.2f} {power_labels[n]}" else: size_str = "0 B" - date_str = datetime.datetime.now().strftime("%d. %B %Y, %H:%M:%S") + date_str = datetime.datetime.now().strftime("%d-%m-%Y %H:%M:%S") info_content = ( f"Backup-Datum: {date_str}\n" @@ -590,11 +590,47 @@ set -e 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 name_regex = re.compile( - r"^(\d{1,2}-\w+-\d{4})_(\d{6})_system_(full|incremental)(\.tar\.gz|\.luks)?$", 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): - if item.endswith('.txt'): + if item.endswith('.txt') or item.endswith('.luks'): continue match = name_regex.match(item) @@ -603,16 +639,15 @@ set -e 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) is_compressed = (extension == ".tar.gz") - is_encrypted = (extension == ".luks") + is_encrypted = False # Individual folders are not encrypted in this logic backup_type = backup_type_base if is_compressed: backup_type += " (Compressed)" - elif is_encrypted: - backup_type += " (Encrypted)" backup_size = "N/A" comment = "" @@ -623,11 +658,11 @@ set -e with open(info_file_path, 'r') as f: for line in f: if line.strip().lower().startswith("originalgröße:"): - size_match = re.search(r":\s*(.*?)\s*(" , line) - if size_match: - backup_size = size_match.group(1).strip() + size_part = line.split(":", 1)[1].strip() + if '(' in size_part: + backup_size = size_part.split('(')[0].strip() else: - backup_size = line.split(":")[1].strip() + backup_size = size_part elif line.strip().lower().startswith("kommentar:"): comment = line.split(":", 1)[1].strip() except Exception as e: @@ -636,22 +671,25 @@ set -e system_backups.append({ "date": date_str, + "time": time_str, "type": backup_type, "size": backup_size, "folder_name": item, "full_path": full_path, "comment": comment, "is_compressed": is_compressed, - "is_encrypted": is_encrypted + "is_encrypted": is_encrypted, + "backup_type_base": backup_type_base }) try: + # Sort chronologically, oldest first system_backups.sort(key=lambda x: datetime.datetime.strptime( - x['date'], '%d-%B-%Y'), reverse=True) + f"{x['date']} {x['time']}", '%d-%m-%Y %H:%M:%S'), reverse=False) except ValueError: self.logger.log( - "Could not sort backups by date due to format mismatch.") - system_backups.sort(key=lambda x: x['folder_name'], reverse=True) + "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 @@ -671,18 +709,25 @@ set -e if os.path.exists(info_file_path): backup_size = "N/A" backup_date = "N/A" + backup_time = "N/A" comment = "" try: with open(info_file_path, 'r') as f: for line in f: if line.strip().lower().startswith("originalgröße:"): - size_match = re.search(r":\s*(.*?)\s*(" , line) - if size_match: - backup_size = size_match.group(1).strip() + size_part = line.split(":", 1)[1].strip() + if '(' in size_part: + backup_size = size_part.split('(')[0].strip() else: - backup_size = line.split(":")[1].strip() + backup_size = size_part elif line.strip().lower().startswith("backup-datum:"): - backup_date = line.split(":", 1)[1].strip() + full_date_str = line.split(":", 1)[1].strip() + date_parts = full_date_str.split() + if len(date_parts) >= 2: + backup_date = date_parts[0] + backup_time = date_parts[1] + else: + backup_date = full_date_str elif line.strip().lower().startswith("kommentar:"): comment = line.split(":", 1)[1].strip() except Exception as e: @@ -691,13 +736,15 @@ set -e user_backups.append({ "date": backup_date, + "time": backup_time, "size": backup_size, "folder_name": item, "full_path": full_path, "comment": comment }) - user_backups.sort(key=lambda x: x['folder_name'], reverse=True) + # Sort chronologically, oldest first + user_backups.sort(key=lambda x: f"{x['date']} {x['time']}", reverse=False) return user_backups def get_comment(self, info_file_path: str) -> str: diff --git a/main_app.py b/main_app.py index 5c704a3..60448b9 100644 --- a/main_app.py +++ b/main_app.py @@ -622,9 +622,6 @@ class MainApplication(tk.Tk): self.drawing.update_target_projection() - if self.mode == 'backup' and self.destination_path: - self.start_pause_button.config(state="normal") - if calc_type == 'accurate_incremental': self.source_size_bytes = folder_size self.drawing.update_target_projection() diff --git a/pbp_app_config.py b/pbp_app_config.py index cf33837..168315f 100644 --- a/pbp_app_config.py +++ b/pbp_app_config.py @@ -239,6 +239,7 @@ class Msg: "name": _("Name"), "path": _("Path"), "date": _("Date"), + "time": _("Time"), "size": _("Size"), "type": _("Type"), "folder": _("Folder"), diff --git a/pyimage_ui/actions.py b/pyimage_ui/actions.py index 61bb5b7..7e3e754 100644 --- a/pyimage_ui/actions.py +++ b/pyimage_ui/actions.py @@ -38,7 +38,7 @@ class Actions: system_backups = self.app.backup_manager.list_system_backups( self.app.destination_path) for backup in system_backups: - if backup.get('type') == 'Full': + if backup.get('backup_type_base') == 'Full': full_backup_exists = True break @@ -619,8 +619,8 @@ class Actions: "Could not set locale to de_DE.UTF-8. Using default.") now = datetime.datetime.now() - date_str = now.strftime("%d-%B-%Y") - time_str = now.strftime("%H%M%S") + date_str = now.strftime("%d-%m-%Y") + time_str = now.strftime("%H:%M:%S") folder_name = f"{date_str}_{time_str}_system_{mode}" final_dest = os.path.join(base_dest, "pybackup", folder_name) self.app.current_backup_path = final_dest diff --git a/pyimage_ui/drawing.py b/pyimage_ui/drawing.py index c5387c8..552576a 100644 --- a/pyimage_ui/drawing.py +++ b/pyimage_ui/drawing.py @@ -234,54 +234,60 @@ class Drawing: self.app.after(50, self.update_target_projection) return - projected_total_used = self.app.destination_used_bytes + self.app.source_size_bytes - projected_total_percentage = projected_total_used / \ - self.app.destination_total_bytes + # Determine required space, considering compression + required_space = self.app.source_size_bytes + if self.app.compressed_var.get(): + required_space *= 2 # Double the space for compression process - info_font = (AppConfig.UI_CONFIG["font_family"], 12, "bold") - - if projected_total_percentage >= 0.95: - self.app.start_pause_button.config(state="disabled") - canvas.create_rectangle(0, 0, canvas_width, canvas.winfo_height( - ), fill="#ff0000", outline="") # Red bar - elif projected_total_percentage >= 0.90: - self.app.start_pause_button.config(state="normal") - canvas.create_rectangle(0, 0, canvas_width, canvas.winfo_height( - ), fill="#ff8c00", outline="") # Orange bar + projected_total_used = self.app.destination_used_bytes + required_space + + if self.app.destination_total_bytes > 0: + projected_total_percentage = projected_total_used / self.app.destination_total_bytes else: - self.app.start_pause_button.config(state="normal") - used_percentage = self.app.destination_used_bytes / \ - self.app.destination_total_bytes - used_width = canvas_width * used_percentage - canvas.create_rectangle( - 0, 0, used_width, canvas.winfo_height(), fill="#0078d7", outline="") - - projected_percentage = self.app.source_size_bytes / \ - self.app.destination_total_bytes - projected_width = canvas_width * projected_percentage - canvas.create_rectangle(used_width, 0, used_width + projected_width, - canvas.winfo_height(), fill="#ff8c00", outline="") + projected_total_percentage = 0 + info_font = (AppConfig.UI_CONFIG["font_family"], 10, "bold") info_messages = [] - if self.app.source_larger_than_partition: - info_messages.append( - Msg.STR["warning_source_larger_than_partition"]) - if projected_total_percentage >= 0.95: + # First, check for critical space issues + if projected_total_used > self.app.destination_total_bytes or projected_total_percentage >= 0.95: + self.app.start_pause_button.config(state="disabled") + canvas.create_rectangle(0, 0, canvas_width, canvas.winfo_height(), fill="#D32F2F", outline="") # Red bar info_messages.append(Msg.STR["warning_not_enough_space"]) + elif projected_total_percentage >= 0.90: + self.app.start_pause_button.config(state="normal") + canvas.create_rectangle(0, 0, canvas_width, canvas.winfo_height(), fill="#E8740C", outline="") # Orange bar info_messages.append(Msg.STR["warning_space_over_90_percent"]) + + else: + # Only enable the button if the source is not larger than the partition itself + if not self.app.source_larger_than_partition: + self.app.start_pause_button.config(state="normal") + else: + self.app.start_pause_button.config(state="disabled") - if not info_messages: # If no warnings, show other messages or default text - if self.app.is_first_backup: + used_percentage = self.app.destination_used_bytes / self.app.destination_total_bytes + used_width = canvas_width * used_percentage + canvas.create_rectangle(0, 0, used_width, canvas.winfo_height(), fill="#0078d7", outline="") + + # Draw the projected part only if there is space + projected_percentage = self.app.source_size_bytes / self.app.destination_total_bytes + projected_width = canvas_width * projected_percentage + canvas.create_rectangle(used_width, 0, used_width + projected_width, canvas.winfo_height(), fill="#50E6FF", outline="") + + # Add other informational messages if no critical warnings are present + if not info_messages: + if self.app.source_larger_than_partition: + info_messages.append(Msg.STR["warning_source_larger_than_partition"]) + elif self.app.is_first_backup: info_messages.append(Msg.STR["ready_for_first_backup"]) elif self.app.mode == "backup": info_messages.append(Msg.STR["backup_mode_info"]) else: info_messages.append(Msg.STR["restore_mode_info"]) - self.app.info_label.config( - text="\n".join(info_messages), font=info_font) + self.app.info_label.config(text="\n".join(info_messages), font=info_font) self.app.target_size_label.config( text=f"{projected_total_used / (1024**3):.2f} GB / {self.app.destination_total_bytes / (1024**3):.2f} GB") diff --git a/pyimage_ui/system_backup_content_frame.py b/pyimage_ui/system_backup_content_frame.py index 12cd7fe..7b29f2b 100644 --- a/pyimage_ui/system_backup_content_frame.py +++ b/pyimage_ui/system_backup_content_frame.py @@ -15,30 +15,38 @@ class SystemBackupContentFrame(ttk.Frame): self.backup_path = None + # --- Color Tags --- + self.tag_colors = [ + ("full_blue", "#0078D7", "inc_blue", "#50E6FF"), + ("full_orange", "#E8740C", "inc_orange", "#FFB366"), + ("full_green", "#107C10", "inc_green", "#50E680"), + ("full_purple", "#8B107C", "inc_purple", "#D46EE5"), + ] + # --- Backup Content List View --- self.content_frame = ttk.LabelFrame( self, text=Msg.STR["backup_content"], padding=10) self.content_frame.pack(fill=tk.BOTH, expand=True) - columns = ("date", "type", "size", "comment", "folder_name") + 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.heading( - "folder_name", text=Msg.STR["folder"]) - self.content_tree.column("date", width=120, anchor="w") - self.content_tree.column("type", width=80, anchor="center") + self.content_tree.column("date", width=100, anchor="w") + self.content_tree.column("time", width=80, anchor="center") + self.content_tree.column("type", width=120, anchor="center") self.content_tree.column("size", width=100, anchor="e") - self.content_tree.column("comment", width=200, anchor="w") - self.content_tree.column("folder_name", width=250, anchor="w") + self.content_tree.column("comment", width=300, anchor="w") self.content_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) @@ -74,18 +82,32 @@ class SystemBackupContentFrame(ttk.Frame): if not self.backup_path or not os.path.isdir(self.backup_path): return - # Use the new method to get structured system backup data - system_backups = self.backup_manager.list_system_backups( - self.backup_path) + system_backups = self.backup_manager.list_system_backups(self.backup_path) + self.system_backups_list = system_backups # Store for later use + + color_index = 0 + for i, backup_info in enumerate(system_backups): + # Determine the color tag for the group + 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 - for backup_info in system_backups: self.content_tree.insert("", "end", values=( backup_info.get("date", "N/A"), + backup_info.get("time", "N/A"), backup_info.get("type", "N/A"), backup_info.get("size", "N/A"), backup_info.get("comment", ""), - backup_info.get("folder_name", "N/A") - )) + ), tags=(current_tag,), iid=backup_info.get("folder_name")) + self._on_item_select(None) # Disable buttons initially def _on_item_select(self, event): @@ -99,56 +121,42 @@ class SystemBackupContentFrame(ttk.Frame): state="normal" if is_selected else "disabled") def _edit_comment(self): - selected_item = self.content_tree.focus() - if not selected_item: + selected_item_id = self.content_tree.focus() + if not selected_item_id: return - item_values = self.content_tree.item(selected_item)["values"] - folder_name = item_values[4] # Assuming folder_name is the 5th value - # Construct the path to the info file pybackup_path = os.path.join(self.backup_path, "pybackup") - info_file_path = os.path.join(pybackup_path, f"{folder_name}.txt") + info_file_path = os.path.join(pybackup_path, f"{selected_item_id}.txt") - # The file should exist, but we can handle cases where it might not. if not os.path.exists(info_file_path): - # If for some reason the info file is missing, we can create an empty one. self.backup_manager.update_comment(info_file_path, "") CommentEditorDialog(self, info_file_path, self.backup_manager) self._load_backup_content() # Refresh list to show new comment def _restore_selected(self): - selected_item = self.content_tree.focus() - if not selected_item: + selected_item_id = self.content_tree.focus() + if not selected_item_id: return - item_values = self.content_tree.item(selected_item)["values"] - folder_name = item_values[4] # 5th column is folder_name - selected_backup = None for backup in self.system_backups_list: - if backup.get("folder_name") == folder_name: + if backup.get("folder_name") == selected_item_id: selected_backup = backup break if not selected_backup: - print(f"Error: Could not find backup info for {folder_name}") + print(f"Error: Could not find backup info for {selected_item_id}") return - # We need to get the restore destination from the main app - # This is a bit tricky as this frame is isolated. - # We assume the main app has a way to provide this. - # Let's get it from the config manager, which should be accessible via the backup_manager's app instance. try: - # Accessing the app instance through the master hierarchy main_app = self.winfo_toplevel() restore_dest_path = main_app.config_manager.get_setting( "restore_destination_path", "/") if not restore_dest_path: print("Error: Restore destination not set.") - # Optionally, show a message box to the user return self.backup_manager.start_restore( @@ -160,16 +168,13 @@ class SystemBackupContentFrame(ttk.Frame): print("Could not access main application instance to get restore path.") def _delete_selected(self): - selected_item = self.content_tree.focus() - if not selected_item: + selected_item_id = self.content_tree.focus() + if not selected_item_id: return - item_values = self.content_tree.item(selected_item)["values"] - folder_name = item_values[4] # Assuming folder_name is the 5th value - # Construct the full path to the backup folder pybackup_path = os.path.join(self.backup_path, "pybackup") - folder_to_delete = os.path.join(pybackup_path, folder_name) + folder_to_delete = os.path.join(pybackup_path, selected_item_id) # Lock UI and show status self.actions._set_ui_state(False) # Lock UI diff --git a/pyimage_ui/user_backup_content_frame.py b/pyimage_ui/user_backup_content_frame.py index bc821c9..c80bb9c 100644 --- a/pyimage_ui/user_backup_content_frame.py +++ b/pyimage_ui/user_backup_content_frame.py @@ -47,6 +47,63 @@ class UserBackupContentFrame(ttk.Frame): 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) + self.backup_manager = backup_manager + self.actions = actions + 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.column("date", width=100, anchor="w") + self.content_tree.column("time", width=80, anchor="center") + self.content_tree.column("size", width=100, anchor="e") + self.content_tree.column("comment", width=250, 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.bind("<>", 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) @@ -59,6 +116,65 @@ class UserBackupContentFrame(ttk.Frame): 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 + + self.user_backups_list = self.backup_manager.list_user_backups(self.backup_path) + + for backup_info in self.user_backups_list: + self.content_tree.insert("", "end", values=( + backup_info.get("date", "N/A"), + backup_info.get("time", "N/A"), + backup_info.get("size", "N/A"), + 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 + + 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_id = self.content_tree.focus() + if not selected_item_id: + return + + 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 + + def _restore_selected(self): + # This functionality needs to be implemented based on app requirements + pass + + 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)