From 9a7470f017e9aed364eaa12f1c706fe40c506565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A9sir=C3=A9=20Werner=20Menrath?= Date: Wed, 3 Sep 2025 17:45:31 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Verbesserung=20der=20Backup-Gr=C3=B6?= =?UTF-8?q?=C3=9Fenberechnung=20und=20des=20UI-Feedbacks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactor '_execute_rsync', um die 'Gesamtgröße' aus der rsync-Ausgabe zu parsen und zurückzugeben, was eine genauere Größe für vollständige Backups liefert. - Verbessertes Logging im Backup-Manager, um die Ausführung von rsync und die Logik zur Größenberechnung besser zu verfolgen. - Implementierung eines live aktualisierenden Dauer-Timers auf der Haupt-UI, der die verstrichene Zeit während eines Backup-Vorgangs anzeigt. - Die Zusammenfassung am Ende des Backups verwendet nun die genaueren Größeninformationen. --- backup_manager.py | 53 ++++++++++++++++++++++++++++++++----------- main_app.py | 25 +++++++++++--------- pyimage_ui/actions.py | 1 + 3 files changed, 55 insertions(+), 24 deletions(-) diff --git a/backup_manager.py b/backup_manager.py index d3bb7c8..fcb5c71 100644 --- a/backup_manager.py +++ b/backup_manager.py @@ -281,8 +281,10 @@ set -e command.append('--dry-run') command.extend([source_path, dest_path]) + self.logger.log(f"Rsync command: {' '.join(command)}") - transferred_size = self._execute_rsync(queue, command) + transferred_size, total_size = self._execute_rsync(queue, command) + self.logger.log(f"_execute_rsync returned: transferred_size={transferred_size}, total_size={total_size}") if self.process: return_code = self.process.returncode @@ -300,10 +302,18 @@ set -e if status in ['success', 'warning'] and not is_dry_run: info_filename_base = os.path.basename(dest_path) - if latest_backup_path is None: # This was a full backup - final_size = source_size + self.logger.log(f"latest_backup_path: {latest_backup_path}") + self.logger.log(f"source_size (from UI): {source_size}") + + if mode == "full": # If explicitly a full backup + final_size = total_size if total_size > 0 else source_size + self.logger.log(f"Explicit Full backup: final_size set to {final_size} (total_size if >0 else source_size)") + elif latest_backup_path is None: # This was the first backup to this location (implicitly full) + final_size = total_size if total_size > 0 else source_size + self.logger.log(f"Implicit Full backup (first to location): final_size set to {final_size} (total_size if >0 else source_size)") else: # This was an incremental backup final_size = transferred_size + self.logger.log(f"Incremental backup: final_size set to {final_size} (transferred_size)") if is_compressed: self.logger.log(f"Compression requested for {dest_path}") @@ -371,6 +381,7 @@ set -e def _execute_rsync(self, queue, command: List[str]): transferred_size = 0 + total_size = 0 try: try: # Force C locale to ensure rsync output is in English for parsing @@ -382,27 +393,28 @@ set -e self.logger.log( "Error: 'pkexec' or 'rsync' command not found in PATH during Popen call.") queue.put(('error', None)) - return 0 + return 0, 0 except Exception as e: self.logger.log( f"Error starting rsync process with Popen: {e}") queue.put(('error', None)) - return 0 + return 0, 0 if self.process is None: # This check might be redundant if exceptions are caught, but good for safety self.logger.log( "Error: subprocess.Popen returned None for rsync process (after exception handling).") queue.put(('error', None)) - return 0 # Exit early if process didn't start + return 0, 0 # Exit early if process didn't start progress_regex = re.compile(r'\s*(\d+)%\s+') output_lines = [] if self.process.stdout: + full_stdout = [] for line in iter(self.process.stdout.readline, ''): stripped_line = line.strip() - self.logger.log(stripped_line) - output_lines.append(stripped_line) + self.logger.log(f"Rsync stdout line: {stripped_line}") # Log every line + full_stdout.append(stripped_line) match = progress_regex.search(stripped_line) if match: @@ -416,18 +428,23 @@ set -e if self.process.stderr: stderr_output = self.process.stderr.read() if stderr_output: - self.logger.log(f"Rsync Error: {stderr_output.strip()}") - output_lines.extend(stderr_output.strip().split('\n')) + self.logger.log(f"Rsync Stderr: {stderr_output.strip()}") # Log stderr + full_stdout.extend(stderr_output.strip().split('\n')) # Add stderr to output_lines for parsing + + output_lines = full_stdout # Use the collected stdout/stderr for parsing # After process completion, parse the output for transferred size. # This is tricky because the output format can vary. We'll try to find the # summary line from --info=progress2, which looks like "sent X bytes received Y bytes". transferred_size = 0 + total_size = 0 summary_regex = re.compile(r"sent ([\d,.]+) bytes\s+received ([\d,.]+) bytes") + total_size_regex = re.compile(r"total size is ([\d,.]+) speedup") + for line in reversed(output_lines): # Search from the end, as summary is usually last match = summary_regex.search(line) - if match: + if match and transferred_size == 0: # Only set if not already found try: sent_str = match.group(1).replace(',', '').replace('.', '') received_str = match.group(2).replace(',', '').replace('.', '') @@ -436,10 +453,20 @@ set -e transferred_size = bytes_sent + bytes_received self.logger.log( f"Detected total bytes transferred from summary: {transferred_size} bytes") - break # Found it except (ValueError, IndexError) as e: self.logger.log( f"Could not parse sent/received bytes from line: '{line}'. Error: {e}") + + total_match = total_size_regex.search(line) + if total_match and total_size == 0: # Only set if not already found + try: + total_size_str = total_match.group(1).replace(',', '').replace('.', '') + total_size = int(total_size_str) + self.logger.log(f"Detected total size from summary: {total_size} bytes") + except(ValueError, IndexError) as e: + self.logger.log(f"Could not parse total size from line: '{line}'. Error: {e}") + + self.logger.log(f"_execute_rsync final parsed values: transferred_size={transferred_size}, total_size={total_size}") if transferred_size == 0: # Fallback for --stats format if the regex fails @@ -475,7 +502,7 @@ set -e self.logger.log(f"An unexpected error occurred: {e}") queue.put(('error', None)) - return transferred_size + return transferred_size, total_size def start_restore(self, source_path: str, dest_path: str, is_compressed: bool): """Starts a restore process in a separate thread.""" diff --git a/main_app.py b/main_app.py index cddc21c..0ab4484 100644 --- a/main_app.py +++ b/main_app.py @@ -425,14 +425,14 @@ class MainApplication(tk.Tk): self.time_info_frame, text="Start: --:--:--") self.start_time_label.pack(side=tk.LEFT, padx=5) - self.end_time_label = ttk.Label( - self.time_info_frame, text="Ende: --:--:--") - self.end_time_label.pack(side=tk.LEFT, padx=5) - self.duration_label = ttk.Label( self.time_info_frame, text="Dauer: --:--:--") self.duration_label.pack(side=tk.LEFT, padx=5) + self.end_time_label = ttk.Label( + 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) @@ -688,15 +688,8 @@ class MainApplication(tk.Tk): if self.start_time: end_time = datetime.datetime.now() - duration = end_time - self.start_time - total_seconds = int(duration.total_seconds()) - hours, remainder = divmod(total_seconds, 3600) - minutes, seconds = divmod(remainder, 60) - duration_str = f"{hours:02}:{minutes:02}:{seconds:02}" end_str = end_time.strftime("%H:%M:%S") self.end_time_label.config(text=f"Ende: {end_str}") - self.duration_label.config( - text=f"Dauer: {duration_str}") self.start_time = None self.backup_is_running = False @@ -714,6 +707,16 @@ class MainApplication(tk.Tk): # Always schedule the next check. self.after(100, self._process_queue) + def _update_duration(self): + if self.backup_is_running and self.start_time: + duration = datetime.datetime.now() - self.start_time + total_seconds = int(duration.total_seconds()) + hours, remainder = divmod(total_seconds, 3600) + minutes, seconds = divmod(remainder, 60) + duration_str = f"{hours:02}:{minutes:02}:{seconds:02}" + self.duration_label.config(text=f"Dauer: {duration_str}") + self.after(1000, self._update_duration) + def quit(self): self.on_closing() diff --git a/pyimage_ui/actions.py b/pyimage_ui/actions.py index ff4a2a8..69c19ef 100644 --- a/pyimage_ui/actions.py +++ b/pyimage_ui/actions.py @@ -614,6 +614,7 @@ class Actions: self.app.end_time_label.config(text="Ende: --:--:--") self.app.duration_label.config(text="Dauer: --:--:--") self.app.info_label.config(text="Backup wird vorbereitet...") + self.app._update_duration() # Start the continuous duration update # --- End Time Logic --- self.app.start_pause_button["text"] = Msg.STR["cancel_backup"]