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:
@@ -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:
|
||||
|
29
main_app.py
29
main_app.py
@@ -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.
|
||||
|
@@ -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()
|
||||
|
||||
|
Reference in New Issue
Block a user