refactor: Metadaten-Speicherung zur Behebung von Berechtigungsproblemen

- Ändert die Logik zum Erstellen und Auflisten von Backups, um Metadaten-Dateien (z.B. 'backup_name.txt') im übergeordneten Verzeichnis des Backups statt darin zu speichern.
- Dies behebt den 'Permission denied'-Fehler beim Schreiben von Metadaten für System-Backups, die root gehören, da das übergeordnete Verzeichnis für den Benutzer beschreibbar ist.
- Ermöglicht vollständig unbeaufsichtigte/automatisierte Backups ohne eine zweite Passwortabfrage.
This commit is contained in:
2025-08-31 01:12:10 +02:00
parent 8132c5cef9
commit 9e88ac8bb5

View File

@@ -144,15 +144,12 @@ class BackupManager:
"""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)
@@ -173,33 +170,12 @@ class BackupManager:
source_path += '/'
parent_dest = os.path.dirname(dest_path)
# Ensure the parent directory exists. For system backups, rsync with pkexec will create the final destination.
# For user backups, this creates the destination.
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:
@@ -207,8 +183,7 @@ 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
if latest_backup_path and not is_dry_run:
self.logger.log(f"Using --link-dest='{latest_backup_path}'")
command.append(f"--link-dest={latest_backup_path}")
@@ -229,9 +204,11 @@ class BackupManager:
self.logger.log(
f"Rsync process finished with return code: {self.process.returncode}")
if self.process.returncode == 0 and not is_dry_run:
info_filename = "pbp-info.txt" if is_system else "pbp-user-info.txt"
# For user backups, the info file is named after the folder.
# For system backups, it's named after the folder inside 'pybackup'.
info_filename_base = os.path.basename(dest_path)
self._create_info_file(
dest_path, info_filename, source_size)
dest_path, f"{info_filename_base}.txt", source_size)
else:
self.logger.log(
"Info file not created due to non-zero return code or dry run.")
@@ -247,7 +224,10 @@ class BackupManager:
def _create_info_file(self, dest_path: str, filename: str, source_size: int):
try:
info_file_path = os.path.join(dest_path, filename)
# Info file is now stored in the parent directory of the backup folder.
parent_dir = os.path.dirname(dest_path)
info_file_path = os.path.join(parent_dir, filename)
original_bytes = source_size
if source_size > 0:
power = 1024
@@ -262,14 +242,11 @@ class BackupManager:
size_str = "0 B"
date_str = datetime.datetime.now().strftime("%d. %B %Y, %H:%M:%S")
# 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:
@@ -279,7 +256,7 @@ class BackupManager:
except Exception as e:
self.logger.log(
f"Failed to create metadata file. Please check permissions for {dest_path}. Error: {e}")
f"Failed to create metadata file. Please check permissions for {os.path.dirname(info_file_path)}. Error: {e}")
def _execute_rsync(self, queue, command: List[str]):
try:
@@ -413,7 +390,6 @@ class BackupManager:
if not os.path.isdir(pybackup_path):
return system_backups
# Regex to parse folder names like '6-März-2024_system_full'
name_regex = re.compile(
r"^(\d{1,2}-\w+-\d{4})_system_(full|incremental)$", re.IGNORECASE)
@@ -430,18 +406,17 @@ class BackupManager:
backup_type = match.group(2).capitalize()
backup_size = "N/A"
# Try to read the metadata file for the size
info_file_path = os.path.join(full_path, "pbp-info.txt")
# NEW: Look for info file in the parent directory, named after the backup folder
info_file_path = os.path.join(pybackup_path, f"{item}.txt")
if os.path.exists(info_file_path):
try:
with open(info_file_path, 'r') as f:
for line in f:
if line.strip().lower().startswith("originalgröße:"):
# Extract size, e.g., "Originalgröße: 13.45 GB (...)"
size_match = re.search(r":\s*(.*)\s*\(", line)
size_match = re.search(r":\s*(.*)\s*(", line)
if size_match:
backup_size = size_match.group(1).strip()
else: # Fallback if format is just "Originalgröße: 13.45 GB"
else:
backup_size = line.split(":")[1].strip()
break
except Exception as e:
@@ -456,14 +431,12 @@ class BackupManager:
"full_path": full_path
})
# Sort by parsing the date from the folder name
try:
system_backups.sort(key=lambda x: datetime.datetime.strptime(
x['date'], '%d-%B-%Y'), reverse=True)
except ValueError:
self.logger.log(
"Could not sort backups by date due to format mismatch.")
# Fallback to simple string sort if date parsing fails
system_backups.sort(key=lambda x: x['folder_name'], reverse=True)
return system_backups
@@ -479,7 +452,10 @@ class BackupManager:
if not os.path.isdir(full_path):
continue
info_file_path = os.path.join(full_path, "pbp-user-info.txt")
# NEW: Look for info file in the parent directory, named after the backup folder
info_file_path = os.path.join(base_backup_path, f"{item}.txt")
# We identify a user backup by the presence of its corresponding info file.
if os.path.exists(info_file_path):
backup_size = "N/A"
backup_date = "N/A"
@@ -487,7 +463,7 @@ class BackupManager:
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)
size_match = re.search(r":\s*(.*)\s*(", line)
if size_match:
backup_size = size_match.group(1).strip()
else: