feat: Zeitanzeige & Fix für unbeaufsichtigte Backups

- Behebt das Problem, bei dem für die Metadaten-Datei eine zweite pkexec-Passwortabfrage nach einem langen Backup-Lauf erforderlich war. Dies ermöglicht nun unbeaufsichtigte/automatisierte Backups.
- Fügt eine neue UI-Anzeige für Startzeit, Endzeit und Dauer des Backups hinzu.
- Das Info-Label zeigt nun den prozentualen Fortschritt während des Backups an.
This commit is contained in:
2025-08-30 22:32:52 +02:00
parent 269da49a1a
commit 8132c5cef9
3 changed files with 112 additions and 25 deletions

View File

@@ -140,6 +140,29 @@ class BackupManager:
thread.daemon = True
thread.start()
def _find_latest_backup(self, base_backup_path: str) -> Optional[str]:
"""Finds the most recent backup directory in a given path."""
self.logger.log(f"Searching for latest backup in: {base_backup_path}")
# We need to find the most recent backup directory.
# The existing list_backups function returns a sorted list of directory names.
backup_names = self.list_backups(base_backup_path)
if not backup_names:
self.logger.log("No previous backups found to link against.")
return None
# The list is sorted descending, so the first one is the latest.
latest_backup_name = backup_names[0]
latest_backup_path = os.path.join(base_backup_path, latest_backup_name)
if os.path.isdir(latest_backup_path):
self.logger.log(f"Found latest backup for --link-dest: {latest_backup_path}")
return latest_backup_path
self.logger.log(f"Latest backup entry '{latest_backup_name}' was not a directory. No link will be used.")
return None
def _run_backup_path(self, queue, source_path: str, dest_path: str, is_system: bool, is_dry_run: bool, exclude_files: Optional[List[Path]], source_size: int):
try:
self.is_system_process = is_system
@@ -150,8 +173,33 @@ class BackupManager:
source_path += '/'
parent_dest = os.path.dirname(dest_path)
if not os.path.exists(parent_dest):
os.makedirs(parent_dest, exist_ok=True)
# For system backups, create the destination dir as root and give the user ownership.
# This allows the subsequent info file to be written without a second pkexec call.
if is_system and not is_dry_run:
user = os.getenv("USER")
if user:
self.logger.log(
f"Preparing destination directory '{dest_path}' for user '{user}'.")
script_content = f'mkdir -p "{dest_path}" && chown {user} "{dest_path}"'
if not self._execute_as_root(script_content):
self.logger.log(
f"FATAL: Failed to create and set permissions on destination directory. Aborting backup.")
queue.put(('error', "Failed to prepare destination."))
return
else:
self.logger.log(
"FATAL: Could not determine current user ($USER). Aborting backup.")
queue.put(('error', "Could not determine user."))
return
else:
# For non-system backups, just ensure the parent directory exists.
if not os.path.exists(parent_dest):
os.makedirs(parent_dest, exist_ok=True)
# --- Incremental Backup Logic ---
latest_backup_path = self._find_latest_backup(parent_dest)
# --- End Incremental Logic ---
command = []
if is_system:
@@ -159,6 +207,11 @@ class BackupManager:
else:
command.extend(['rsync', '-av'])
# Add link-dest if a previous backup was found
if latest_backup_path and not is_dry_run: # Don't use link-dest on dry runs to get a full picture
self.logger.log(f"Using --link-dest='{latest_backup_path}'")
command.append(f"--link-dest={latest_backup_path}")
command.extend(['--info=progress2'])
if exclude_files:
@@ -208,30 +261,25 @@ class BackupManager:
else:
size_str = "0 B"
date_str = datetime.datetime.now().strftime("%d. %B %Y, %H:%M:%S")
# Use pkexec to write the info file
info_content = f"Backup-Datum: {date_str}\nOriginalgröße: {size_str} ({original_bytes} Bytes)\n"
command = ['pkexec', 'sh', '-c',
f'echo -e "{info_content}" > "{info_file_path}"']
try:
result = subprocess.run(
command, capture_output=True, text=True, check=False)
if result.returncode == 0:
self.logger.log(
f"Created metadata file at {info_file_path} using pkexec.")
else:
self.logger.log(
f"Failed to create metadata file using pkexec. Return code: {result.returncode}")
self.logger.log(f"pkexec stdout: {result.stdout.strip()}")
self.logger.log(f"pkexec stderr: {result.stderr.strip()}")
except FileNotFoundError:
self.logger.log(
"Error: 'pkexec' or 'sh' command not found when creating metadata file.")
except Exception as e:
self.logger.log(
f"An unexpected error occurred while creating metadata file with pkexec: {e}")
except Exception as e: # This outer try-except catches errors before pkexec call
self.logger.log(f"Failed to prepare metadata file content: {e}")
# Content with real newlines for Python's write()
info_content = (
f"Backup-Datum: {date_str}\n"
f"Originalgröße: {size_str} ({original_bytes} Bytes)\n"
)
# Write the info file as the current user.
# For system backups, the directory ownership should have been set correctly already.
self.logger.log(
f"Attempting to write info file to {info_file_path} as current user.")
with open(info_file_path, 'w') as f:
f.write(info_content)
self.logger.log(
f"Successfully created metadata file: {info_file_path}")
except Exception as e:
self.logger.log(
f"Failed to create metadata file. Please check permissions for {dest_path}. Error: {e}")
def _execute_rsync(self, queue, command: List[str]):
try:

View File

@@ -2,6 +2,7 @@
import tkinter as tk
from tkinter import ttk
import os
import datetime
from queue import Queue, Empty
from shared_libs.log_window import LogWindow
@@ -80,6 +81,7 @@ class MainApplication(tk.Tk):
self.actions = Actions(self)
self.backup_is_running = False
self.start_time = None
self.calculation_thread = None
self.calculation_stop_event = None
@@ -406,6 +408,19 @@ 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)
self.start_time_label = ttk.Label(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)
checkbox_frame = ttk.Frame(self.info_checkbox_frame)
checkbox_frame.pack(fill=tk.X, pady=5)
self.vollbackup_var = tk.BooleanVar()
@@ -526,6 +541,7 @@ class MainApplication(tk.Tk):
if message_type == 'progress':
self.task_progress["value"] = value
self.info_label.config(text=f"Fortschritt: {value}%") # Update progress text
elif message_type == 'file_update':
# Truncate long text to avoid window resizing issues
max_len = 120
@@ -541,6 +557,19 @@ class MainApplication(tk.Tk):
self.start_pause_button["text"] = "Start"
self.task_progress["value"] = 0
self.current_file_label.config(text="Backup finished.")
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
else:
# This message is not for us (likely for DataProcessing), put it back and yield.

View File

@@ -418,6 +418,16 @@ class Actions:
return
self.app.backup_is_running = True
# --- Record and Display Start Time ---
self.app.start_time = datetime.datetime.now()
start_str = self.app.start_time.strftime("%H:%M:%S")
self.app.start_time_label.config(text=f"Start: {start_str}")
self.app.end_time_label.config(text="Ende: --:--:--")
self.app.duration_label.config(text="Dauer: --:--:--")
self.app.info_label.config(text="Backup wird vorbereitet...")
# --- End Time Logic ---
self.app.start_pause_button["text"] = Msg.STR["cancel_backup"]
self.app.update_idletasks()