Mehrere grundlegende Probleme in der Anwendungslogik wurden behoben:
- **UI-Verarbeitungsschleife:** Die Verarbeitung von Nachrichten aus Hintergrund-Threads wurde komplett überarbeitet. Zuvor führten zwei konkurrierende Schleifen zu einer Race Condition, bei der Nachrichten verloren gingen. Jetzt gibt es eine einzige, zentrale Verarbeitungsschleife, die Nachrichten in Stapeln verarbeitet. Dies behebt das Problem, dass die Benutzeroberfläche nach dem Löschen oder dem Abschluss eines Backups im "in Arbeit"-Zustand hängen blieb.
- **Backup-Grössenberechnung:** Die Ermittlung der Grösse von inkrementellen Backups wurde robuster gestaltet.
- Die rsync-Ausgabe wird nun zuverlässig auf Englisch erzwungen, um Parsing-Fehler in anderen System-Locales zu vermeiden.
- Die Grösse wird nun aus der `sent... received...` Zusammenfassungszeile von rsync ausgelesen, was auch bei Backups ohne Datenänderungen einen Wert ungleich Null liefert.
- Es wird nun korrekt zwischen Voll-Backups (Anzeige der Gesamtgrösse) und inkrementellen Backups (Anzeige der Übertragungsgrösse) unterschieden.
- **Sonstige Korrekturen:**
- Eine fehlende Übersetzung für die manuelle Ausschlussliste wurde hinzugefügt.
- Ein überflüssiger Aufruf zum Starten der Verarbeitungsschleife wurde entfernt.
701 lines
30 KiB
Python
701 lines
30 KiB
Python
# pyimage_ui/actions.py
|
|
import tkinter as tk
|
|
import os
|
|
import shutil
|
|
import threading
|
|
import datetime
|
|
import locale
|
|
from typing import Optional
|
|
|
|
from shared_libs.message import MessageDialog
|
|
from shared_libs.custom_file_dialog import CustomFileDialog
|
|
from pbp_app_config import AppConfig, Msg
|
|
from shared_libs.logger import app_logger
|
|
from shared_libs.common_tools import message_box_animation
|
|
|
|
|
|
class Actions:
|
|
def __init__(self, app):
|
|
self.app = app
|
|
|
|
def _set_backup_type(self, backup_type: str):
|
|
if backup_type == "full":
|
|
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")
|
|
elif backup_type == "incremental":
|
|
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")
|
|
|
|
def _update_backup_type_controls(self):
|
|
"""
|
|
Updates the state of the Full/Incremental backup radio buttons based on
|
|
advanced settings and the content of the destination folder.
|
|
This logic only applies to 'Computer' backups.
|
|
"""
|
|
# Do nothing if the backup mode is not 'backup' or source is not 'Computer'
|
|
if self.app.mode != 'backup' or self.app.left_canvas_data.get('folder') != "Computer":
|
|
self.app.full_backup_cb.config(state="normal")
|
|
self.app.incremental_cb.config(state="normal")
|
|
return
|
|
|
|
# Respect that advanced settings might have already disabled the controls
|
|
# This check is based on the user's confirmation that this logic exists elsewhere
|
|
if self.app.full_backup_cb.cget('state') == 'disabled':
|
|
return
|
|
|
|
# --- Standard Logic ---
|
|
full_backup_exists = False
|
|
if self.app.destination_path and os.path.isdir(self.app.destination_path):
|
|
system_backups = self.app.backup_manager.list_system_backups(
|
|
self.app.destination_path)
|
|
for backup in system_backups:
|
|
if backup.get('type') == 'Full':
|
|
full_backup_exists = True
|
|
break
|
|
|
|
if full_backup_exists:
|
|
self._set_backup_type("incremental")
|
|
else:
|
|
self._set_backup_type("full")
|
|
|
|
def handle_backup_type_change(self, changed_var_name):
|
|
if changed_var_name == 'voll':
|
|
if self.app.vollbackup_var.get():
|
|
self._set_backup_type("full")
|
|
elif changed_var_name == 'inkrementell':
|
|
if self.app.inkrementell_var.get():
|
|
self._set_backup_type("incremental")
|
|
|
|
def handle_compression_change(self):
|
|
if self.app.compressed_var.get():
|
|
# Compression is enabled, force 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")
|
|
# Also disable accurate incremental size calculation, as it's irrelevant
|
|
self.app.accurate_size_cb.config(state="disabled")
|
|
self.app.genaue_berechnung_var.set(False)
|
|
else:
|
|
# Compression is disabled, restore normal logic
|
|
self.app.full_backup_cb.config(state="normal")
|
|
# The state of the incremental checkbox depends on whether a full backup exists
|
|
self._update_backup_type_controls()
|
|
|
|
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
|
|
# Lock the UI, keep Cancel enabled
|
|
self._set_ui_state(False, keep_cancel_enabled=True)
|
|
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)
|
|
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
|
|
exclude_file_paths.append(AppConfig.MANUAL_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 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()
|
|
if not self.app.canvas_frame.winfo_viewable():
|
|
self.app.navigation.toggle_mode(
|
|
self.app.mode, trigger_calculation=False)
|
|
|
|
self.app.log_window.clear_log()
|
|
|
|
# Reverse map from translated UI string to canonical key
|
|
REVERSE_FOLDER_MAP = {
|
|
"Computer": "Computer",
|
|
Msg.STR["cat_documents"]: "Documents",
|
|
Msg.STR["cat_images"]: "Pictures",
|
|
Msg.STR["cat_music"]: "Music",
|
|
Msg.STR["cat_videos"]: "Videos",
|
|
}
|
|
canonical_key = REVERSE_FOLDER_MAP.get(button_text, button_text)
|
|
|
|
folder_path = AppConfig.FOLDER_PATHS.get(canonical_key)
|
|
|
|
if not folder_path or not folder_path.exists():
|
|
print(
|
|
f"Folder not found for {canonical_key} (Path: {folder_path})")
|
|
self.app.start_pause_button.config(state="disabled")
|
|
return
|
|
|
|
if self.app.mode == 'restore':
|
|
try:
|
|
total, used, free = shutil.disk_usage(str(folder_path))
|
|
self.app.destination_total_bytes = total
|
|
self.app.destination_used_bytes = used
|
|
except FileNotFoundError:
|
|
app_logger.log(f"Path not found for disk usage: {folder_path}")
|
|
return
|
|
|
|
icon_name = self.app.buttons_map[button_text]['icon']
|
|
|
|
# Determine the correct description based on mode and selection
|
|
extra_info = ""
|
|
if button_text == "Computer":
|
|
if self.app.mode == 'backup':
|
|
extra_info = Msg.STR["system_backup_info"]
|
|
else: # restore
|
|
extra_info = Msg.STR["system_restore_info"]
|
|
else: # User folder
|
|
if self.app.mode == 'backup':
|
|
extra_info = Msg.STR["user_backup_info"]
|
|
else: # restore
|
|
extra_info = Msg.STR["user_restore_info"]
|
|
|
|
# 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)
|
|
|
|
def _start_left_canvas_calculation(self, button_text, folder_path, icon_name, extra_info):
|
|
self.app.start_pause_button.config(state="disabled")
|
|
if self.app.mode == 'backup' and not self.app.destination_path:
|
|
self.app.left_canvas_data.update({
|
|
'icon': icon_name,
|
|
'folder': button_text,
|
|
'path_display': folder_path,
|
|
'size': Msg.STR["select_destination_first"],
|
|
'calculating': False,
|
|
'extra_info': extra_info
|
|
})
|
|
self.app.drawing.redraw_left_canvas()
|
|
return
|
|
|
|
if self.app.mode == 'restore' and not self.app.right_canvas_data.get('path_display'):
|
|
self.app.left_canvas_data.update({
|
|
'icon': icon_name,
|
|
'folder': button_text,
|
|
'path_display': folder_path,
|
|
'size': Msg.STR["select_source_first"],
|
|
'calculating': False,
|
|
'extra_info': extra_info
|
|
})
|
|
self.app.drawing.redraw_left_canvas()
|
|
return
|
|
|
|
if self.app.calculation_thread and self.app.calculation_thread.is_alive():
|
|
self.app.calculation_stop_event.set()
|
|
|
|
data_dict = self.app.left_canvas_data
|
|
data_dict.update({
|
|
'icon': icon_name,
|
|
'folder': button_text,
|
|
'path_display': folder_path,
|
|
'size': Msg.STR["calculating_size"],
|
|
'calculating': True,
|
|
'extra_info': extra_info
|
|
})
|
|
|
|
self.app.drawing.start_backup_calculation_display()
|
|
|
|
self.app.calculation_stop_event = threading.Event()
|
|
|
|
# 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":
|
|
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,
|
|
exclude_patterns, self.app.mode)
|
|
else:
|
|
# For user folders, do not use any exclusions
|
|
target_method = self.app.data_processing.get_user_folder_size_threaded
|
|
args = (folder_path, button_text,
|
|
self.app.calculation_stop_event, self.app.mode)
|
|
|
|
self.app.calculation_thread = threading.Thread(
|
|
target=target_method, args=args)
|
|
self.app.calculation_thread.daemon = True
|
|
self.app.calculation_thread.start()
|
|
|
|
if self.app.mode == 'backup':
|
|
self._update_backup_type_controls()
|
|
else: # restore mode
|
|
self.app.config_manager.set_setting(
|
|
"restore_destination_path", folder_path)
|
|
|
|
def on_right_canvas_click(self, event):
|
|
if self.app.backup_is_running:
|
|
app_logger.log("Action blocked: Backup is in progress.")
|
|
return
|
|
|
|
self.app.drawing.reset_projection_canvases()
|
|
self.app.after(100, self._open_source_or_destination_dialog)
|
|
|
|
def _open_source_or_destination_dialog(self):
|
|
title = Msg.STR["select_dest_folder_title"] if self.app.mode == 'backup' else Msg.STR["select_restore_source_title"]
|
|
path = self._get_path_from_dialog(title)
|
|
if path:
|
|
self._update_right_canvas_info(path)
|
|
|
|
def _get_path_from_dialog(self, title) -> Optional[str]:
|
|
self.app.update_idletasks()
|
|
dialog = CustomFileDialog(self.app, mode="dir", title=title)
|
|
self.app.wait_window(dialog)
|
|
path = dialog.get_result()
|
|
dialog.destroy()
|
|
return path
|
|
|
|
def _update_right_canvas_info(self, path):
|
|
try:
|
|
if self.app.mode == "backup":
|
|
self.app.destination_path = path
|
|
|
|
backup_root_to_exclude = f"/{path.strip('/').split('/')[0]}"
|
|
try:
|
|
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'r+') as f:
|
|
lines = f.readlines()
|
|
f.seek(0)
|
|
if not any(backup_root_to_exclude in line for line in lines):
|
|
lines.append(f"\n{backup_root_to_exclude}\n")
|
|
f.writelines(lines)
|
|
except FileNotFoundError:
|
|
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'w') as f:
|
|
f.write(f"{backup_root_to_exclude}\n")
|
|
except IOError as e:
|
|
app_logger.log(f"Error updating exclusion list: {e}")
|
|
|
|
total, used, free = shutil.disk_usage(path)
|
|
self.app.destination_total_bytes = total
|
|
self.app.destination_used_bytes = used
|
|
size_str = f"{used / (1024**3):.2f} GB / {total / (1024**3):.2f} GB"
|
|
|
|
self.app.right_canvas_data.update({
|
|
'folder': os.path.basename(path.rstrip('/')),
|
|
'path_display': path,
|
|
'size': size_str
|
|
})
|
|
self.app.config_manager.set_setting(
|
|
"backup_destination_path", path)
|
|
self.app.drawing.redraw_right_canvas()
|
|
self.app.drawing.update_target_projection()
|
|
|
|
current_source = self.app.left_canvas_data.get('folder')
|
|
if current_source:
|
|
self.on_sidebar_button_click(current_source)
|
|
self._update_backup_type_controls()
|
|
|
|
elif self.app.mode == "restore":
|
|
self.app.right_canvas_data.update({
|
|
'folder': os.path.basename(path.rstrip('/')),
|
|
'path_display': path,
|
|
'size': ''
|
|
})
|
|
self.app.config_manager.set_setting(
|
|
"restore_source_path", path)
|
|
self.app.drawing.calculate_restore_folder_size()
|
|
self.app.start_pause_button.config(state="normal")
|
|
|
|
except FileNotFoundError:
|
|
with message_box_animation(self.app.calculating_animation):
|
|
MessageDialog(master=self.app, message_type="error",
|
|
title=Msg.STR["error"], text=Msg.STR["path_not_found"].format(path=path)).show()
|
|
|
|
def reset_to_default_settings(self):
|
|
try:
|
|
AppConfig.create_default_user_excludes()
|
|
except OSError as e:
|
|
app_logger.log(f"Error creating default user exclude list: {e}")
|
|
|
|
self.app.config_manager.set_setting("backup_destination_path", None)
|
|
self.app.config_manager.set_setting("restore_source_path", None)
|
|
self.app.config_manager.set_setting("restore_destination_path", None)
|
|
|
|
# Remove advanced settings
|
|
self.app.config_manager.remove_setting("backup_animation_type")
|
|
self.app.config_manager.remove_setting("calculation_animation_type")
|
|
self.app.config_manager.remove_setting("force_full_backup")
|
|
self.app.config_manager.remove_setting("force_incremental_backup")
|
|
self.app.config_manager.remove_setting("force_compression")
|
|
self.app.config_manager.remove_setting("force_encryption")
|
|
|
|
# Update the main UI to reflect the cleared settings
|
|
self.app.update_backup_options_from_config()
|
|
|
|
AppConfig.generate_and_write_final_exclude_list()
|
|
app_logger.log("Settings have been reset to default values.")
|
|
|
|
settings_frame = self.app.settings_frame
|
|
if settings_frame:
|
|
settings_frame.load_and_display_excludes()
|
|
settings_frame._load_hidden_files()
|
|
self.app.destination_path = None
|
|
self.app.start_pause_button.config(state="disabled")
|
|
|
|
# Clear the canvases and reset the UI to its initial state for the current mode
|
|
self.app.backup_left_canvas_data.clear()
|
|
self.app.backup_right_canvas_data.clear()
|
|
self.app.restore_left_canvas_data.clear()
|
|
self.app.restore_right_canvas_data.clear()
|
|
|
|
current_source = self.app.left_canvas_data.get('folder')
|
|
if current_source:
|
|
self.on_sidebar_button_click(current_source)
|
|
|
|
self.app.backup_content_frame.system_backups_frame._load_backup_content()
|
|
self.app.backup_content_frame.user_backups_frame._load_backup_content()
|
|
|
|
with message_box_animation(self.app.animated_icon):
|
|
MessageDialog(master=self.app, message_type="info",
|
|
title=Msg.STR["settings_reset_title"], text=Msg.STR["settings_reset_text"])
|
|
|
|
def _parse_size_string_to_bytes(self, size_str: str) -> int:
|
|
"""Parses a size string like '38.61 GB' into bytes."""
|
|
if not size_str or size_str == Msg.STR["calculating_size"]:
|
|
return 0
|
|
|
|
parts = size_str.split()
|
|
if len(parts) != 2:
|
|
return 0 # Invalid format
|
|
|
|
try:
|
|
value = float(parts[0])
|
|
unit = parts[1].upper()
|
|
|
|
if unit == 'B':
|
|
return int(value)
|
|
elif unit == 'KB':
|
|
return int(value * (1024**1))
|
|
elif unit == 'MB':
|
|
return int(value * (1024**2))
|
|
elif unit == 'GB':
|
|
return int(value * (1024**3))
|
|
elif unit == 'TB':
|
|
return int(value * (1024**4))
|
|
else:
|
|
return 0
|
|
except ValueError:
|
|
return 0
|
|
|
|
def _set_ui_state(self, enable: bool, keep_cancel_enabled: bool = False, allow_log_and_backup_toggle: bool = False):
|
|
# Sidebar Buttons
|
|
for text, data in self.app.buttons_map.items():
|
|
# Find the actual button widget in the sidebar_buttons_frame
|
|
# This assumes the order of creation is consistent or we can identify by text
|
|
# A more robust way would be to store references to the buttons in a dict in MainApplication
|
|
# For now, let's iterate through children and match text
|
|
for child in self.app.sidebar_buttons_frame.winfo_children():
|
|
if isinstance(child, tk.ttk.Button) and child.cget("text") == text:
|
|
child.config(state="normal" if enable else "disabled")
|
|
break
|
|
|
|
# Schedule and Settings buttons in sidebar
|
|
self.app.schedule_dialog_button.config(
|
|
state="normal" if enable else "disabled")
|
|
self.app.settings_button.config(
|
|
state="normal" if enable else "disabled")
|
|
|
|
# Mode Button (arrow between canvases)
|
|
self.app.mode_button.config(state="normal" if enable else "disabled")
|
|
|
|
# Top Navigation Buttons
|
|
for i, button in enumerate(self.app.nav_buttons):
|
|
if allow_log_and_backup_toggle and self.app.nav_buttons_defs[i][0] in [Msg.STR["log"], Msg.STR["backup_menu"]]:
|
|
continue
|
|
button.config(state="normal" if enable else "disabled")
|
|
|
|
# Right Canvas (Destination/Restore Source)
|
|
if enable:
|
|
self.app.right_canvas.bind(
|
|
"<Button-1>", self.app.actions.on_right_canvas_click)
|
|
self.app.right_canvas.config(cursor="hand2")
|
|
else:
|
|
self.app.right_canvas.unbind("<Button-1>")
|
|
self.app.right_canvas.config(cursor="")
|
|
|
|
# Checkboxes in the task bar
|
|
if enable:
|
|
# When enabling, re-run the logic that sets the correct state
|
|
# for all checkboxes based on config and context.
|
|
self.app.update_backup_options_from_config()
|
|
self.app.actions._update_backup_type_controls()
|
|
else:
|
|
# When disabling, just disable all of them.
|
|
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,
|
|
self.app.bypass_security_cb
|
|
]
|
|
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(
|
|
# Orange for cancelled
|
|
text=Msg.STR["accurate_calc_cancelled"], foreground="#E8740C")
|
|
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")
|
|
|
|
delete_path = getattr(self.app, 'current_backup_path', None)
|
|
is_system_backup = self.app.backup_manager.is_system_process
|
|
|
|
if is_system_backup:
|
|
if delete_path:
|
|
self.app.backup_manager.cancel_and_delete_privileged_backup(
|
|
delete_path)
|
|
app_logger.log(
|
|
Msg.STR["backup_cancelled_and_deleted_msg"])
|
|
self.app.info_label.config(
|
|
text=Msg.STR["backup_cancelled_and_deleted_msg"])
|
|
else:
|
|
self.app.backup_manager.cancel_backup()
|
|
app_logger.log(
|
|
"Backup cancelled, but directory could not be deleted (path unknown).")
|
|
self.app.info_label.config(
|
|
text="Backup cancelled, but directory could not be deleted (path unknown).")
|
|
else:
|
|
self.app.backup_manager.cancel_backup()
|
|
if delete_path:
|
|
try:
|
|
app_logger.log(
|
|
f"Attempting to delete non-privileged path: {delete_path}")
|
|
if os.path.isdir(delete_path):
|
|
shutil.rmtree(delete_path)
|
|
app_logger.log(
|
|
Msg.STR["backup_cancelled_and_deleted_msg"])
|
|
self.app.info_label.config(
|
|
text=Msg.STR["backup_cancelled_and_deleted_msg"])
|
|
except Exception as e:
|
|
app_logger.log(f"Error deleting backup directory: {e}")
|
|
self.app.info_label.config(
|
|
text=f"Error deleting backup directory: {e}")
|
|
else:
|
|
app_logger.log(
|
|
"Backup cancelled, but no path found to delete.")
|
|
self.app.info_label.config(
|
|
text="Backup cancelled, but no path found to delete.")
|
|
|
|
if hasattr(self.app, 'current_backup_path'):
|
|
self.app.current_backup_path = None
|
|
|
|
# Reset state
|
|
self.app.backup_is_running = False
|
|
self.app.start_pause_button["text"] = Msg.STR["start"]
|
|
self._set_ui_state(True)
|
|
|
|
# Otherwise, we are starting a new backup.
|
|
else:
|
|
if self.app.start_pause_button['state'] == 'disabled':
|
|
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()
|
|
|
|
self.app.log_window.clear_log()
|
|
self._set_ui_state(False, allow_log_and_backup_toggle=True)
|
|
|
|
self.app.animated_icon.start()
|
|
|
|
if self.app.mode == "backup":
|
|
source_size_bytes = self.app.source_size_bytes
|
|
if self.app.vollbackup_var.get():
|
|
self._start_system_backup("full", source_size_bytes)
|
|
else:
|
|
self._start_system_backup("incremental", source_size_bytes)
|
|
else:
|
|
pass
|
|
|
|
def _start_system_backup(self, mode, source_size_bytes):
|
|
base_dest = self.app.destination_path
|
|
if not base_dest:
|
|
MessageDialog(master=self.app, message_type="error",
|
|
title=Msg.STR["error"], text=Msg.STR["err_no_dest_folder"])
|
|
return
|
|
|
|
if base_dest.startswith("/home"):
|
|
with message_box_animation(self.app.animated_icon):
|
|
MessageDialog(master=self.app, message_type="error",
|
|
title=Msg.STR["error"], text=Msg.STR["system_backup_in_home_error"])
|
|
return
|
|
|
|
try:
|
|
locale.setlocale(locale.LC_TIME, 'de_DE.UTF-8')
|
|
except locale.Error:
|
|
app_logger.log(
|
|
"Could not set locale to de_DE.UTF-8. Using default.")
|
|
|
|
now = datetime.datetime.now()
|
|
date_str = now.strftime("%d-%B-%Y")
|
|
time_str = now.strftime("%H%M%S")
|
|
folder_name = f"{date_str}_{time_str}_system_{mode}"
|
|
final_dest = os.path.join(base_dest, "pybackup", folder_name)
|
|
# Store the path for potential deletion
|
|
self.app.current_backup_path = final_dest
|
|
|
|
source_size_bytes = self.app.left_canvas_data.get('total_bytes', 0)
|
|
|
|
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)
|
|
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
|
|
exclude_file_paths.append(AppConfig.MANUAL_EXCLUDE_LIST_PATH)
|
|
|
|
is_dry_run = self.app.testlauf_var.get()
|
|
is_compressed = self.app.compressed_var.get()
|
|
|
|
self.app.backup_manager.start_backup(
|
|
queue=self.app.queue,
|
|
source_path="/",
|
|
dest_path=final_dest,
|
|
is_system=True,
|
|
is_dry_run=is_dry_run,
|
|
exclude_files=exclude_file_paths,
|
|
source_size=source_size_bytes,
|
|
is_compressed=is_compressed)
|
|
|
|
def _start_user_backup(self, sources):
|
|
dest = self.app.destination_path
|
|
if not dest:
|
|
MessageDialog(master=self.app, message_type="error",
|
|
title=Msg.STR["error"], text=Msg.STR["err_no_dest_folder"])
|
|
return
|
|
|
|
is_dry_run = self.app.testlauf_var.get()
|
|
for source in sources:
|
|
self.app.backup_manager.start_backup(
|
|
queue=self.app.queue,
|
|
source_path=source,
|
|
dest_path=dest,
|
|
is_system=False,
|
|
is_dry_run=is_dry_run)
|