feat: Verbesserung der Backup-Typ-Auswahl und Korrektur der Füllstandsanzeige
2
3 Dieses Commit enthält zwei wesentliche Verbesserungen:
4
5 1. **Flexible Auswahl des Backup-Typs:** Der Benutzer kann jetzt manuell die
Erstellung eines vollständigen Backups auswählen, auch wenn bereits ein früheres
vollständiges Backup vorhanden ist. Die Anwendung wechselt in diesem Fall nicht mehr
automatisch zu einem inkrementellen Backup.
6
7 2. **Korrektur der Füllstandsanzeige:** Die Füllstandsanzeige zeigt jetzt die
voraussichtliche Backup-Größe sowohl für vollständige als auch für inkrementelle
Backups korrekt an. Dies wurde erreicht, indem sichergestellt wurde, dass die
Quellgröße in allen Fällen korrekt berechnet und an die Benutzeroberfläche übergeben
wird.
This commit is contained in:
@@ -416,7 +416,7 @@ class BackupManager:
|
||||
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"
|
||||
@@ -471,7 +471,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:
|
||||
|
||||
83
config_manager.py
Normal file
83
config_manager.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from json import JSONEncoder
|
||||
|
||||
|
||||
class PathEncoder(JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, Path):
|
||||
return str(obj)
|
||||
return JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""Manages loading and saving of application settings in a JSON file."""
|
||||
|
||||
def __init__(self, file_path: Path):
|
||||
"""
|
||||
Initializes the ConfigManager.
|
||||
|
||||
Args:
|
||||
file_path: The path to the configuration file.
|
||||
"""
|
||||
self.file_path = file_path
|
||||
self.settings: Dict[str, Any] = {}
|
||||
self.load()
|
||||
|
||||
def load(self) -> None:
|
||||
"""Loads the settings from the JSON file."""
|
||||
if self.file_path.exists():
|
||||
try:
|
||||
with open(self.file_path, 'r', encoding='utf-8') as f:
|
||||
self.settings = json.load(f)
|
||||
except (IOError, json.JSONDecodeError) as e:
|
||||
print(f"Error loading config file {self.file_path}: {e}")
|
||||
self.settings = {}
|
||||
else:
|
||||
self.settings = {}
|
||||
|
||||
def save(self) -> None:
|
||||
"""Saves the current settings to the JSON file."""
|
||||
try:
|
||||
# Ensure the parent directory exists
|
||||
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(self.file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.settings, f, indent=4, cls=PathEncoder)
|
||||
except IOError as e:
|
||||
print(f"Error saving config file {self.file_path}: {e}")
|
||||
|
||||
def get_setting(self, key: str, default: Any = None) -> Any:
|
||||
"""
|
||||
Gets a setting value.
|
||||
|
||||
Args:
|
||||
key: The key of the setting.
|
||||
default: The default value to return if the key is not found.
|
||||
|
||||
Returns:
|
||||
The value of the setting or the default value.
|
||||
"""
|
||||
return self.settings.get(key, default)
|
||||
|
||||
def set_setting(self, key: str, value: Any) -> None:
|
||||
"""
|
||||
Sets a setting value and immediately saves it to the file.
|
||||
|
||||
Args:
|
||||
key: The key of the setting.
|
||||
value: The value to set.
|
||||
"""
|
||||
self.settings[key] = value
|
||||
self.save()
|
||||
|
||||
def remove_setting(self, key: str) -> None:
|
||||
"""
|
||||
Removes a setting from the configuration.
|
||||
|
||||
Args:
|
||||
key: The key of the setting to remove.
|
||||
"""
|
||||
if key in self.settings:
|
||||
del self.settings[key]
|
||||
self.save()
|
||||
@@ -3,7 +3,9 @@ import os
|
||||
import fnmatch
|
||||
import shutil
|
||||
import re
|
||||
from pbp_app_config import AppConfig
|
||||
import subprocess
|
||||
from queue import Empty
|
||||
from pbp_app_config import AppConfig, Msg
|
||||
from shared_libs.logger import app_logger
|
||||
|
||||
|
||||
@@ -101,19 +103,110 @@ class DataProcessing:
|
||||
if not stop_event.is_set():
|
||||
self.app.queue.put((button_text, total_size, mode))
|
||||
|
||||
def get_incremental_backup_size(self, source_path: str, dest_path: str, is_system: bool, exclude_files: list = None) -> int:
|
||||
"""
|
||||
Calculates the approximate size of an incremental backup using rsync's dry-run feature.
|
||||
This is much faster than walking the entire file tree.
|
||||
"""
|
||||
app_logger.log(f"Calculating incremental backup size for source: {source_path}")
|
||||
|
||||
parent_dest = os.path.dirname(dest_path)
|
||||
if not os.path.exists(parent_dest):
|
||||
# If the parent destination doesn't exist, there are no previous backups to link to.
|
||||
# In this case, the incremental size is the full size of the source.
|
||||
# We can use the existing full-size calculation method.
|
||||
# This is a simplified approach for the estimation.
|
||||
# A more accurate one would run rsync without --link-dest.
|
||||
app_logger.log("Destination parent does not exist, cannot calculate incremental size. Returning 0.")
|
||||
return 0
|
||||
|
||||
# Find the latest backup to link against
|
||||
try:
|
||||
backups = sorted([d for d in os.listdir(parent_dest) if os.path.isdir(os.path.join(parent_dest, d))], reverse=True)
|
||||
if not backups:
|
||||
app_logger.log("No previous backups found. Incremental size is full size.")
|
||||
return 0 # Or trigger a full size calculation
|
||||
latest_backup_path = os.path.join(parent_dest, backups[0])
|
||||
except FileNotFoundError:
|
||||
app_logger.log("Could not list backups, assuming no prior backups exist.")
|
||||
return 0
|
||||
|
||||
|
||||
command = []
|
||||
if is_system:
|
||||
command.extend(['pkexec', 'rsync', '-aAXHvn', '--stats'])
|
||||
else:
|
||||
command.extend(['rsync', '-avn', '--stats'])
|
||||
|
||||
command.append(f"--link-dest={latest_backup_path}")
|
||||
|
||||
if exclude_files:
|
||||
for exclude_file in exclude_files:
|
||||
command.append(f"--exclude-from={exclude_file}")
|
||||
|
||||
# The destination for a dry run can be a dummy path, but it must exist.
|
||||
# Let's use a temporary directory.
|
||||
dummy_dest = os.path.join(parent_dest, "dry_run_dest")
|
||||
os.makedirs(dummy_dest, exist_ok=True)
|
||||
|
||||
command.extend([source_path, dummy_dest])
|
||||
|
||||
app_logger.log(f"Executing rsync dry-run command: {' '.join(command)}")
|
||||
|
||||
try:
|
||||
result = subprocess.run(command, capture_output=True, text=True, check=False)
|
||||
|
||||
# Clean up the dummy directory
|
||||
shutil.rmtree(dummy_dest)
|
||||
|
||||
if result.returncode != 0:
|
||||
app_logger.log(f"Rsync dry-run failed with code {result.returncode}: {result.stderr}")
|
||||
return 0
|
||||
|
||||
output = result.stdout + "\n" + result.stderr
|
||||
# The regex now accepts dots as thousands separators (e.g., 1.234.567).
|
||||
match = re.search(r"Total transferred file size: ([\d,.]+) bytes", output)
|
||||
if match:
|
||||
# Remove both dots and commas before converting to an integer.
|
||||
size_str = match.group(1).replace(',', '').replace('.', '')
|
||||
size_bytes = int(size_str)
|
||||
app_logger.log(f"Estimated incremental backup size: {size_bytes} bytes")
|
||||
return size_bytes
|
||||
else:
|
||||
app_logger.log("Could not find 'Total transferred file size' in rsync output.")
|
||||
# Log the output just in case something changes in the future
|
||||
app_logger.log(f"Full rsync output for debugging:\n{output}")
|
||||
return 0
|
||||
|
||||
except FileNotFoundError:
|
||||
app_logger.log("Error: 'rsync' or 'pkexec' command not found.")
|
||||
return 0
|
||||
except Exception as e:
|
||||
app_logger.log(f"An unexpected error occurred during incremental size calculation: {e}")
|
||||
return 0
|
||||
|
||||
def process_queue(self):
|
||||
try:
|
||||
message = self.app.queue.get_nowait()
|
||||
button_text, folder_size, mode_when_started = message
|
||||
|
||||
# Check for the new message format with status
|
||||
calc_type, status = None, None
|
||||
if len(message) == 5:
|
||||
button_text, folder_size, mode_when_started, calc_type, status = message
|
||||
elif len(message) == 3:
|
||||
button_text, folder_size, mode_when_started = message
|
||||
else:
|
||||
return # Ignore malformed messages
|
||||
|
||||
if mode_when_started != self.app.mode:
|
||||
return # Discard stale result from a different mode
|
||||
|
||||
if self.app.mode == 'restore':
|
||||
self.app.restore_destination_folder_size_bytes = folder_size
|
||||
else: # backup mode
|
||||
self.app.source_size_bytes = folder_size
|
||||
if calc_type == 'accurate_incremental':
|
||||
self.app.actions._set_ui_state(True) # Unlock UI
|
||||
self.app.genaue_berechnung_var.set(False) # Uncheck the box
|
||||
self.app.accurate_calculation_running = False
|
||||
self.app.animated_icon.stop("DISABLE")
|
||||
return # Discard stale result
|
||||
|
||||
# --- Update Main Canvas ---
|
||||
current_folder_name = self.app.left_canvas_data.get('folder')
|
||||
if current_folder_name == button_text:
|
||||
if self.app.left_canvas_animation:
|
||||
@@ -121,43 +214,54 @@ class DataProcessing:
|
||||
self.app.left_canvas_animation.destroy()
|
||||
self.app.left_canvas_animation = None
|
||||
|
||||
if not self.app.right_canvas_data.get('calculating', False):
|
||||
self.app.start_pause_button.config(state="normal")
|
||||
|
||||
size_in_gb = folder_size / (1024**3)
|
||||
if size_in_gb >= 1:
|
||||
size_str = f"{size_in_gb:.2f} GB"
|
||||
else:
|
||||
size_str = f"{folder_size / (1024*1024):.2f} MB"
|
||||
size_str = f"{size_in_gb:.2f} GB" if size_in_gb >= 1 else f"{folder_size / (1024*1024):.2f} MB"
|
||||
|
||||
self.app.left_canvas_data['size'] = size_str
|
||||
self.app.left_canvas_data['total_bytes'] = folder_size
|
||||
self.app.left_canvas_data['calculating'] = False
|
||||
self.app.drawing.redraw_left_canvas()
|
||||
|
||||
self.app.source_size_bytes = folder_size
|
||||
# --- Update Bottom Canvases ---
|
||||
if self.app.mode == 'backup':
|
||||
total_disk_size, _, _ = shutil.disk_usage(
|
||||
AppConfig.FOLDER_PATHS[button_text])
|
||||
if folder_size > total_disk_size:
|
||||
self.app.source_larger_than_partition = True
|
||||
else:
|
||||
self.app.source_larger_than_partition = False
|
||||
# Ensure button_text is a valid key in FOLDER_PATHS
|
||||
if button_text in AppConfig.FOLDER_PATHS:
|
||||
total_disk_size, _, _ = shutil.disk_usage(AppConfig.FOLDER_PATHS[button_text])
|
||||
if folder_size > total_disk_size:
|
||||
self.app.source_larger_than_partition = True
|
||||
else:
|
||||
self.app.source_larger_than_partition = False
|
||||
|
||||
if total_disk_size > 0:
|
||||
percentage = (folder_size / total_disk_size) * 100
|
||||
else:
|
||||
percentage = 0
|
||||
|
||||
self.app.source_size_canvas.delete("all")
|
||||
fill_width = (
|
||||
self.app.source_size_canvas.winfo_width() / 100) * percentage
|
||||
self.app.source_size_canvas.create_rectangle(
|
||||
0, 0, fill_width, self.app.source_size_canvas.winfo_height(), fill="#0078d7", outline="")
|
||||
self.app.source_size_label.config(
|
||||
text=f"{folder_size / (1024**3):.2f} GB / {total_disk_size / (1024**3):.2f} GB")
|
||||
percentage = (folder_size / total_disk_size) * 100 if total_disk_size > 0 else 0
|
||||
|
||||
self.app.source_size_canvas.delete("all")
|
||||
fill_width = (self.app.source_size_canvas.winfo_width() / 100) * percentage
|
||||
self.app.source_size_canvas.create_rectangle(0, 0, fill_width, self.app.source_size_canvas.winfo_height(), fill="#0078d7", outline="")
|
||||
self.app.source_size_label.config(text=f"{folder_size / (1024**3):.2f} GB / {total_disk_size / (1024**3):.2f} GB")
|
||||
|
||||
self.app.drawing.update_target_projection()
|
||||
|
||||
except Exception as e:
|
||||
# --- Handle Accurate Calculation Completion ---
|
||||
if calc_type == 'accurate_incremental':
|
||||
self.app.source_size_bytes = folder_size # Update the source size
|
||||
self.app.drawing.update_target_projection() # Redraw the projection
|
||||
|
||||
self.app.animated_icon.stop("DISABLE")
|
||||
self.app.task_progress.stop()
|
||||
self.app.task_progress.config(mode="determinate", value=0)
|
||||
self.app.actions._set_ui_state(True)
|
||||
self.app.genaue_berechnung_var.set(False)
|
||||
self.app.accurate_calculation_running = False
|
||||
self.app.start_pause_button.config(text=Msg.STR["start"])
|
||||
if status == 'success':
|
||||
self.app.info_label.config(text=Msg.STR["accurate_size_success"], foreground="#0078d7")
|
||||
self.app.current_file_label.config(text="")
|
||||
else:
|
||||
self.app.info_label.config(text=Msg.STR["accurate_size_failed"], foreground="#D32F2F") # Red for failed
|
||||
self.app.current_file_label.config(text="")
|
||||
|
||||
except Empty:
|
||||
pass
|
||||
finally:
|
||||
self.app.after(100, self.process_queue)
|
||||
|
||||
37
main_app.py
37
main_app.py
@@ -86,6 +86,7 @@ class MainApplication(tk.Tk):
|
||||
self.calculation_thread = None
|
||||
self.calculation_stop_event = None
|
||||
self.source_larger_than_partition = False
|
||||
self.accurate_calculation_running = False
|
||||
self.is_first_backup = False
|
||||
|
||||
self.left_canvas_animation = None
|
||||
@@ -401,6 +402,15 @@ class MainApplication(tk.Tk):
|
||||
self.backup_content_frame.grid_remove()
|
||||
|
||||
def _setup_task_bar(self):
|
||||
# Define all boolean vars at the top to ensure they exist before use.
|
||||
self.vollbackup_var = tk.BooleanVar()
|
||||
self.inkrementell_var = tk.BooleanVar()
|
||||
self.genaue_berechnung_var = tk.BooleanVar()
|
||||
self.testlauf_var = tk.BooleanVar()
|
||||
self.compressed_var = tk.BooleanVar()
|
||||
self.encrypted_var = tk.BooleanVar()
|
||||
self.bypass_security_var = tk.BooleanVar()
|
||||
|
||||
self.info_checkbox_frame = ttk.Frame(self.content_frame, padding=10)
|
||||
self.info_checkbox_frame.grid(row=3, column=0, sticky="ew")
|
||||
|
||||
@@ -421,20 +431,25 @@ class MainApplication(tk.Tk):
|
||||
self.duration_label = ttk.Label(self.time_info_frame, text="Dauer: --:--:--")
|
||||
self.duration_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)
|
||||
|
||||
self.accurate_size_cb = ttk.Checkbutton(accurate_size_frame, text=Msg.STR["accurate_size_cb_label"],
|
||||
variable=self.genaue_berechnung_var, command=self.actions.on_toggle_accurate_size_calc)
|
||||
self.accurate_size_cb.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
accurate_size_info_label = ttk.Label(accurate_size_frame, text=Msg.STR["accurate_size_info_label"], foreground="gray")
|
||||
accurate_size_info_label.pack(side=tk.LEFT)
|
||||
|
||||
checkbox_frame = ttk.Frame(self.info_checkbox_frame)
|
||||
checkbox_frame.pack(fill=tk.X, pady=5)
|
||||
self.vollbackup_var = tk.BooleanVar()
|
||||
self.inkrementell_var = tk.BooleanVar()
|
||||
self.testlauf_var = tk.BooleanVar()
|
||||
self.compressed_var = tk.BooleanVar()
|
||||
self.encrypted_var = tk.BooleanVar()
|
||||
self.bypass_security_var = tk.BooleanVar()
|
||||
|
||||
self.full_backup_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["full_backup"],
|
||||
variable=self.vollbackup_var, command=lambda: enforce_backup_type_exclusivity(self.vollbackup_var, self.inkrementell_var, self.vollbackup_var.get()))
|
||||
variable=self.vollbackup_var, command=lambda: self.actions.handle_backup_type_change('voll'))
|
||||
self.full_backup_cb.pack(side=tk.LEFT, padx=5)
|
||||
self.incremental_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["incremental"],
|
||||
variable=self.inkrementell_var, command=lambda: enforce_backup_type_exclusivity(self.inkrementell_var, self.vollbackup_var, self.inkrementell_var.get()))
|
||||
variable=self.inkrementell_var, command=lambda: self.actions.handle_backup_type_change('inkrementell'))
|
||||
self.incremental_cb.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.compressed_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["compressed"],
|
||||
@@ -572,6 +587,12 @@ class MainApplication(tk.Tk):
|
||||
|
||||
self.backup_is_running = False
|
||||
self.actions._set_ui_state(True) # Re-enable UI
|
||||
elif message_type == 'completion_accurate':
|
||||
if value == 'success':
|
||||
self.current_file_label.config(text=Msg.STR["accurate_size_success"])
|
||||
else:
|
||||
self.current_file_label.config(text=Msg.STR["accurate_size_failed"])
|
||||
self.actions._set_ui_state(True) # Re-enable UI
|
||||
else:
|
||||
# This message is not for us (likely for DataProcessing), put it back and yield.
|
||||
self.queue.put(message)
|
||||
|
||||
@@ -256,6 +256,12 @@ class Msg:
|
||||
"cancel_backup": _("Cancel"),
|
||||
"backup_cancelled_and_deleted_msg": _("Backup cancelled and partially completed backup deleted."),
|
||||
"info_text_placeholder": _("Info text about the current view."),
|
||||
"accurate_size_cb_label": _("Accurate inkrem. size"),
|
||||
"accurate_size_info_label": _("(Calculation may take longer)"),
|
||||
"accurate_size_success": _("Accurate size calculated successfully."),
|
||||
"accurate_size_failed": _("Failed to calculate size. See log for details."),
|
||||
"please_wait": _("Please wait, calculating..."),
|
||||
"accurate_calc_cancelled": _("Calculate size cancelled."),
|
||||
|
||||
# Menus
|
||||
"file_menu": _("File"),
|
||||
|
||||
@@ -47,20 +47,123 @@ class Actions:
|
||||
|
||||
if full_backup_exists:
|
||||
# Case 1: A full backup exists. Allow user to choose, default to incremental.
|
||||
self.app.vollbackup_var.set(False)
|
||||
self.app.inkrementell_var.set(True)
|
||||
if not self.app.vollbackup_var.get(): # If user has not manually selected full backup
|
||||
self.app.vollbackup_var.set(False)
|
||||
self.app.inkrementell_var.set(True)
|
||||
self.app.full_backup_cb.config(state="normal")
|
||||
self.app.incremental_cb.config(state="normal")
|
||||
self.app.accurate_size_cb.config(state="normal") # Enable accurate calc
|
||||
else:
|
||||
# Case 2: No full backup exists. Force a full backup.
|
||||
self.app.vollbackup_var.set(True)
|
||||
self.app.inkrementell_var.set(False)
|
||||
self.app.full_backup_cb.config(state="disabled")
|
||||
self.app.incremental_cb.config(state="disabled")
|
||||
self.app.accurate_size_cb.config(state="disabled") # Disable accurate calc
|
||||
|
||||
def handle_backup_type_change(self, changed_var_name):
|
||||
# This function is called when the user clicks on "Full" or "Incremental".
|
||||
# It enforces that only one can be selected and updates the accurate size checkbox accordingly.
|
||||
if changed_var_name == 'voll':
|
||||
if self.app.vollbackup_var.get(): # if "Full" was checked
|
||||
self.app.inkrementell_var.set(False)
|
||||
elif changed_var_name == 'inkrementell':
|
||||
if self.app.inkrementell_var.get(): # if "Incremental" was checked
|
||||
self.app.vollbackup_var.set(False)
|
||||
|
||||
# Now, update the state of the accurate calculation checkbox
|
||||
if self.app.vollbackup_var.get():
|
||||
self.app.accurate_size_cb.config(state="disabled")
|
||||
self.app.genaue_berechnung_var.set(False)
|
||||
else:
|
||||
# Only enable if it's an incremental backup of the system
|
||||
if self.app.left_canvas_data.get('folder') == "Computer":
|
||||
self.app.accurate_size_cb.config(state="normal")
|
||||
else:
|
||||
self.app.accurate_size_cb.config(state="disabled")
|
||||
|
||||
def on_toggle_accurate_size_calc(self):
|
||||
# This method is called when the user clicks the "Genaue inkrem. Größe" checkbox.
|
||||
if not self.app.genaue_berechnung_var.get():
|
||||
# Box was unchecked by user, but this shouldn't happen if disabled.
|
||||
# Or it was unchecked automatically after a run. Do nothing.
|
||||
return
|
||||
|
||||
# If a normal calculation is running, stop it.
|
||||
if self.app.calculation_thread and self.app.calculation_thread.is_alive():
|
||||
self.app.calculation_stop_event.set()
|
||||
app_logger.log("Stopping previous size calculation.")
|
||||
|
||||
# --- Start the accurate calculation ---
|
||||
app_logger.log("Accurate incremental size calculation requested.")
|
||||
self.app.accurate_calculation_running = True
|
||||
self._set_ui_state(False, keep_cancel_enabled=True) # Lock the UI, keep Cancel enabled
|
||||
self.app.start_pause_button.config(text=Msg.STR["cancel_backup"])
|
||||
|
||||
# Immediately clear the projection canvases
|
||||
self.app.drawing.reset_projection_canvases()
|
||||
|
||||
# Update canvas to show it's calculating and start animation
|
||||
self.app.info_label.config(text=Msg.STR["please_wait"], foreground="#0078d7")
|
||||
self.app.task_progress.config(mode="indeterminate")
|
||||
self.app.task_progress.start()
|
||||
self.app.left_canvas_data.update({
|
||||
'size': Msg.STR["calculating_size"],
|
||||
'calculating': True,
|
||||
})
|
||||
self.app.drawing.start_backup_calculation_display()
|
||||
self.app.animated_icon.start()
|
||||
|
||||
# Get folder path from the current canvas data
|
||||
folder_path = self.app.left_canvas_data.get('path_display')
|
||||
button_text = self.app.left_canvas_data.get('folder')
|
||||
|
||||
if not folder_path or not button_text:
|
||||
app_logger.log("Cannot start accurate calculation, source folder info missing.")
|
||||
self._set_ui_state(True) # Unlock UI
|
||||
self.app.genaue_berechnung_var.set(False) # Uncheck the box
|
||||
self.app.accurate_calculation_running = False
|
||||
self.app.animated_icon.stop("DISABLE")
|
||||
return
|
||||
|
||||
def threaded_incremental_calc():
|
||||
status = 'failure' # Default to failure
|
||||
size = 0
|
||||
try:
|
||||
exclude_file_paths = []
|
||||
if AppConfig.GENERATED_EXCLUDE_LIST_PATH.exists():
|
||||
exclude_file_paths.append(AppConfig.GENERATED_EXCLUDE_LIST_PATH)
|
||||
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
|
||||
exclude_file_paths.append(AppConfig.USER_EXCLUDE_LIST_PATH)
|
||||
|
||||
base_dest = self.app.destination_path
|
||||
correct_parent_dir = os.path.join(base_dest, "pybackup")
|
||||
dummy_dest_for_calc = os.path.join(correct_parent_dir, "dummy_name")
|
||||
|
||||
size = self.app.data_processing.get_incremental_backup_size(
|
||||
source_path=folder_path,
|
||||
dest_path=dummy_dest_for_calc,
|
||||
is_system=True,
|
||||
exclude_files=exclude_file_paths
|
||||
)
|
||||
# If rsync fails, size is 0, so status will be failure.
|
||||
status = 'success' if size > 0 else 'failure'
|
||||
except Exception as e:
|
||||
app_logger.log(f"Error during threaded_incremental_calc: {e}")
|
||||
status = 'failure'
|
||||
finally:
|
||||
# This message MUST be sent to unlock the UI and update the display.
|
||||
# It is now handled by the main data_processing queue.
|
||||
if self.app.accurate_calculation_running: # Only post if not cancelled
|
||||
self.app.queue.put((button_text, size, self.app.mode, 'accurate_incremental', status))
|
||||
|
||||
self.app.calculation_thread = threading.Thread(target=threaded_incremental_calc)
|
||||
self.app.calculation_thread.daemon = True
|
||||
self.app.calculation_thread.start()
|
||||
|
||||
def on_sidebar_button_click(self, button_text):
|
||||
if self.app.backup_is_running:
|
||||
app_logger.log("Action blocked: Backup is in progress.")
|
||||
if self.app.backup_is_running or self.app.accurate_calculation_running:
|
||||
app_logger.log("Action blocked: Backup or accurate calculation is in progress.")
|
||||
return
|
||||
|
||||
self.app.drawing.reset_projection_canvases()
|
||||
@@ -112,7 +215,15 @@ class Actions:
|
||||
else: # restore
|
||||
extra_info = Msg.STR["user_restore_info"]
|
||||
|
||||
# Unified logic for starting a calculation on the left canvas
|
||||
# CORRECTED LOGIC ORDER:
|
||||
# 1. Update backup type checkboxes BEFORE starting the calculation.
|
||||
if self.app.mode == 'backup':
|
||||
self._update_backup_type_controls()
|
||||
else: # restore mode
|
||||
self.app.config_manager.set_setting(
|
||||
"restore_destination_path", folder_path)
|
||||
|
||||
# 2. Unified logic for starting a calculation on the left canvas.
|
||||
self._start_left_canvas_calculation(
|
||||
button_text, str(folder_path), icon_name, extra_info)
|
||||
|
||||
@@ -159,9 +270,10 @@ class Actions:
|
||||
|
||||
self.app.calculation_stop_event = threading.Event()
|
||||
|
||||
# Decide which calculation method to use based on the source
|
||||
# For system backup, now we default to the simple, fast (but for incremental inaccurate) full size calculation.
|
||||
# The accurate calculation is now triggered explicitly by the new checkbox.
|
||||
if button_text == "Computer":
|
||||
# For system backup, use exclusions
|
||||
app_logger.log("Using default (full) size calculation for system backup display.")
|
||||
exclude_patterns = self.app.data_processing.load_exclude_patterns()
|
||||
target_method = self.app.data_processing.get_folder_size_threaded
|
||||
args = (folder_path, button_text, self.app.calculation_stop_event,
|
||||
@@ -329,7 +441,7 @@ class Actions:
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
def _set_ui_state(self, enable: bool):
|
||||
def _set_ui_state(self, enable: bool, keep_cancel_enabled: bool = False):
|
||||
# Sidebar Buttons
|
||||
for text, data in self.app.buttons_map.items():
|
||||
# Find the actual button widget in the sidebar_buttons_frame
|
||||
@@ -352,13 +464,7 @@ class Actions:
|
||||
|
||||
# Top Navigation Buttons
|
||||
for i, button in enumerate(self.app.nav_buttons):
|
||||
# Keep "Backup" and "Log" always enabled
|
||||
if (
|
||||
self.app.nav_buttons_defs[i][0] == Msg.STR["backup_menu"] or
|
||||
self.app.nav_buttons_defs[i][0] == Msg.STR["log"]):
|
||||
button.config(state="normal")
|
||||
else:
|
||||
button.config(state="normal" if enable else "disabled")
|
||||
button.config(state="normal" if enable else "disabled")
|
||||
|
||||
# Right Canvas (Destination/Restore Source)
|
||||
if enable:
|
||||
@@ -380,6 +486,7 @@ class Actions:
|
||||
checkboxes = [
|
||||
self.app.full_backup_cb,
|
||||
self.app.incremental_cb,
|
||||
self.app.accurate_size_cb,
|
||||
self.app.compressed_cb,
|
||||
self.app.encrypted_cb,
|
||||
self.app.test_run_cb,
|
||||
@@ -387,8 +494,33 @@ class Actions:
|
||||
]
|
||||
for cb in checkboxes:
|
||||
cb.config(state="disabled")
|
||||
|
||||
# Special handling for the cancel button during accurate calculation
|
||||
if keep_cancel_enabled:
|
||||
self.app.start_pause_button.config(state="normal")
|
||||
|
||||
def toggle_start_cancel(self):
|
||||
# If an accurate calculation is running, cancel it.
|
||||
if self.app.accurate_calculation_running:
|
||||
app_logger.log("Accurate size calculation cancelled by user.")
|
||||
self.app.accurate_calculation_running = False
|
||||
self.app.genaue_berechnung_var.set(False)
|
||||
|
||||
# Stop all animations and restore UI
|
||||
self.app.animated_icon.stop("DISABLE")
|
||||
if self.app.left_canvas_animation:
|
||||
self.app.left_canvas_animation.stop()
|
||||
self.app.left_canvas_data['calculating'] = False
|
||||
self.app.drawing.redraw_left_canvas()
|
||||
|
||||
# Restore bottom action bar
|
||||
self.app.task_progress.stop()
|
||||
self.app.task_progress.config(mode="determinate", value=0)
|
||||
self.app.info_label.config(text=Msg.STR["accurate_calc_cancelled"], foreground="#E8740C") # Orange for cancelled
|
||||
self.app.start_pause_button.config(text=Msg.STR["start"])
|
||||
self._set_ui_state(True)
|
||||
return
|
||||
|
||||
# If a backup is already running, we must be cancelling.
|
||||
if self.app.backup_is_running:
|
||||
self.app.animated_icon.stop("DISABLE")
|
||||
@@ -499,9 +631,7 @@ class Actions:
|
||||
# Store the path for potential deletion
|
||||
self.app.current_backup_path = final_dest
|
||||
|
||||
# Get source size from canvas data and parse it
|
||||
size_display_str = self.app.left_canvas_data.get('size', '0 B')
|
||||
source_size_bytes = self._parse_size_string_to_bytes(size_display_str)
|
||||
source_size_bytes = self.app.left_canvas_data.get('total_bytes', 0)
|
||||
|
||||
exclude_file_paths = []
|
||||
if AppConfig.GENERATED_EXCLUDE_LIST_PATH.exists():
|
||||
|
||||
Reference in New Issue
Block a user