Improve UI and settings; ignore caches
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -2,3 +2,9 @@
|
|||||||
/dist/
|
/dist/
|
||||||
/build/
|
/build/
|
||||||
/.venv/
|
/.venv/
|
||||||
|
/.idea/
|
||||||
|
/.pytest_cache/
|
||||||
|
/__pycache__/
|
||||||
|
**/__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.log
|
||||||
|
|||||||
602
main.py
602
main.py
@@ -140,7 +140,7 @@ class BackgroundFileCopyApp:
|
|||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
self.root = root
|
self.root = root
|
||||||
self.root.title("Планировщик копирования бекапов")
|
self.root.title("Планировщик копирования бекапов")
|
||||||
self.root.geometry("900x700")
|
self.root.geometry("900x800")
|
||||||
self.setup_window_icon()
|
self.setup_window_icon()
|
||||||
|
|
||||||
# Для работы с очередью сообщений из потоков
|
# Для работы с очередью сообщений из потоков
|
||||||
@@ -148,6 +148,8 @@ class BackgroundFileCopyApp:
|
|||||||
|
|
||||||
# Список пар для копирования
|
# Список пар для копирования
|
||||||
self.copy_pairs = []
|
self.copy_pairs = []
|
||||||
|
self.pair_counter = 0
|
||||||
|
self.selected_pair_id = None
|
||||||
|
|
||||||
# Планировщик
|
# Планировщик
|
||||||
self.scheduler = FileCopyScheduler(self)
|
self.scheduler = FileCopyScheduler(self)
|
||||||
@@ -169,6 +171,7 @@ class BackgroundFileCopyApp:
|
|||||||
|
|
||||||
# Последний успешный запуск
|
# Последний успешный запуск
|
||||||
self.last_success_time: Optional[str] = None
|
self.last_success_time: Optional[str] = None
|
||||||
|
self.last_result_summary: Optional[str] = None
|
||||||
|
|
||||||
# Хеши последних копий
|
# Хеши последних копий
|
||||||
self.last_hashes: Dict[str, str] = {}
|
self.last_hashes: Dict[str, str] = {}
|
||||||
@@ -190,24 +193,48 @@ class BackgroundFileCopyApp:
|
|||||||
self.start_scheduler()
|
self.start_scheduler()
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
# Основной фрейм
|
|
||||||
main_frame = ttk.Frame(self.root, padding="10")
|
main_frame = ttk.Frame(self.root, padding="10")
|
||||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
# Заголовок
|
notebook = ttk.Notebook(main_frame)
|
||||||
title_label = ttk.Label(main_frame, text="Планировщик копирования бекапов",
|
notebook.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
settings_tab = ttk.Frame(notebook)
|
||||||
|
log_tab = ttk.Frame(notebook)
|
||||||
|
help_tab = ttk.Frame(notebook)
|
||||||
|
notebook.add(settings_tab, text="Настройки")
|
||||||
|
notebook.add(log_tab, text="Лог")
|
||||||
|
notebook.add(help_tab, text="Справка")
|
||||||
|
|
||||||
|
title_label = ttk.Label(settings_tab, text="Планировщик копирования бекапов",
|
||||||
font=("Arial", 14, "bold"))
|
font=("Arial", 14, "bold"))
|
||||||
title_label.pack(pady=10)
|
title_label.pack(pady=8)
|
||||||
|
|
||||||
# Фрейм настроек расписания
|
status_frame = ttk.LabelFrame(settings_tab, text="Состояние", padding="10")
|
||||||
schedule_frame = ttk.LabelFrame(main_frame, text="Настройки расписания", padding="10")
|
status_frame.pack(fill=tk.X, pady=5)
|
||||||
schedule_frame.pack(fill=tk.X, pady=10)
|
|
||||||
|
self.status_summary_label = ttk.Label(status_frame, text="—")
|
||||||
|
self.status_summary_label.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
self.next_run_label = ttk.Label(status_frame, text="(следующий запуск: —)",
|
||||||
|
font=("Arial", 9, "italic"))
|
||||||
|
self.next_run_label.pack(side=tk.LEFT, padx=10)
|
||||||
|
|
||||||
|
self.last_success_label = ttk.Label(status_frame, text="(последний успех: —)",
|
||||||
|
font=("Arial", 9, "italic"))
|
||||||
|
self.last_success_label.pack(side=tk.LEFT, padx=10)
|
||||||
|
|
||||||
|
self.last_result_label = ttk.Label(status_frame, text="(последний результат: —)",
|
||||||
|
font=("Arial", 9, "italic"))
|
||||||
|
self.last_result_label.pack(side=tk.LEFT, padx=10)
|
||||||
|
|
||||||
|
schedule_frame = ttk.LabelFrame(settings_tab, text="Расписание и поведение", padding="10")
|
||||||
|
schedule_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
# Время копирования
|
|
||||||
time_frame = ttk.Frame(schedule_frame)
|
time_frame = ttk.Frame(schedule_frame)
|
||||||
time_frame.pack(fill=tk.X, pady=5)
|
time_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
ttk.Label(time_frame, text="Время копирования:", width=15).pack(side=tk.LEFT)
|
ttk.Label(time_frame, text="Время копирования:", width=18).pack(side=tk.LEFT)
|
||||||
|
|
||||||
self.hour_var = tk.StringVar(value="03")
|
self.hour_var = tk.StringVar(value="03")
|
||||||
self.minute_var = tk.StringVar(value="00")
|
self.minute_var = tk.StringVar(value="00")
|
||||||
@@ -221,117 +248,139 @@ class BackgroundFileCopyApp:
|
|||||||
ttk.Combobox(time_frame, textvariable=self.minute_var, values=minutes,
|
ttk.Combobox(time_frame, textvariable=self.minute_var, values=minutes,
|
||||||
width=5, state="readonly").pack(side=tk.LEFT, padx=2)
|
width=5, state="readonly").pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
ttk.Label(time_frame, text="(ежедневно)", font=("Arial", 9, "italic")).pack(side=tk.LEFT, padx=10)
|
|
||||||
|
|
||||||
# Включение/выключение планировщика
|
|
||||||
scheduler_ctrl_frame = ttk.Frame(schedule_frame)
|
|
||||||
scheduler_ctrl_frame.pack(fill=tk.X, pady=5)
|
|
||||||
|
|
||||||
self.scheduler_enabled = tk.BooleanVar(value=False)
|
self.scheduler_enabled = tk.BooleanVar(value=False)
|
||||||
self.scheduler_check = ttk.Checkbutton(scheduler_ctrl_frame,
|
self.scheduler_check = ttk.Checkbutton(time_frame,
|
||||||
text="Включить автоматическое копирование по расписанию",
|
text="Ежедневно",
|
||||||
variable=self.scheduler_enabled,
|
variable=self.scheduler_enabled,
|
||||||
command=self.toggle_scheduler)
|
command=self.toggle_scheduler)
|
||||||
self.scheduler_check.pack(side=tk.LEFT)
|
self.scheduler_check.pack(side=tk.LEFT, padx=10)
|
||||||
|
|
||||||
self.scheduler_status = ttk.Label(scheduler_ctrl_frame, text="(остановлен)",
|
self.scheduler_status = ttk.Label(time_frame, text="(остановлен)",
|
||||||
font=("Arial", 9, "italic"))
|
font=("Arial", 9, "italic"))
|
||||||
self.scheduler_status.pack(side=tk.LEFT, padx=10)
|
self.scheduler_status.pack(side=tk.LEFT, padx=10)
|
||||||
|
|
||||||
self.next_run_label = ttk.Label(scheduler_ctrl_frame, text="(следующий запуск: —)",
|
options_frame = ttk.Frame(schedule_frame)
|
||||||
font=("Arial", 9, "italic"))
|
options_frame.pack(fill=tk.X, pady=5)
|
||||||
self.next_run_label.pack(side=tk.LEFT, padx=10)
|
|
||||||
|
|
||||||
self.last_success_label = ttk.Label(scheduler_ctrl_frame, text="(последний успех: —)",
|
|
||||||
font=("Arial", 9, "italic"))
|
|
||||||
self.last_success_label.pack(side=tk.LEFT, padx=10)
|
|
||||||
|
|
||||||
# Автозапуск
|
|
||||||
autostart_frame = ttk.Frame(schedule_frame)
|
|
||||||
autostart_frame.pack(fill=tk.X, pady=5)
|
|
||||||
|
|
||||||
self.autostart_check = ttk.Checkbutton(
|
self.autostart_check = ttk.Checkbutton(
|
||||||
autostart_frame,
|
options_frame,
|
||||||
text="Добавить программу в автозапуск Windows",
|
text="Автозапуск Windows",
|
||||||
variable=self.autostart_enabled,
|
variable=self.autostart_enabled,
|
||||||
command=self.toggle_autostart
|
command=self.toggle_autostart
|
||||||
)
|
)
|
||||||
self.autostart_check.pack(side=tk.LEFT)
|
self.autostart_check.pack(side=tk.LEFT)
|
||||||
|
|
||||||
minimize_frame = ttk.Frame(schedule_frame)
|
ttk.Button(options_frame, text="?", width=2,
|
||||||
minimize_frame.pack(fill=tk.X, pady=5)
|
command=lambda: self.show_hint("Автозапуск",
|
||||||
|
"Добавляет программу в автозапуск Windows.")).pack(side=tk.LEFT, padx=4)
|
||||||
|
|
||||||
self.minimize_check = ttk.Checkbutton(
|
self.minimize_check = ttk.Checkbutton(
|
||||||
minimize_frame,
|
options_frame,
|
||||||
text="Сворачивать в трей при закрытии",
|
text="Сворачивать в трей при закрытии",
|
||||||
variable=self.minimize_to_tray_enabled
|
variable=self.minimize_to_tray_enabled
|
||||||
)
|
)
|
||||||
self.minimize_check.pack(side=tk.LEFT)
|
self.minimize_check.pack(side=tk.LEFT, padx=10)
|
||||||
|
|
||||||
verify_frame = ttk.Frame(schedule_frame)
|
ttk.Button(options_frame, text="?", width=2,
|
||||||
verify_frame.pack(fill=tk.X, pady=5)
|
command=lambda: self.show_hint("Сворачивание",
|
||||||
|
"По крестику окно скрывается в трей.")).pack(side=tk.LEFT, padx=4)
|
||||||
|
|
||||||
self.verify_check = ttk.Checkbutton(
|
self.verify_check = ttk.Checkbutton(
|
||||||
verify_frame,
|
options_frame,
|
||||||
text="Только проверка (без копирования)",
|
text="Только проверка (без копирования)",
|
||||||
variable=self.verify_only
|
variable=self.verify_only
|
||||||
)
|
)
|
||||||
self.verify_check.pack(side=tk.LEFT)
|
self.verify_check.pack(side=tk.LEFT, padx=10)
|
||||||
|
|
||||||
|
ttk.Button(options_frame, text="?", width=2,
|
||||||
|
command=lambda: self.show_hint("Проверка",
|
||||||
|
"Проверяет совпадение контрольных сумм без копирования.")).pack(side=tk.LEFT, padx=4)
|
||||||
|
|
||||||
# Кнопки управления
|
|
||||||
buttons_frame = ttk.Frame(schedule_frame)
|
buttons_frame = ttk.Frame(schedule_frame)
|
||||||
buttons_frame.pack(fill=tk.X, pady=5)
|
buttons_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
ttk.Button(buttons_frame, text="✅ Проверить и сохранить",
|
ttk.Button(buttons_frame, text="🧭 Мастер настройки",
|
||||||
|
command=self.open_wizard).pack(side=tk.LEFT, padx=5)
|
||||||
|
ttk.Button(buttons_frame, text="💾 Сохранить",
|
||||||
command=self.check_and_save).pack(side=tk.LEFT, padx=5)
|
command=self.check_and_save).pack(side=tk.LEFT, padx=5)
|
||||||
ttk.Button(buttons_frame, text="▶ Запустить копирование сейчас",
|
ttk.Button(buttons_frame, text="▶ Запустить сейчас",
|
||||||
command=self.start_manual_copy).pack(side=tk.LEFT, padx=5)
|
command=self.start_manual_copy).pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
# Фрейм для списка путей
|
paths_frame = ttk.LabelFrame(settings_tab, text="Пары копирования", padding="10")
|
||||||
paths_frame = ttk.LabelFrame(main_frame, text="Пути для копирования", padding="10")
|
paths_frame.pack(fill=tk.BOTH, expand=True, pady=5)
|
||||||
paths_frame.pack(fill=tk.BOTH, expand=True, pady=10)
|
|
||||||
|
|
||||||
# Заголовки колонок
|
self.pairs_tree = ttk.Treeview(
|
||||||
headers_frame = ttk.Frame(paths_frame)
|
paths_frame,
|
||||||
headers_frame.pack(fill=tk.X, pady=5)
|
columns=("source", "dest", "mode", "status"),
|
||||||
|
show="headings",
|
||||||
ttk.Label(headers_frame, text="Откуда копировать (папка с бекапами)",
|
selectmode="browse"
|
||||||
font=("Arial", 10, "bold")).pack(side=tk.LEFT, padx=(0, 20))
|
|
||||||
ttk.Label(headers_frame, text="Куда копировать (сетевая папка)",
|
|
||||||
font=("Arial", 10, "bold")).pack(side=tk.LEFT, padx=(20, 0))
|
|
||||||
|
|
||||||
# Canvas для прокрутки списка
|
|
||||||
canvas = tk.Canvas(paths_frame, borderwidth=0, highlightthickness=0)
|
|
||||||
scrollbar = ttk.Scrollbar(paths_frame, orient="vertical", command=canvas.yview)
|
|
||||||
self.scrollable_frame = ttk.Frame(canvas)
|
|
||||||
|
|
||||||
self.scrollable_frame.bind(
|
|
||||||
"<Configure>",
|
|
||||||
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
|
|
||||||
)
|
)
|
||||||
|
self.pairs_tree.heading("source", text="Источник")
|
||||||
|
self.pairs_tree.heading("dest", text="Назначение")
|
||||||
|
self.pairs_tree.heading("mode", text="Режим")
|
||||||
|
self.pairs_tree.heading("status", text="Статус")
|
||||||
|
self.pairs_tree.column("source", width=280)
|
||||||
|
self.pairs_tree.column("dest", width=280)
|
||||||
|
self.pairs_tree.column("mode", width=110, anchor=tk.CENTER)
|
||||||
|
self.pairs_tree.column("status", width=120, anchor=tk.CENTER)
|
||||||
|
|
||||||
canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
|
tree_scroll = ttk.Scrollbar(paths_frame, orient="vertical", command=self.pairs_tree.yview)
|
||||||
canvas.configure(yscrollcommand=scrollbar.set)
|
self.pairs_tree.configure(yscrollcommand=tree_scroll.set)
|
||||||
|
|
||||||
canvas.pack(side="left", fill="both", expand=True)
|
self.pairs_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
scrollbar.pack(side="right", fill="y")
|
tree_scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
|
||||||
# Кнопки управления списком
|
self.pairs_tree.tag_configure("ok", foreground="green")
|
||||||
list_buttons_frame = ttk.Frame(paths_frame)
|
self.pairs_tree.tag_configure("warn", foreground="orange")
|
||||||
list_buttons_frame.pack(fill=tk.X, pady=10)
|
self.pairs_tree.tag_configure("error", foreground="red")
|
||||||
|
self.pairs_tree.tag_configure("idle", foreground="gray")
|
||||||
|
|
||||||
ttk.Button(list_buttons_frame, text="➕ Добавить пару папок",
|
list_buttons_frame = ttk.Frame(settings_tab)
|
||||||
|
list_buttons_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
|
ttk.Button(list_buttons_frame, text="➕ Добавить",
|
||||||
command=self.add_path_pair).pack(side=tk.LEFT, padx=5)
|
command=self.add_path_pair).pack(side=tk.LEFT, padx=5)
|
||||||
ttk.Button(list_buttons_frame, text="❌ Удалить все",
|
ttk.Button(list_buttons_frame, text="❌ Удалить",
|
||||||
command=self.remove_all_pairs).pack(side=tk.LEFT, padx=5)
|
command=self.remove_selected_pair).pack(side=tk.LEFT, padx=5)
|
||||||
ttk.Button(list_buttons_frame, text="🔍 Проверить пути",
|
ttk.Button(list_buttons_frame, text="📂 Открыть назначение",
|
||||||
command=self.check_paths).pack(side=tk.LEFT, padx=5)
|
command=self.open_selected_destination).pack(side=tk.LEFT, padx=5)
|
||||||
|
ttk.Button(list_buttons_frame, text="🔁 Переключить режим",
|
||||||
|
command=self.toggle_selected_mode).pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
# Лог операций
|
edit_frame = ttk.LabelFrame(settings_tab, text="Редактирование выбранной пары", padding="10")
|
||||||
log_frame = ttk.LabelFrame(main_frame, text="Лог операций", padding="5")
|
edit_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
|
ttk.Label(edit_frame, text="Источник:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=2)
|
||||||
|
ttk.Label(edit_frame, text="Назначение:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=2)
|
||||||
|
|
||||||
|
self.edit_source = ttk.Entry(edit_frame)
|
||||||
|
self.edit_dest = ttk.Entry(edit_frame)
|
||||||
|
self.edit_source.grid(row=0, column=1, sticky=tk.EW, padx=5, pady=2)
|
||||||
|
self.edit_dest.grid(row=1, column=1, sticky=tk.EW, padx=5, pady=2)
|
||||||
|
|
||||||
|
ttk.Button(edit_frame, text="📁", width=3,
|
||||||
|
command=lambda: self.browse_folder(self.edit_source)).grid(row=0, column=2, padx=3)
|
||||||
|
ttk.Button(edit_frame, text="📁", width=3,
|
||||||
|
command=lambda: self.browse_folder(self.edit_dest)).grid(row=1, column=2, padx=3)
|
||||||
|
|
||||||
|
self.edit_full_copy = tk.BooleanVar(value=False)
|
||||||
|
ttk.Checkbutton(edit_frame, text="Вся папка", variable=self.edit_full_copy).grid(
|
||||||
|
row=0, column=3, rowspan=2, padx=10
|
||||||
|
)
|
||||||
|
ttk.Button(edit_frame, text="?", width=2,
|
||||||
|
command=lambda: self.show_hint("Вся папка",
|
||||||
|
"Копирует всю папку и подпапки, а не только последний файл.")).grid(row=0, column=4, rowspan=2, padx=3)
|
||||||
|
|
||||||
|
edit_frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
self.edit_source.bind("<KeyRelease>", lambda _e: self.update_selected_pair_from_edit())
|
||||||
|
self.edit_dest.bind("<KeyRelease>", lambda _e: self.update_selected_pair_from_edit())
|
||||||
|
self.edit_full_copy.trace_add("write", lambda *_: self.update_selected_pair_from_edit())
|
||||||
|
self.pairs_tree.bind("<<TreeviewSelect>>", lambda _e: self.on_pair_select())
|
||||||
|
|
||||||
|
log_frame = ttk.LabelFrame(log_tab, text="Лог операций", padding="5")
|
||||||
log_frame.pack(fill=tk.BOTH, expand=True, pady=5)
|
log_frame.pack(fill=tk.BOTH, expand=True, pady=5)
|
||||||
|
|
||||||
# Кнопки управления логом
|
|
||||||
log_buttons = ttk.Frame(log_frame)
|
log_buttons = ttk.Frame(log_frame)
|
||||||
log_buttons.pack(fill=tk.X, pady=2)
|
log_buttons.pack(fill=tk.X, pady=2)
|
||||||
|
|
||||||
@@ -340,17 +389,23 @@ class BackgroundFileCopyApp:
|
|||||||
ttk.Button(log_buttons, text="🔍 Отладка",
|
ttk.Button(log_buttons, text="🔍 Отладка",
|
||||||
command=self.debug_settings).pack(side=tk.RIGHT, padx=2)
|
command=self.debug_settings).pack(side=tk.RIGHT, padx=2)
|
||||||
|
|
||||||
# Текст лога
|
|
||||||
self.log_text = scrolledtext.ScrolledText(log_frame, height=8, wrap=tk.WORD)
|
self.log_text = scrolledtext.ScrolledText(log_frame, height=8, wrap=tk.WORD)
|
||||||
self.log_text.pack(fill=tk.BOTH, expand=True)
|
self.log_text.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
# Настройка тегов для цветного лога
|
|
||||||
self.log_text.tag_configure("success", foreground="green")
|
self.log_text.tag_configure("success", foreground="green")
|
||||||
self.log_text.tag_configure("error", foreground="red")
|
self.log_text.tag_configure("error", foreground="red")
|
||||||
self.log_text.tag_configure("warning", foreground="orange")
|
self.log_text.tag_configure("warning", foreground="orange")
|
||||||
self.log_text.tag_configure("info", foreground="blue")
|
self.log_text.tag_configure("info", foreground="blue")
|
||||||
|
|
||||||
# Статус бар
|
help_text = (
|
||||||
|
"Быстрый старт:\n"
|
||||||
|
"1) Нажмите 'Добавить' и заполните источник/назначение.\n"
|
||||||
|
"2) Выберите режим: последний файл или вся папка.\n"
|
||||||
|
"3) Настройте расписание и нажмите 'Сохранить'.\n\n"
|
||||||
|
"Подсказки доступны по кнопке '?'."
|
||||||
|
)
|
||||||
|
ttk.Label(help_tab, text=help_text, justify=tk.LEFT).pack(anchor=tk.W, padx=10, pady=10)
|
||||||
|
|
||||||
self.status_bar = ttk.Label(self.root, text="Готов к работе",
|
self.status_bar = ttk.Label(self.root, text="Готов к работе",
|
||||||
relief=tk.SUNKEN, anchor=tk.W)
|
relief=tk.SUNKEN, anchor=tk.W)
|
||||||
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
|
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
|
||||||
@@ -499,8 +554,7 @@ class BackgroundFileCopyApp:
|
|||||||
else:
|
else:
|
||||||
source, dest = item
|
source, dest = item
|
||||||
full_copy = False
|
full_copy = False
|
||||||
self.add_path_pair(source, dest)
|
self.add_path_pair(source, dest, full_copy)
|
||||||
self.copy_pairs[-1]['full_copy'].set(full_copy)
|
|
||||||
self.log_message(f"✅ Загружено {len(pairs_data)} пар путей", "success")
|
self.log_message(f"✅ Загружено {len(pairs_data)} пар путей", "success")
|
||||||
else:
|
else:
|
||||||
# Если нет сохраненных пар, добавляем одну пустую
|
# Если нет сохраненных пар, добавляем одну пустую
|
||||||
@@ -519,8 +573,10 @@ class BackgroundFileCopyApp:
|
|||||||
|
|
||||||
self.verify_only.set(settings.get('verify_only', False))
|
self.verify_only.set(settings.get('verify_only', False))
|
||||||
self.last_success_time = settings.get('last_success_time')
|
self.last_success_time = settings.get('last_success_time')
|
||||||
|
self.last_result_summary = settings.get('last_result_summary')
|
||||||
self.last_hashes = settings.get('last_hashes', {})
|
self.last_hashes = settings.get('last_hashes', {})
|
||||||
self.update_last_success_label()
|
self.update_last_success_label()
|
||||||
|
self.update_status_summary()
|
||||||
|
|
||||||
self.log_message(f"📂 Настройки загружены из {settings_path}", "info")
|
self.log_message(f"📂 Настройки загружены из {settings_path}", "info")
|
||||||
else:
|
else:
|
||||||
@@ -530,8 +586,10 @@ class BackgroundFileCopyApp:
|
|||||||
self.autostart_enabled.set(self.is_autostart_enabled())
|
self.autostart_enabled.set(self.is_autostart_enabled())
|
||||||
self.verify_only.set(False)
|
self.verify_only.set(False)
|
||||||
self.last_success_time = None
|
self.last_success_time = None
|
||||||
|
self.last_result_summary = None
|
||||||
self.last_hashes = {}
|
self.last_hashes = {}
|
||||||
self.update_last_success_label()
|
self.update_last_success_label()
|
||||||
|
self.update_status_summary()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_message(f"❌ Ошибка при загрузке настроек: {e}", "error")
|
self.log_message(f"❌ Ошибка при загрузке настроек: {e}", "error")
|
||||||
@@ -542,8 +600,10 @@ class BackgroundFileCopyApp:
|
|||||||
self.autostart_enabled.set(self.is_autostart_enabled())
|
self.autostart_enabled.set(self.is_autostart_enabled())
|
||||||
self.verify_only.set(False)
|
self.verify_only.set(False)
|
||||||
self.last_success_time = None
|
self.last_success_time = None
|
||||||
|
self.last_result_summary = None
|
||||||
self.last_hashes = {}
|
self.last_hashes = {}
|
||||||
self.update_last_success_label()
|
self.update_last_success_label()
|
||||||
|
self.update_status_summary()
|
||||||
|
|
||||||
def save_settings(self):
|
def save_settings(self):
|
||||||
"""Сохраняет настройки в файл"""
|
"""Сохраняет настройки в файл"""
|
||||||
@@ -551,17 +611,13 @@ class BackgroundFileCopyApp:
|
|||||||
# Собираем данные из полей ввода
|
# Собираем данные из полей ввода
|
||||||
pairs_data = []
|
pairs_data = []
|
||||||
for pair in self.copy_pairs:
|
for pair in self.copy_pairs:
|
||||||
source = pair['source'].get().strip()
|
source = pair.get("source", "").strip()
|
||||||
dest = pair['dest'].get().strip()
|
dest = pair.get("dest", "").strip()
|
||||||
if source.startswith("Например: "):
|
|
||||||
source = ""
|
|
||||||
if dest.startswith("Например: "):
|
|
||||||
dest = ""
|
|
||||||
if source or dest: # Сохраняем даже если одно поле пустое
|
if source or dest: # Сохраняем даже если одно поле пустое
|
||||||
pairs_data.append({
|
pairs_data.append({
|
||||||
"source": source,
|
"source": source,
|
||||||
"dest": dest,
|
"dest": dest,
|
||||||
"full_copy": pair['full_copy'].get()
|
"full_copy": bool(pair.get("full_copy", False))
|
||||||
})
|
})
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
@@ -572,6 +628,7 @@ class BackgroundFileCopyApp:
|
|||||||
'minimize_to_tray': self.minimize_to_tray_enabled.get(),
|
'minimize_to_tray': self.minimize_to_tray_enabled.get(),
|
||||||
'verify_only': self.verify_only.get(),
|
'verify_only': self.verify_only.get(),
|
||||||
'last_success_time': self.last_success_time,
|
'last_success_time': self.last_success_time,
|
||||||
|
'last_result_summary': self.last_result_summary,
|
||||||
'last_hashes': self.last_hashes,
|
'last_hashes': self.last_hashes,
|
||||||
'pairs': pairs_data
|
'pairs': pairs_data
|
||||||
}
|
}
|
||||||
@@ -594,88 +651,141 @@ class BackgroundFileCopyApp:
|
|||||||
self.log_message(f"❌ Ошибка при сохранении настроек: {e}", "error")
|
self.log_message(f"❌ Ошибка при сохранении настроек: {e}", "error")
|
||||||
messagebox.showerror("Ошибка", f"Не удалось сохранить настройки:\n{e}")
|
messagebox.showerror("Ошибка", f"Не удалось сохранить настройки:\n{e}")
|
||||||
|
|
||||||
def add_path_pair(self, source="", dest=""):
|
def add_path_pair(self, source="", dest="", full_copy: bool = False):
|
||||||
"""Добавляет новую пару полей для ввода путей"""
|
"""Добавляет новую пару в таблицу."""
|
||||||
pair_frame = ttk.Frame(self.scrollable_frame)
|
pair_id = f"pair_{self.pair_counter}"
|
||||||
pair_frame.pack(fill=tk.X, pady=5)
|
self.pair_counter += 1
|
||||||
|
|
||||||
# Поле "Откуда"
|
pair = {
|
||||||
source_frame = ttk.Frame(pair_frame)
|
"id": pair_id,
|
||||||
source_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
|
"source": source,
|
||||||
|
"dest": dest,
|
||||||
|
"full_copy": bool(full_copy),
|
||||||
|
"status": "?",
|
||||||
|
"status_tag": "idle"
|
||||||
|
}
|
||||||
|
if source or dest:
|
||||||
|
status_text, status_tag = self.validate_pair(source, dest)
|
||||||
|
pair["status"] = status_text
|
||||||
|
pair["status_tag"] = status_tag
|
||||||
|
self.copy_pairs.append(pair)
|
||||||
|
|
||||||
source_entry = ttk.Entry(source_frame)
|
mode_text = "Вся папка" if pair["full_copy"] else "Последний файл"
|
||||||
source_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
self.pairs_tree.insert("", "end", iid=pair_id,
|
||||||
if source:
|
values=(source, dest, mode_text, pair["status"]),
|
||||||
source_entry.insert(0, source)
|
tags=(pair["status_tag"],))
|
||||||
else:
|
self.pairs_tree.selection_set(pair_id)
|
||||||
self.create_placeholder(source_entry, "Например: C:\\Backups")
|
self.on_pair_select()
|
||||||
|
|
||||||
ttk.Button(source_frame, text="📁", width=3,
|
def remove_selected_pair(self):
|
||||||
command=lambda: self.browse_folder(source_entry)).pack(side=tk.RIGHT, padx=2)
|
pair_id = self.selected_pair_id
|
||||||
|
if not pair_id:
|
||||||
|
return
|
||||||
|
self.copy_pairs = [p for p in self.copy_pairs if p["id"] != pair_id]
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
self.pairs_tree.delete(pair_id)
|
||||||
|
self.selected_pair_id = None
|
||||||
|
self.edit_source.delete(0, tk.END)
|
||||||
|
self.edit_dest.delete(0, tk.END)
|
||||||
|
self.edit_full_copy.set(False)
|
||||||
|
|
||||||
# Поле "Куда"
|
def remove_path_pair(self, pair_id):
|
||||||
dest_frame = ttk.Frame(pair_frame)
|
"""Совместимость: удалить пару по id."""
|
||||||
dest_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(5, 0))
|
if pair_id:
|
||||||
|
self.selected_pair_id = pair_id
|
||||||
dest_entry = ttk.Entry(dest_frame)
|
self.remove_selected_pair()
|
||||||
dest_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
||||||
if dest:
|
|
||||||
dest_entry.insert(0, dest)
|
|
||||||
else:
|
|
||||||
self.create_placeholder(dest_entry, "Например: D:\\BackupArchive")
|
|
||||||
|
|
||||||
ttk.Button(dest_frame, text="📁", width=3,
|
|
||||||
command=lambda: self.browse_folder(dest_entry)).pack(side=tk.RIGHT, padx=2)
|
|
||||||
|
|
||||||
# Опции пары
|
|
||||||
options_frame = ttk.Frame(pair_frame)
|
|
||||||
options_frame.pack(side=tk.LEFT, padx=5)
|
|
||||||
|
|
||||||
full_copy_var = tk.BooleanVar(value=False)
|
|
||||||
full_copy_check = ttk.Checkbutton(options_frame, text="Вся папка", variable=full_copy_var)
|
|
||||||
full_copy_check.pack(side=tk.LEFT)
|
|
||||||
|
|
||||||
ttk.Button(options_frame, text="📂", width=3,
|
|
||||||
command=lambda: self.open_destination(dest_entry.get())).pack(side=tk.LEFT, padx=2)
|
|
||||||
|
|
||||||
# Кнопка удаления
|
|
||||||
ttk.Button(pair_frame, text="✖", width=3,
|
|
||||||
command=lambda: self.remove_path_pair(pair_frame)).pack(side=tk.RIGHT, padx=5)
|
|
||||||
|
|
||||||
# Сохраняем ссылки на entry
|
|
||||||
self.copy_pairs.append({
|
|
||||||
'frame': pair_frame,
|
|
||||||
'source': source_entry,
|
|
||||||
'dest': dest_entry,
|
|
||||||
'full_copy': full_copy_var
|
|
||||||
})
|
|
||||||
|
|
||||||
# Прокручиваем к новому элементу
|
|
||||||
self.scrollable_frame.update_idletasks()
|
|
||||||
canvas = self.scrollable_frame.master
|
|
||||||
canvas.yview_moveto(1.0)
|
|
||||||
|
|
||||||
def remove_path_pair(self, frame):
|
|
||||||
"""Удаляет пару полей"""
|
|
||||||
for pair in self.copy_pairs:
|
|
||||||
if pair['frame'] == frame:
|
|
||||||
self.copy_pairs.remove(pair)
|
|
||||||
break
|
|
||||||
frame.destroy()
|
|
||||||
|
|
||||||
# Если не осталось пар, добавляем пустую
|
|
||||||
if not self.copy_pairs:
|
|
||||||
self.add_path_pair()
|
|
||||||
|
|
||||||
def remove_all_pairs(self, silent=False):
|
def remove_all_pairs(self, silent=False):
|
||||||
"""Удаляет все пары"""
|
"""Удаляет все пары"""
|
||||||
if not silent:
|
if not silent:
|
||||||
if not messagebox.askyesno("Подтверждение", "Удалить все пути?"):
|
if not messagebox.askyesno("Подтверждение", "Удалить все пути?"):
|
||||||
return
|
return
|
||||||
|
|
||||||
for pair in self.copy_pairs[:]:
|
|
||||||
pair['frame'].destroy()
|
|
||||||
self.copy_pairs.clear()
|
self.copy_pairs.clear()
|
||||||
|
for item in self.pairs_tree.get_children():
|
||||||
|
self.pairs_tree.delete(item)
|
||||||
|
self.selected_pair_id = None
|
||||||
|
|
||||||
|
def get_pair_by_id(self, pair_id: str):
|
||||||
|
for pair in self.copy_pairs:
|
||||||
|
if pair["id"] == pair_id:
|
||||||
|
return pair
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update_pair_row(self, pair):
|
||||||
|
mode_text = "Вся папка" if pair["full_copy"] else "Последний файл"
|
||||||
|
self.pairs_tree.item(pair["id"], values=(pair["source"], pair["dest"], mode_text, pair["status"]),
|
||||||
|
tags=(pair["status_tag"],))
|
||||||
|
|
||||||
|
def on_pair_select(self):
|
||||||
|
selection = self.pairs_tree.selection()
|
||||||
|
if not selection:
|
||||||
|
return
|
||||||
|
pair_id = selection[0]
|
||||||
|
self.selected_pair_id = pair_id
|
||||||
|
pair = self.get_pair_by_id(pair_id)
|
||||||
|
if not pair:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.edit_source.delete(0, tk.END)
|
||||||
|
self.edit_dest.delete(0, tk.END)
|
||||||
|
self.edit_source.insert(0, pair["source"])
|
||||||
|
self.edit_dest.insert(0, pair["dest"])
|
||||||
|
self.edit_full_copy.set(pair["full_copy"])
|
||||||
|
|
||||||
|
def update_selected_pair_from_edit(self):
|
||||||
|
pair_id = self.selected_pair_id
|
||||||
|
if not pair_id:
|
||||||
|
return
|
||||||
|
pair = self.get_pair_by_id(pair_id)
|
||||||
|
if not pair:
|
||||||
|
return
|
||||||
|
pair["source"] = self.edit_source.get().strip()
|
||||||
|
pair["dest"] = self.edit_dest.get().strip()
|
||||||
|
pair["full_copy"] = bool(self.edit_full_copy.get())
|
||||||
|
status_text, status_tag = self.validate_pair(pair["source"], pair["dest"])
|
||||||
|
pair["status"] = status_text
|
||||||
|
pair["status_tag"] = status_tag
|
||||||
|
self.update_pair_row(pair)
|
||||||
|
|
||||||
|
def validate_pair(self, source: str, dest: str):
|
||||||
|
if not source or not dest:
|
||||||
|
return "?", "idle"
|
||||||
|
|
||||||
|
if not os.path.exists(source):
|
||||||
|
return "Нет источника", "error"
|
||||||
|
|
||||||
|
if not os.path.exists(dest):
|
||||||
|
return "Нет назначения", "error"
|
||||||
|
|
||||||
|
test_file = os.path.join(dest, 'test_write.tmp')
|
||||||
|
try:
|
||||||
|
with open(test_file, 'w') as f:
|
||||||
|
f.write('test')
|
||||||
|
os.remove(test_file)
|
||||||
|
except Exception:
|
||||||
|
return "Нет доступа", "error"
|
||||||
|
|
||||||
|
return "OK", "ok"
|
||||||
|
|
||||||
|
def open_selected_destination(self):
|
||||||
|
pair_id = self.selected_pair_id
|
||||||
|
if not pair_id:
|
||||||
|
return
|
||||||
|
pair = self.get_pair_by_id(pair_id)
|
||||||
|
if not pair:
|
||||||
|
return
|
||||||
|
self.open_destination(pair["dest"])
|
||||||
|
|
||||||
|
def toggle_selected_mode(self):
|
||||||
|
pair_id = self.selected_pair_id
|
||||||
|
if not pair_id:
|
||||||
|
return
|
||||||
|
pair = self.get_pair_by_id(pair_id)
|
||||||
|
if not pair:
|
||||||
|
return
|
||||||
|
pair["full_copy"] = not pair["full_copy"]
|
||||||
|
self.edit_full_copy.set(pair["full_copy"])
|
||||||
|
self.update_pair_row(pair)
|
||||||
|
|
||||||
def browse_folder(self, entry):
|
def browse_folder(self, entry):
|
||||||
"""Открывает диалог выбора папки"""
|
"""Открывает диалог выбора папки"""
|
||||||
@@ -691,18 +801,122 @@ class BackgroundFileCopyApp:
|
|||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
os.startfile(path)
|
os.startfile(path)
|
||||||
|
|
||||||
|
def show_hint(self, title: str, message: str):
|
||||||
|
messagebox.showinfo(title, message)
|
||||||
|
|
||||||
|
def open_wizard(self):
|
||||||
|
if getattr(self, "wizard_window", None) is not None:
|
||||||
|
try:
|
||||||
|
self.wizard_window.lift()
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
wiz = tk.Toplevel(self.root)
|
||||||
|
wiz.title("Мастер настройки")
|
||||||
|
wiz.geometry("520x320")
|
||||||
|
wiz.transient(self.root)
|
||||||
|
wiz.grab_set()
|
||||||
|
self.wizard_window = wiz
|
||||||
|
|
||||||
|
step_var = tk.IntVar(value=0)
|
||||||
|
source_var = tk.StringVar()
|
||||||
|
dest_var = tk.StringVar()
|
||||||
|
full_copy_var = tk.BooleanVar(value=False)
|
||||||
|
schedule_enabled_var = tk.BooleanVar(value=self.scheduler_enabled.get())
|
||||||
|
hour_var = tk.StringVar(value=self.hour_var.get())
|
||||||
|
minute_var = tk.StringVar(value=self.minute_var.get())
|
||||||
|
autostart_var = tk.BooleanVar(value=self.autostart_enabled.get())
|
||||||
|
minimize_var = tk.BooleanVar(value=self.minimize_to_tray_enabled.get())
|
||||||
|
verify_var = tk.BooleanVar(value=self.verify_only.get())
|
||||||
|
|
||||||
|
def show_step(index: int):
|
||||||
|
step_var.set(index)
|
||||||
|
for i, frame in enumerate(steps):
|
||||||
|
frame.pack_forget()
|
||||||
|
if i == index:
|
||||||
|
frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
back_btn.config(state=tk.NORMAL if index > 0 else tk.DISABLED)
|
||||||
|
next_btn.config(state=tk.NORMAL if index < len(steps) - 1 else tk.DISABLED)
|
||||||
|
finish_btn.config(state=tk.NORMAL if index == len(steps) - 1 else tk.DISABLED)
|
||||||
|
|
||||||
|
steps = []
|
||||||
|
|
||||||
|
step1 = ttk.Frame(wiz)
|
||||||
|
ttk.Label(step1, text="Шаг 1/3: Пути", font=("Arial", 11, "bold")).pack(anchor=tk.W, pady=5)
|
||||||
|
ttk.Label(step1, text="Источник:").pack(anchor=tk.W)
|
||||||
|
src_entry = ttk.Entry(step1, textvariable=source_var)
|
||||||
|
src_entry.pack(fill=tk.X, pady=2)
|
||||||
|
ttk.Button(step1, text="Выбрать...", command=lambda: self.browse_folder(src_entry)).pack(anchor=tk.W, pady=2)
|
||||||
|
ttk.Label(step1, text="Назначение:").pack(anchor=tk.W, pady=5)
|
||||||
|
dst_entry = ttk.Entry(step1, textvariable=dest_var)
|
||||||
|
dst_entry.pack(fill=tk.X, pady=2)
|
||||||
|
ttk.Button(step1, text="Выбрать...", command=lambda: self.browse_folder(dst_entry)).pack(anchor=tk.W, pady=2)
|
||||||
|
steps.append(step1)
|
||||||
|
|
||||||
|
step2 = ttk.Frame(wiz)
|
||||||
|
ttk.Label(step2, text="Шаг 2/3: Режим копирования", font=("Arial", 11, "bold")).pack(anchor=tk.W, pady=5)
|
||||||
|
ttk.Checkbutton(step2, text="Копировать всю папку с подпапками", variable=full_copy_var).pack(anchor=tk.W)
|
||||||
|
steps.append(step2)
|
||||||
|
|
||||||
|
step3 = ttk.Frame(wiz)
|
||||||
|
ttk.Label(step3, text="Шаг 3/3: Расписание и поведение", font=("Arial", 11, "bold")).pack(anchor=tk.W, pady=5)
|
||||||
|
time_frame = ttk.Frame(step3)
|
||||||
|
time_frame.pack(fill=tk.X, pady=4)
|
||||||
|
ttk.Label(time_frame, text="Время:").pack(side=tk.LEFT)
|
||||||
|
ttk.Combobox(time_frame, textvariable=hour_var, values=[f"{h:02d}" for h in range(24)], width=5, state="readonly").pack(side=tk.LEFT, padx=2)
|
||||||
|
ttk.Label(time_frame, text=":").pack(side=tk.LEFT)
|
||||||
|
ttk.Combobox(time_frame, textvariable=minute_var, values=[f"{m:02d}" for m in range(60)], width=5, state="readonly").pack(side=tk.LEFT, padx=2)
|
||||||
|
ttk.Checkbutton(step3, text="Ежедневно", variable=schedule_enabled_var).pack(anchor=tk.W, pady=4)
|
||||||
|
ttk.Checkbutton(step3, text="Автозапуск Windows", variable=autostart_var).pack(anchor=tk.W)
|
||||||
|
ttk.Checkbutton(step3, text="Сворачивать в трей при закрытии", variable=minimize_var).pack(anchor=tk.W)
|
||||||
|
ttk.Checkbutton(step3, text="Только проверка (без копирования)", variable=verify_var).pack(anchor=tk.W)
|
||||||
|
steps.append(step3)
|
||||||
|
|
||||||
|
controls = ttk.Frame(wiz)
|
||||||
|
controls.pack(fill=tk.X, pady=5)
|
||||||
|
back_btn = ttk.Button(controls, text="Назад", command=lambda: show_step(step_var.get() - 1))
|
||||||
|
next_btn = ttk.Button(controls, text="Далее", command=lambda: show_step(step_var.get() + 1))
|
||||||
|
|
||||||
|
def finish():
|
||||||
|
if source_var.get().strip() and dest_var.get().strip():
|
||||||
|
self.add_path_pair(source_var.get().strip(), dest_var.get().strip(), full_copy_var.get())
|
||||||
|
self.hour_var.set(hour_var.get())
|
||||||
|
self.minute_var.set(minute_var.get())
|
||||||
|
self.scheduler_enabled.set(schedule_enabled_var.get())
|
||||||
|
self.autostart_enabled.set(autostart_var.get())
|
||||||
|
self.minimize_to_tray_enabled.set(minimize_var.get())
|
||||||
|
self.verify_only.set(verify_var.get())
|
||||||
|
if self.scheduler_enabled.get():
|
||||||
|
self.start_scheduler()
|
||||||
|
else:
|
||||||
|
self.stop_scheduler()
|
||||||
|
if self.autostart_enabled.get() != self.is_autostart_enabled():
|
||||||
|
self.set_autostart_enabled(self.autostart_enabled.get())
|
||||||
|
wiz.destroy()
|
||||||
|
self.wizard_window = None
|
||||||
|
|
||||||
|
finish_btn = ttk.Button(controls, text="Готово", command=finish)
|
||||||
|
back_btn.pack(side=tk.LEFT, padx=5)
|
||||||
|
next_btn.pack(side=tk.LEFT, padx=5)
|
||||||
|
finish_btn.pack(side=tk.RIGHT, padx=5)
|
||||||
|
|
||||||
|
def on_close():
|
||||||
|
self.wizard_window = None
|
||||||
|
wiz.destroy()
|
||||||
|
|
||||||
|
wiz.protocol("WM_DELETE_WINDOW", on_close)
|
||||||
|
show_step(0)
|
||||||
|
|
||||||
def get_valid_pairs(self) -> List[tuple]:
|
def get_valid_pairs(self) -> List[tuple]:
|
||||||
"""Возвращает список валидных пар папок"""
|
"""Возвращает список валидных пар папок"""
|
||||||
valid_pairs = []
|
valid_pairs = []
|
||||||
for pair in self.copy_pairs:
|
for pair in self.copy_pairs:
|
||||||
source = pair['source'].get().strip()
|
source = pair.get("source", "").strip()
|
||||||
dest = pair['dest'].get().strip()
|
dest = pair.get("dest", "").strip()
|
||||||
if source.startswith("Например: "):
|
full_copy = bool(pair.get("full_copy", False))
|
||||||
source = ""
|
|
||||||
if dest.startswith("Например: "):
|
|
||||||
dest = ""
|
|
||||||
if source and dest:
|
if source and dest:
|
||||||
valid_pairs.append((source, dest, pair['full_copy'].get()))
|
valid_pairs.append((source, dest, full_copy))
|
||||||
return valid_pairs
|
return valid_pairs
|
||||||
|
|
||||||
def check_paths(self):
|
def check_paths(self):
|
||||||
@@ -719,29 +933,22 @@ class BackgroundFileCopyApp:
|
|||||||
mode_text = "вся папка" if full_copy else "последний файл"
|
mode_text = "вся папка" if full_copy else "последний файл"
|
||||||
self.log_message(f" Режим: {mode_text}", "info")
|
self.log_message(f" Режим: {mode_text}", "info")
|
||||||
|
|
||||||
# Проверяем исходную папку
|
status_text, status_tag = self.validate_pair(source, dest)
|
||||||
|
if status_tag == "ok":
|
||||||
|
self.log_message(f" ✅ Проверка: OK", "success")
|
||||||
|
else:
|
||||||
|
self.log_message(f" ❌ Проверка: {status_text}", "error")
|
||||||
|
|
||||||
if os.path.exists(source):
|
if os.path.exists(source):
|
||||||
self.log_message(f" ✅ Исходная папка: {source}", "success")
|
|
||||||
# Считаем файлы .bak (без дублей из-за регистра)
|
|
||||||
bak_files = [p for p in Path(source).iterdir() if p.is_file() and p.suffix.lower() == '.bak']
|
bak_files = [p for p in Path(source).iterdir() if p.is_file() and p.suffix.lower() == '.bak']
|
||||||
self.log_message(f" Найдено .bak файлов: {len(bak_files)}", "info")
|
self.log_message(f" Найдено .bak файлов: {len(bak_files)}", "info")
|
||||||
else:
|
|
||||||
self.log_message(f" ❌ Исходная папка НЕ существует: {source}", "error")
|
|
||||||
|
|
||||||
# Проверяем целевую папку
|
# Обновляем статусы в таблице
|
||||||
if os.path.exists(dest):
|
for pair in self.copy_pairs:
|
||||||
self.log_message(f" ✅ Целевая папка: {dest}", "success")
|
status_text, status_tag = self.validate_pair(pair.get("source", ""), pair.get("dest", ""))
|
||||||
# Проверяем права на запись
|
pair["status"] = status_text
|
||||||
test_file = os.path.join(dest, 'test_write.tmp')
|
pair["status_tag"] = status_tag
|
||||||
try:
|
self.update_pair_row(pair)
|
||||||
with open(test_file, 'w') as f:
|
|
||||||
f.write('test')
|
|
||||||
os.remove(test_file)
|
|
||||||
self.log_message(f" Права на запись: есть", "success")
|
|
||||||
except:
|
|
||||||
self.log_message(f" ❌ Права на запись: нет", "error")
|
|
||||||
else:
|
|
||||||
self.log_message(f" ❌ Целевая папка НЕ существует: {dest}", "error")
|
|
||||||
|
|
||||||
def find_latest_file(self, folder_path: str) -> Optional[Path]:
|
def find_latest_file(self, folder_path: str) -> Optional[Path]:
|
||||||
"""Находит самый последний файл в папке"""
|
"""Находит самый последний файл в папке"""
|
||||||
@@ -769,6 +976,9 @@ class BackgroundFileCopyApp:
|
|||||||
"""Планирует выполнение функции в UI-потоке."""
|
"""Планирует выполнение функции в UI-потоке."""
|
||||||
self.queue.put({'type': 'ui', 'func': func})
|
self.queue.put({'type': 'ui', 'func': func})
|
||||||
|
|
||||||
|
def set_status_text(self, text: str):
|
||||||
|
self.root.after(0, lambda: self.status_bar.config(text=text))
|
||||||
|
|
||||||
def check_previous_hash(self, target_file: Path):
|
def check_previous_hash(self, target_file: Path):
|
||||||
key = str(target_file)
|
key = str(target_file)
|
||||||
if key in self.last_hashes:
|
if key in self.last_hashes:
|
||||||
@@ -813,6 +1023,7 @@ class BackgroundFileCopyApp:
|
|||||||
src_file = Path(root) / fname
|
src_file = Path(root) / fname
|
||||||
dst_file = dest_root / fname
|
dst_file = dest_root / fname
|
||||||
try:
|
try:
|
||||||
|
self.set_status_text(f"Копируется: {src_file.name}")
|
||||||
self.check_previous_hash(dst_file) if dst_file.exists() else None
|
self.check_previous_hash(dst_file) if dst_file.exists() else None
|
||||||
if self.verify_only.get():
|
if self.verify_only.get():
|
||||||
if self.verify_only_mode(src_file, dst_file):
|
if self.verify_only_mode(src_file, dst_file):
|
||||||
@@ -890,6 +1101,7 @@ class BackgroundFileCopyApp:
|
|||||||
target_file = dest_path / latest_file.name
|
target_file = dest_path / latest_file.name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
self.set_status_text(f"Копируется: {latest_file.name}")
|
||||||
if target_file.exists():
|
if target_file.exists():
|
||||||
self.check_previous_hash(target_file)
|
self.check_previous_hash(target_file)
|
||||||
if self.verify_only.get():
|
if self.verify_only.get():
|
||||||
@@ -935,6 +1147,9 @@ class BackgroundFileCopyApp:
|
|||||||
f"Копирование завершено!\n\n✅ Скопировано: {copied_files}\n⏭️ Пропущено: {skipped_files}\n❌ Ошибок: {error_files}"
|
f"Копирование завершено!\n\n✅ Скопировано: {copied_files}\n⏭️ Пропущено: {skipped_files}\n❌ Ошибок: {error_files}"
|
||||||
))
|
))
|
||||||
|
|
||||||
|
self.last_result_summary = f"✅ {copied_files} / ⏭️ {skipped_files} / ❌ {error_files}"
|
||||||
|
self.run_on_ui(self.update_status_summary)
|
||||||
|
|
||||||
if error_files == 0:
|
if error_files == 0:
|
||||||
self.last_success_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
self.last_success_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
self.run_on_ui(self.update_last_success_label)
|
self.run_on_ui(self.update_last_success_label)
|
||||||
@@ -1015,6 +1230,11 @@ class BackgroundFileCopyApp:
|
|||||||
else:
|
else:
|
||||||
self.last_success_label.config(text="(последний успех: —)")
|
self.last_success_label.config(text="(последний успех: —)")
|
||||||
|
|
||||||
|
def update_status_summary(self):
|
||||||
|
summary = self.last_result_summary or "—"
|
||||||
|
self.status_summary_label.config(text=summary)
|
||||||
|
self.last_result_label.config(text=f"(последний результат: {summary})")
|
||||||
|
|
||||||
def debug_settings(self):
|
def debug_settings(self):
|
||||||
"""Отладочный метод для проверки загрузки настроек"""
|
"""Отладочный метод для проверки загрузки настроек"""
|
||||||
self.log_message("\n" + "=" * 50, "info")
|
self.log_message("\n" + "=" * 50, "info")
|
||||||
@@ -1039,11 +1259,13 @@ class BackgroundFileCopyApp:
|
|||||||
self.log_message(f" Пар в интерфейсе: {len(self.copy_pairs)}", "info")
|
self.log_message(f" Пар в интерфейсе: {len(self.copy_pairs)}", "info")
|
||||||
|
|
||||||
for i, pair in enumerate(self.copy_pairs, 1):
|
for i, pair in enumerate(self.copy_pairs, 1):
|
||||||
source = pair['source'].get()
|
source = pair.get("source", "")
|
||||||
dest = pair['dest'].get()
|
dest = pair.get("dest", "")
|
||||||
|
mode = "вся папка" if pair.get("full_copy", False) else "последний файл"
|
||||||
self.log_message(f" Пара {i}:", "info")
|
self.log_message(f" Пара {i}:", "info")
|
||||||
self.log_message(f" Откуда: '{source}'", "info")
|
self.log_message(f" Откуда: '{source}'", "info")
|
||||||
self.log_message(f" Куда: '{dest}'", "info")
|
self.log_message(f" Куда: '{dest}'", "info")
|
||||||
|
self.log_message(f" Режим: {mode}", "info")
|
||||||
|
|
||||||
self.log_message("=" * 50, "info")
|
self.log_message("=" * 50, "info")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user