Improve UI and settings; ignore caches

This commit is contained in:
2026-02-19 19:37:57 +03:00
parent 36708967f7
commit 8656c4dac9
2 changed files with 418 additions and 190 deletions

6
.gitignore vendored
View File

@@ -2,3 +2,9 @@
/dist/
/build/
/.venv/
/.idea/
/.pytest_cache/
/__pycache__/
**/__pycache__/
*.pyc
*.log

602
main.py
View File

@@ -140,7 +140,7 @@ class BackgroundFileCopyApp:
def __init__(self, root):
self.root = root
self.root.title("Планировщик копирования бекапов")
self.root.geometry("900x700")
self.root.geometry("900x800")
self.setup_window_icon()
# Для работы с очередью сообщений из потоков
@@ -148,6 +148,8 @@ class BackgroundFileCopyApp:
# Список пар для копирования
self.copy_pairs = []
self.pair_counter = 0
self.selected_pair_id = None
# Планировщик
self.scheduler = FileCopyScheduler(self)
@@ -169,6 +171,7 @@ class BackgroundFileCopyApp:
# Последний успешный запуск
self.last_success_time: Optional[str] = None
self.last_result_summary: Optional[str] = None
# Хеши последних копий
self.last_hashes: Dict[str, str] = {}
@@ -190,24 +193,48 @@ class BackgroundFileCopyApp:
self.start_scheduler()
def setup_ui(self):
# Основной фрейм
main_frame = ttk.Frame(self.root, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
# Заголовок
title_label = ttk.Label(main_frame, text="Планировщик копирования бекапов",
notebook = ttk.Notebook(main_frame)
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"))
title_label.pack(pady=10)
title_label.pack(pady=8)
# Фрейм настроек расписания
schedule_frame = ttk.LabelFrame(main_frame, text="Настройки расписания", padding="10")
schedule_frame.pack(fill=tk.X, pady=10)
status_frame = ttk.LabelFrame(settings_tab, text="Состояние", padding="10")
status_frame.pack(fill=tk.X, pady=5)
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.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.minute_var = tk.StringVar(value="00")
@@ -221,117 +248,139 @@ class BackgroundFileCopyApp:
ttk.Combobox(time_frame, textvariable=self.minute_var, values=minutes,
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_check = ttk.Checkbutton(scheduler_ctrl_frame,
text="Включить автоматическое копирование по расписанию",
self.scheduler_check = ttk.Checkbutton(time_frame,
text="Ежедневно",
variable=self.scheduler_enabled,
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"))
self.scheduler_status.pack(side=tk.LEFT, padx=10)
self.next_run_label = ttk.Label(scheduler_ctrl_frame, text="(следующий запуск: —)",
font=("Arial", 9, "italic"))
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)
options_frame = ttk.Frame(schedule_frame)
options_frame.pack(fill=tk.X, pady=5)
self.autostart_check = ttk.Checkbutton(
autostart_frame,
text="Добавить программу в автозапуск Windows",
options_frame,
text="Автозапуск Windows",
variable=self.autostart_enabled,
command=self.toggle_autostart
)
self.autostart_check.pack(side=tk.LEFT)
minimize_frame = ttk.Frame(schedule_frame)
minimize_frame.pack(fill=tk.X, pady=5)
ttk.Button(options_frame, text="?", width=2,
command=lambda: self.show_hint("Автозапуск",
"Добавляет программу в автозапуск Windows.")).pack(side=tk.LEFT, padx=4)
self.minimize_check = ttk.Checkbutton(
minimize_frame,
options_frame,
text="Сворачивать в трей при закрытии",
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)
verify_frame.pack(fill=tk.X, pady=5)
ttk.Button(options_frame, text="?", width=2,
command=lambda: self.show_hint("Сворачивание",
"По крестику окно скрывается в трей.")).pack(side=tk.LEFT, padx=4)
self.verify_check = ttk.Checkbutton(
verify_frame,
options_frame,
text="Только проверка (без копирования)",
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.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)
ttk.Button(buttons_frame, text="▶ Запустить копирование сейчас",
ttk.Button(buttons_frame, text="▶ Запустить сейчас",
command=self.start_manual_copy).pack(side=tk.LEFT, padx=5)
# Фрейм для списка путей
paths_frame = ttk.LabelFrame(main_frame, text="Пути для копирования", padding="10")
paths_frame.pack(fill=tk.BOTH, expand=True, pady=10)
paths_frame = ttk.LabelFrame(settings_tab, text="Пары копирования", padding="10")
paths_frame.pack(fill=tk.BOTH, expand=True, pady=5)
# Заголовки колонок
headers_frame = ttk.Frame(paths_frame)
headers_frame.pack(fill=tk.X, pady=5)
ttk.Label(headers_frame, text="Откуда копировать (папка с бекапами)",
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 = ttk.Treeview(
paths_frame,
columns=("source", "dest", "mode", "status"),
show="headings",
selectmode="browse"
)
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")
canvas.configure(yscrollcommand=scrollbar.set)
tree_scroll = ttk.Scrollbar(paths_frame, orient="vertical", command=self.pairs_tree.yview)
self.pairs_tree.configure(yscrollcommand=tree_scroll.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
self.pairs_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
tree_scroll.pack(side=tk.RIGHT, fill=tk.Y)
# Кнопки управления списком
list_buttons_frame = ttk.Frame(paths_frame)
list_buttons_frame.pack(fill=tk.X, pady=10)
self.pairs_tree.tag_configure("ok", foreground="green")
self.pairs_tree.tag_configure("warn", foreground="orange")
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)
ttk.Button(list_buttons_frame, text="❌ Удалить все",
command=self.remove_all_pairs).pack(side=tk.LEFT, padx=5)
ttk.Button(list_buttons_frame, text="🔍 Проверить пути",
command=self.check_paths).pack(side=tk.LEFT, padx=5)
ttk.Button(list_buttons_frame, text="❌ Удалить",
command=self.remove_selected_pair).pack(side=tk.LEFT, padx=5)
ttk.Button(list_buttons_frame, text="📂 Открыть назначение",
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)
# Лог операций
log_frame = ttk.LabelFrame(main_frame, text="Лог операций", padding="5")
edit_frame = ttk.LabelFrame(settings_tab, text="Редактирование выбранной пары", padding="10")
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_buttons = ttk.Frame(log_frame)
log_buttons.pack(fill=tk.X, pady=2)
@@ -340,17 +389,23 @@ class BackgroundFileCopyApp:
ttk.Button(log_buttons, text="🔍 Отладка",
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.pack(fill=tk.BOTH, expand=True)
# Настройка тегов для цветного лога
self.log_text.tag_configure("success", foreground="green")
self.log_text.tag_configure("error", foreground="red")
self.log_text.tag_configure("warning", foreground="orange")
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="Готов к работе",
relief=tk.SUNKEN, anchor=tk.W)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
@@ -499,8 +554,7 @@ class BackgroundFileCopyApp:
else:
source, dest = item
full_copy = False
self.add_path_pair(source, dest)
self.copy_pairs[-1]['full_copy'].set(full_copy)
self.add_path_pair(source, dest, full_copy)
self.log_message(f"✅ Загружено {len(pairs_data)} пар путей", "success")
else:
# Если нет сохраненных пар, добавляем одну пустую
@@ -519,8 +573,10 @@ class BackgroundFileCopyApp:
self.verify_only.set(settings.get('verify_only', False))
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.update_last_success_label()
self.update_status_summary()
self.log_message(f"📂 Настройки загружены из {settings_path}", "info")
else:
@@ -530,8 +586,10 @@ class BackgroundFileCopyApp:
self.autostart_enabled.set(self.is_autostart_enabled())
self.verify_only.set(False)
self.last_success_time = None
self.last_result_summary = None
self.last_hashes = {}
self.update_last_success_label()
self.update_status_summary()
except Exception as e:
self.log_message(f"❌ Ошибка при загрузке настроек: {e}", "error")
@@ -542,8 +600,10 @@ class BackgroundFileCopyApp:
self.autostart_enabled.set(self.is_autostart_enabled())
self.verify_only.set(False)
self.last_success_time = None
self.last_result_summary = None
self.last_hashes = {}
self.update_last_success_label()
self.update_status_summary()
def save_settings(self):
"""Сохраняет настройки в файл"""
@@ -551,17 +611,13 @@ class BackgroundFileCopyApp:
# Собираем данные из полей ввода
pairs_data = []
for pair in self.copy_pairs:
source = pair['source'].get().strip()
dest = pair['dest'].get().strip()
if source.startswith("Например: "):
source = ""
if dest.startswith("Например: "):
dest = ""
source = pair.get("source", "").strip()
dest = pair.get("dest", "").strip()
if source or dest: # Сохраняем даже если одно поле пустое
pairs_data.append({
"source": source,
"dest": dest,
"full_copy": pair['full_copy'].get()
"full_copy": bool(pair.get("full_copy", False))
})
settings = {
@@ -572,6 +628,7 @@ class BackgroundFileCopyApp:
'minimize_to_tray': self.minimize_to_tray_enabled.get(),
'verify_only': self.verify_only.get(),
'last_success_time': self.last_success_time,
'last_result_summary': self.last_result_summary,
'last_hashes': self.last_hashes,
'pairs': pairs_data
}
@@ -594,88 +651,141 @@ class BackgroundFileCopyApp:
self.log_message(f"❌ Ошибка при сохранении настроек: {e}", "error")
messagebox.showerror("Ошибка", f"Не удалось сохранить настройки:\n{e}")
def add_path_pair(self, source="", dest=""):
"""Добавляет новую пару полей для ввода путей"""
pair_frame = ttk.Frame(self.scrollable_frame)
pair_frame.pack(fill=tk.X, pady=5)
def add_path_pair(self, source="", dest="", full_copy: bool = False):
"""Добавляет новую пару в таблицу."""
pair_id = f"pair_{self.pair_counter}"
self.pair_counter += 1
# Поле "Откуда"
source_frame = ttk.Frame(pair_frame)
source_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
pair = {
"id": pair_id,
"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)
source_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
if source:
source_entry.insert(0, source)
else:
self.create_placeholder(source_entry, "Например: C:\\Backups")
mode_text = "Вся папка" if pair["full_copy"] else "Последний файл"
self.pairs_tree.insert("", "end", iid=pair_id,
values=(source, dest, mode_text, pair["status"]),
tags=(pair["status_tag"],))
self.pairs_tree.selection_set(pair_id)
self.on_pair_select()
ttk.Button(source_frame, text="📁", width=3,
command=lambda: self.browse_folder(source_entry)).pack(side=tk.RIGHT, padx=2)
def remove_selected_pair(self):
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)
# Поле "Куда"
dest_frame = ttk.Frame(pair_frame)
dest_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(5, 0))
dest_entry = ttk.Entry(dest_frame)
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_path_pair(self, pair_id):
"""Совместимость: удалить пару по id."""
if pair_id:
self.selected_pair_id = pair_id
self.remove_selected_pair()
def remove_all_pairs(self, silent=False):
"""Удаляет все пары"""
if not silent:
if not messagebox.askyesno("Подтверждение", "Удалить все пути?"):
return
for pair in self.copy_pairs[:]:
pair['frame'].destroy()
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):
"""Открывает диалог выбора папки"""
@@ -691,18 +801,122 @@ class BackgroundFileCopyApp:
with contextlib.suppress(Exception):
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]:
"""Возвращает список валидных пар папок"""
valid_pairs = []
for pair in self.copy_pairs:
source = pair['source'].get().strip()
dest = pair['dest'].get().strip()
if source.startswith("Например: "):
source = ""
if dest.startswith("Например: "):
dest = ""
source = pair.get("source", "").strip()
dest = pair.get("dest", "").strip()
full_copy = bool(pair.get("full_copy", False))
if source and dest:
valid_pairs.append((source, dest, pair['full_copy'].get()))
valid_pairs.append((source, dest, full_copy))
return valid_pairs
def check_paths(self):
@@ -719,29 +933,22 @@ class BackgroundFileCopyApp:
mode_text = "вся папка" if full_copy else "последний файл"
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):
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']
self.log_message(f" Найдено .bak файлов: {len(bak_files)}", "info")
else:
self.log_message(f" ❌ Исходная папка НЕ существует: {source}", "error")
# Проверяем целевую папку
if os.path.exists(dest):
self.log_message(f" ✅ Целевая папка: {dest}", "success")
# Проверяем права на запись
test_file = os.path.join(dest, 'test_write.tmp')
try:
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")
# Обновляем статусы в таблице
for pair in self.copy_pairs:
status_text, status_tag = self.validate_pair(pair.get("source", ""), pair.get("dest", ""))
pair["status"] = status_text
pair["status_tag"] = status_tag
self.update_pair_row(pair)
def find_latest_file(self, folder_path: str) -> Optional[Path]:
"""Находит самый последний файл в папке"""
@@ -769,6 +976,9 @@ class BackgroundFileCopyApp:
"""Планирует выполнение функции в UI-потоке."""
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):
key = str(target_file)
if key in self.last_hashes:
@@ -813,6 +1023,7 @@ class BackgroundFileCopyApp:
src_file = Path(root) / fname
dst_file = dest_root / fname
try:
self.set_status_text(f"Копируется: {src_file.name}")
self.check_previous_hash(dst_file) if dst_file.exists() else None
if self.verify_only.get():
if self.verify_only_mode(src_file, dst_file):
@@ -890,6 +1101,7 @@ class BackgroundFileCopyApp:
target_file = dest_path / latest_file.name
try:
self.set_status_text(f"Копируется: {latest_file.name}")
if target_file.exists():
self.check_previous_hash(target_file)
if self.verify_only.get():
@@ -935,6 +1147,9 @@ class BackgroundFileCopyApp:
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:
self.last_success_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.run_on_ui(self.update_last_success_label)
@@ -1015,6 +1230,11 @@ class BackgroundFileCopyApp:
else:
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):
"""Отладочный метод для проверки загрузки настроек"""
self.log_message("\n" + "=" * 50, "info")
@@ -1039,11 +1259,13 @@ class BackgroundFileCopyApp:
self.log_message(f" Пар в интерфейсе: {len(self.copy_pairs)}", "info")
for i, pair in enumerate(self.copy_pairs, 1):
source = pair['source'].get()
dest = pair['dest'].get()
source = pair.get("source", "")
dest = pair.get("dest", "")
mode = "вся папка" if pair.get("full_copy", False) else "последний файл"
self.log_message(f" Пара {i}:", "info")
self.log_message(f" Откуда: '{source}'", "info")
self.log_message(f" Куда: '{dest}'", "info")
self.log_message(f" Режим: {mode}", "info")
self.log_message("=" * 50, "info")