feat: Verbesserung der Backup-Größenberechnung und des UI-Feedbacks
- 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.
This commit is contained in:
@@ -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."""
|
||||
|
||||
25
main_app.py
25
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()
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user