Files
Py-Backup/core/data_processing.py
Désiré Werner Menrath 94afeb5d45 fix: Correct incremental size calculations and rsync handling
This commit refactors the backup size calculation logic and fixes several bugs related to rsync argument passing and UI actions.

- refactor(core): Centralized all incremental backup size calculation logic within the BackupManager class. This removes duplicated and buggy code from the DataProcessing class and ensures both post-backup reporting and pre-backup estimation use a single, robust implementation.

- fix(backup): The pre-backup "accurate size calculation" now works correctly for all backup types. It uses an `rsync --dry-run` with a proper temporary directory and correctly handles root permissions for system backups.

- fix(backup): The post-backup size reporting for incremental system backups is now accurate. It uses a root-privileged command to inspect file links and calculate the true disk usage, avoiding permission errors.

- fix(backup): Corrected multiple quoting issues with rsync arguments (`--link-dest`, `--exclude-from`) that caused backups to fail or misbehave.

- fix(ui): Fixed a TypeError that occurred when deleting a system backup from the "Backup Content" view.

- feat(backup): Added the `-v` (verbose) flag to rsync for user backups to provide better feedback in the UI.
2025-09-09 13:07:56 +02:00

117 lines
4.5 KiB
Python

# pyimage/core/data_processing.py
import os
import fnmatch
import shutil
import re
import subprocess
from queue import Empty
from core.pbp_app_config import AppConfig, Msg
from shared_libs.logger import app_logger
class DataProcessing:
def __init__(self, app):
self.app = app
def load_exclude_patterns(self):
all_patterns = set()
try:
if AppConfig.GENERATED_EXCLUDE_LIST_PATH.exists():
with open(AppConfig.GENERATED_EXCLUDE_LIST_PATH, 'r') as f:
generated_patterns = [
line.strip() for line in f if line.strip() and not line.startswith('#')]
all_patterns.update(generated_patterns)
app_logger.log(
f"Loaded generated exclusion patterns: {generated_patterns}")
except FileNotFoundError:
app_logger.log(
f"Generated exclusion list not found: {AppConfig.GENERATED_EXCLUDE_LIST_PATH}")
except IOError as e:
app_logger.log(f"Error loading generated exclusion list: {e}")
try:
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'r') as f:
user_patterns = [
line.strip() for line in f if line.strip() and not line.startswith('#')]
all_patterns.update(user_patterns)
app_logger.log(
f"Loaded user-defined exclusion patterns: {user_patterns}")
except FileNotFoundError:
app_logger.log(
f"User-defined exclusion list not found: {AppConfig.USER_EXCLUDE_LIST_PATH}")
except IOError as e:
app_logger.log(f"Error loading user-defined exclusion list: {e}")
try:
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
with open(AppConfig.MANUAL_EXCLUDE_LIST_PATH, 'r') as f:
manual_patterns = [
line.strip() for line in f if line.strip() and not line.startswith('#')]
all_patterns.update(manual_patterns)
app_logger.log(
f"Loaded manual exclusion patterns: {manual_patterns}")
except FileNotFoundError:
app_logger.log(
f"Manual exclusion list not found: {AppConfig.MANUAL_EXCLUDE_LIST_PATH}")
except IOError as e:
app_logger.log(f"Error loading manual exclusion list: {e}")
final_patterns = sorted(list(all_patterns))
app_logger.log(f"Combined exclusion patterns: {final_patterns}")
return final_patterns
def get_folder_size_threaded(self, path, button_text, stop_event, exclude_patterns=None, mode='backup'):
total_size = 0
if exclude_patterns is None:
exclude_patterns = []
if exclude_patterns:
exclude_regex = re.compile(
'|'.join(fnmatch.translate(p) for p in exclude_patterns))
else:
exclude_regex = None
for dirpath, dirnames, filenames in os.walk(path, topdown=True):
if stop_event.is_set():
return
if exclude_regex:
dirnames[:] = [d for d in dirnames if not exclude_regex.match(
os.path.join(dirpath, d))]
for f in filenames:
if stop_event.is_set():
return
fp = os.path.join(dirpath, f)
if not exclude_regex or not exclude_regex.match(fp):
if not os.path.islink(fp):
try:
total_size += os.path.getsize(fp)
except OSError:
pass
if not stop_event.is_set():
self.app.queue.put((button_text, total_size, mode))
def get_user_folder_size_threaded(self, path, button_text, stop_event, mode='backup'):
"""Calculates folder size without applying any exclusion lists."""
total_size = 0
for dirpath, dirnames, filenames in os.walk(path, topdown=True):
if stop_event.is_set():
return
for f in filenames:
if stop_event.is_set():
return
fp = os.path.join(dirpath, f)
if not os.path.islink(fp):
try:
total_size += os.path.getsize(fp)
except OSError:
pass
if not stop_event.is_set():
self.app.queue.put((button_text, total_size, mode))