- Create a separate file for manual excludes (`rsync-manual-excludes.conf`) that is not cleared on reset. - Add a button to the settings frame to add files/folders to the manual exclude list. - Update the backup and calculation logic to use the manual exclude list. - Ensure the UI reflects the combined exclude lists.
704 lines
30 KiB
Python
704 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 _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:
|
|
# Case 1: A full backup exists. Allow user to choose, default to incremental.
|
|
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 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":
|
|
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_path = 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()
|
|
|
|
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):
|
|
# 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):
|
|
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)
|
|
|
|
self.app.animated_icon.start()
|
|
|
|
self.app._process_backup_queue()
|
|
|
|
if self.app.mode == "backup":
|
|
if self.app.vollbackup_var.get():
|
|
self._start_system_backup("full")
|
|
else:
|
|
self._start_system_backup("incremental")
|
|
else:
|
|
pass
|
|
|
|
def _start_system_backup(self, mode):
|
|
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)
|