Fix numeric parsing, active file status, and checksum flow
This commit is contained in:
215
main.py
215
main.py
@@ -40,6 +40,7 @@ COPY_RETRIES = 2
|
|||||||
COPY_RETRY_DELAY = 2
|
COPY_RETRY_DELAY = 2
|
||||||
DEFAULT_MIN_FREE_GB = 1
|
DEFAULT_MIN_FREE_GB = 1
|
||||||
DEFAULT_MAX_WORKERS = 3
|
DEFAULT_MAX_WORKERS = 3
|
||||||
|
DEFAULT_KEEP_FILES = 1
|
||||||
DEFAULT_INCLUDE_MASKS = "*.bak;*.sql;*.backup"
|
DEFAULT_INCLUDE_MASKS = "*.bak;*.sql;*.backup"
|
||||||
DEFAULT_EXCLUDE_MASKS = "*.tmp;*.temp"
|
DEFAULT_EXCLUDE_MASKS = "*.tmp;*.temp"
|
||||||
MUTEX_NAME = "Global\\BackupCopierMutex"
|
MUTEX_NAME = "Global\\BackupCopierMutex"
|
||||||
@@ -106,6 +107,12 @@ def verify_copy(source: Path, target: Path) -> bool:
|
|||||||
return compute_file_checksum(source) == compute_file_checksum(target)
|
return compute_file_checksum(source) == compute_file_checksum(target)
|
||||||
|
|
||||||
|
|
||||||
|
def compare_file_checksums(source: Path, target: Path) -> tuple[bool, str, str]:
|
||||||
|
src_hash = compute_file_checksum(source)
|
||||||
|
dst_hash = compute_file_checksum(target)
|
||||||
|
return src_hash == dst_hash, src_hash, dst_hash
|
||||||
|
|
||||||
|
|
||||||
def get_resource_path(relative_path: str) -> str:
|
def get_resource_path(relative_path: str) -> str:
|
||||||
"""Возвращает абсолютный путь к ресурсу (поддерживает PyInstaller)."""
|
"""Возвращает абсолютный путь к ресурсу (поддерживает PyInstaller)."""
|
||||||
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
||||||
@@ -274,6 +281,8 @@ class BackgroundFileCopyApp:
|
|||||||
# Флаг для отслеживания состояния
|
# Флаг для отслеживания состояния
|
||||||
self.is_copying = False
|
self.is_copying = False
|
||||||
self.copy_lock = threading.Lock()
|
self.copy_lock = threading.Lock()
|
||||||
|
self.active_files_lock = threading.Lock()
|
||||||
|
self.active_files: Dict[str, int] = {}
|
||||||
|
|
||||||
# Автозапуск
|
# Автозапуск
|
||||||
self.autostart_enabled = tk.BooleanVar(value=False)
|
self.autostart_enabled = tk.BooleanVar(value=False)
|
||||||
@@ -302,6 +311,7 @@ class BackgroundFileCopyApp:
|
|||||||
self.min_free_gb_var = tk.DoubleVar(value=DEFAULT_MIN_FREE_GB)
|
self.min_free_gb_var = tk.DoubleVar(value=DEFAULT_MIN_FREE_GB)
|
||||||
self.max_workers_var = tk.IntVar(value=DEFAULT_MAX_WORKERS)
|
self.max_workers_var = tk.IntVar(value=DEFAULT_MAX_WORKERS)
|
||||||
self.cleanup_old_var = tk.BooleanVar(value=False)
|
self.cleanup_old_var = tk.BooleanVar(value=False)
|
||||||
|
self.keep_files_var = tk.IntVar(value=DEFAULT_KEEP_FILES)
|
||||||
self.schedules: List[dict] = []
|
self.schedules: List[dict] = []
|
||||||
self.schedule_days_vars = {
|
self.schedule_days_vars = {
|
||||||
"Mon": tk.BooleanVar(value=False),
|
"Mon": tk.BooleanVar(value=False),
|
||||||
@@ -479,6 +489,12 @@ class BackgroundFileCopyApp:
|
|||||||
command=lambda: self.show_hint("Очистка",
|
command=lambda: self.show_hint("Очистка",
|
||||||
"В режиме 'последний файл' удаляет старые файлы в папке назначения.")).pack(side=tk.LEFT, padx=4)
|
"В режиме 'последний файл' удаляет старые файлы в папке назначения.")).pack(side=tk.LEFT, padx=4)
|
||||||
|
|
||||||
|
ttk.Label(options_row2, text="Хранить последних:").pack(side=tk.LEFT, padx=(10, 4))
|
||||||
|
self.keep_files_spin = ttk.Spinbox(options_row2, from_=1, to=999, width=4, textvariable=self.keep_files_var)
|
||||||
|
self.keep_files_spin.pack(side=tk.LEFT)
|
||||||
|
self.keep_files_spin.bind("<KeyRelease>", lambda _e: self.request_autosave())
|
||||||
|
self.keep_files_spin.bind("<FocusOut>", lambda _e: self.request_autosave())
|
||||||
|
|
||||||
perf_frame = ttk.Frame(schedule_frame)
|
perf_frame = ttk.Frame(schedule_frame)
|
||||||
perf_frame.pack(fill=tk.X, pady=4)
|
perf_frame.pack(fill=tk.X, pady=4)
|
||||||
ttk.Label(perf_frame, text="Мин. свободно, ГБ:").pack(side=tk.LEFT)
|
ttk.Label(perf_frame, text="Мин. свободно, ГБ:").pack(side=tk.LEFT)
|
||||||
@@ -728,6 +744,7 @@ class BackgroundFileCopyApp:
|
|||||||
profiles["Default"]["min_free_gb"] = settings.get('min_free_gb', DEFAULT_MIN_FREE_GB)
|
profiles["Default"]["min_free_gb"] = settings.get('min_free_gb', DEFAULT_MIN_FREE_GB)
|
||||||
profiles["Default"]["max_workers"] = settings.get('max_workers', DEFAULT_MAX_WORKERS)
|
profiles["Default"]["max_workers"] = settings.get('max_workers', DEFAULT_MAX_WORKERS)
|
||||||
profiles["Default"]["verify_only"] = settings.get('verify_only', False)
|
profiles["Default"]["verify_only"] = settings.get('verify_only', False)
|
||||||
|
profiles["Default"]["keep_files"] = settings.get('keep_files', DEFAULT_KEEP_FILES)
|
||||||
profiles["Default"]["last_success_time"] = settings.get('last_success_time')
|
profiles["Default"]["last_success_time"] = settings.get('last_success_time')
|
||||||
profiles["Default"]["last_result_summary"] = settings.get('last_result_summary')
|
profiles["Default"]["last_result_summary"] = settings.get('last_result_summary')
|
||||||
profiles["Default"]["last_hashes"] = settings.get('last_hashes', {})
|
profiles["Default"]["last_hashes"] = settings.get('last_hashes', {})
|
||||||
@@ -736,7 +753,7 @@ class BackgroundFileCopyApp:
|
|||||||
self.profiles = profiles
|
self.profiles = profiles
|
||||||
self.refresh_profile_combo()
|
self.refresh_profile_combo()
|
||||||
active = settings.get("active_profile") or self.active_profile.get()
|
active = settings.get("active_profile") or self.active_profile.get()
|
||||||
self.load_profile(active)
|
self.load_profile(active, save_current=False)
|
||||||
|
|
||||||
# Настройки автозапуска
|
# Настройки автозапуска
|
||||||
actual_autostart = self.is_autostart_enabled()
|
actual_autostart = self.is_autostart_enabled()
|
||||||
@@ -754,7 +771,7 @@ class BackgroundFileCopyApp:
|
|||||||
self.log_message("🆕 Файл настроек не найден, создаем новую конфигурацию", "info")
|
self.log_message("🆕 Файл настроек не найден, создаем новую конфигурацию", "info")
|
||||||
self.profiles = {"Default": self.default_profile()}
|
self.profiles = {"Default": self.default_profile()}
|
||||||
self.refresh_profile_combo()
|
self.refresh_profile_combo()
|
||||||
self.load_profile("Default")
|
self.load_profile("Default", save_current=False)
|
||||||
self.autostart_enabled.set(self.is_autostart_enabled())
|
self.autostart_enabled.set(self.is_autostart_enabled())
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -763,7 +780,7 @@ class BackgroundFileCopyApp:
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
self.profiles = {"Default": self.default_profile()}
|
self.profiles = {"Default": self.default_profile()}
|
||||||
self.refresh_profile_combo()
|
self.refresh_profile_combo()
|
||||||
self.load_profile("Default")
|
self.load_profile("Default", save_current=False)
|
||||||
self.autostart_enabled.set(self.is_autostart_enabled())
|
self.autostart_enabled.set(self.is_autostart_enabled())
|
||||||
finally:
|
finally:
|
||||||
self.autosave_enabled = True
|
self.autosave_enabled = True
|
||||||
@@ -877,7 +894,8 @@ class BackgroundFileCopyApp:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def update_pair_row(self, pair):
|
def update_pair_row(self, pair):
|
||||||
self.render_pairs()
|
# Treeview is not thread-safe; schedule redraw in UI thread.
|
||||||
|
self.run_on_ui(self.render_pairs)
|
||||||
|
|
||||||
def shorten_path(self, path: str, max_len: int = 40) -> str:
|
def shorten_path(self, path: str, max_len: int = 40) -> str:
|
||||||
if len(path) <= max_len:
|
if len(path) <= max_len:
|
||||||
@@ -1316,6 +1334,7 @@ class BackgroundFileCopyApp:
|
|||||||
"max_workers": DEFAULT_MAX_WORKERS,
|
"max_workers": DEFAULT_MAX_WORKERS,
|
||||||
"verify_only": False,
|
"verify_only": False,
|
||||||
"cleanup_old": False,
|
"cleanup_old": False,
|
||||||
|
"keep_files": DEFAULT_KEEP_FILES,
|
||||||
"last_success_time": None,
|
"last_success_time": None,
|
||||||
"last_result_summary": None,
|
"last_result_summary": None,
|
||||||
"last_hashes": {},
|
"last_hashes": {},
|
||||||
@@ -1337,20 +1356,25 @@ class BackgroundFileCopyApp:
|
|||||||
profile["schedules"] = list(self.schedules)
|
profile["schedules"] = list(self.schedules)
|
||||||
profile["include_masks"] = self.include_masks_var.get()
|
profile["include_masks"] = self.include_masks_var.get()
|
||||||
profile["exclude_masks"] = self.exclude_masks_var.get()
|
profile["exclude_masks"] = self.exclude_masks_var.get()
|
||||||
profile["min_free_gb"] = float(self.min_free_gb_var.get() or DEFAULT_MIN_FREE_GB)
|
profile["min_free_gb"] = self.get_float_setting(self.min_free_gb_var, DEFAULT_MIN_FREE_GB)
|
||||||
profile["max_workers"] = int(self.max_workers_var.get() or DEFAULT_MAX_WORKERS)
|
profile["max_workers"] = self.get_int_setting(self.max_workers_var, DEFAULT_MAX_WORKERS, minimum=1)
|
||||||
profile["verify_only"] = bool(self.verify_only.get())
|
profile["verify_only"] = bool(self.verify_only.get())
|
||||||
profile["cleanup_old"] = bool(self.cleanup_old_var.get())
|
profile["cleanup_old"] = bool(self.cleanup_old_var.get())
|
||||||
|
try:
|
||||||
|
profile["keep_files"] = max(1, int(self.keep_files_var.get() or DEFAULT_KEEP_FILES))
|
||||||
|
except Exception:
|
||||||
|
profile["keep_files"] = DEFAULT_KEEP_FILES
|
||||||
profile["skip_cleanup_confirm"] = bool(self.skip_cleanup_confirm)
|
profile["skip_cleanup_confirm"] = bool(self.skip_cleanup_confirm)
|
||||||
profile["last_success_time"] = self.last_success_time
|
profile["last_success_time"] = self.last_success_time
|
||||||
profile["last_result_summary"] = self.last_result_summary
|
profile["last_result_summary"] = self.last_result_summary
|
||||||
profile["last_hashes"] = self.last_hashes
|
profile["last_hashes"] = self.last_hashes
|
||||||
self.profiles[name] = profile
|
self.profiles[name] = profile
|
||||||
|
|
||||||
def load_profile(self, name: str):
|
def load_profile(self, name: str, save_current: bool = True):
|
||||||
self.loading_settings = True
|
self.loading_settings = True
|
||||||
self.autosave_enabled = False
|
self.autosave_enabled = False
|
||||||
self.save_active_profile()
|
if save_current:
|
||||||
|
self.save_active_profile()
|
||||||
profile = self.profiles.get(name) or self.default_profile()
|
profile = self.profiles.get(name) or self.default_profile()
|
||||||
self.active_profile.set(name)
|
self.active_profile.set(name)
|
||||||
self.include_masks_var.set(profile.get("include_masks", DEFAULT_INCLUDE_MASKS))
|
self.include_masks_var.set(profile.get("include_masks", DEFAULT_INCLUDE_MASKS))
|
||||||
@@ -1359,6 +1383,10 @@ class BackgroundFileCopyApp:
|
|||||||
self.max_workers_var.set(profile.get("max_workers", DEFAULT_MAX_WORKERS))
|
self.max_workers_var.set(profile.get("max_workers", DEFAULT_MAX_WORKERS))
|
||||||
self.verify_only.set(profile.get("verify_only", False))
|
self.verify_only.set(profile.get("verify_only", False))
|
||||||
self.cleanup_old_var.set(profile.get("cleanup_old", False))
|
self.cleanup_old_var.set(profile.get("cleanup_old", False))
|
||||||
|
try:
|
||||||
|
self.keep_files_var.set(max(1, int(profile.get("keep_files", DEFAULT_KEEP_FILES) or DEFAULT_KEEP_FILES)))
|
||||||
|
except Exception:
|
||||||
|
self.keep_files_var.set(DEFAULT_KEEP_FILES)
|
||||||
self.skip_cleanup_confirm = bool(profile.get("skip_cleanup_confirm", False))
|
self.skip_cleanup_confirm = bool(profile.get("skip_cleanup_confirm", False))
|
||||||
self.last_success_time = profile.get("last_success_time")
|
self.last_success_time = profile.get("last_success_time")
|
||||||
self.last_result_summary = profile.get("last_result_summary")
|
self.last_result_summary = profile.get("last_result_summary")
|
||||||
@@ -1539,6 +1567,8 @@ class BackgroundFileCopyApp:
|
|||||||
|
|
||||||
def stop_copying(self):
|
def stop_copying(self):
|
||||||
if self.is_copying:
|
if self.is_copying:
|
||||||
|
if not messagebox.askyesno("Подтверждение", "Остановить текущее копирование?"):
|
||||||
|
return
|
||||||
self.cancel_event.set()
|
self.cancel_event.set()
|
||||||
self.log_message("⏹ Остановка копирования по запросу пользователя", "warning")
|
self.log_message("⏹ Остановка копирования по запросу пользователя", "warning")
|
||||||
|
|
||||||
@@ -1716,9 +1746,72 @@ class BackgroundFileCopyApp:
|
|||||||
"""Планирует выполнение функции в UI-потоке."""
|
"""Планирует выполнение функции в UI-потоке."""
|
||||||
self.queue.put({'type': 'ui', 'func': func})
|
self.queue.put({'type': 'ui', 'func': func})
|
||||||
|
|
||||||
|
def get_float_setting(self, var, default: float) -> float:
|
||||||
|
try:
|
||||||
|
raw = var.get()
|
||||||
|
except Exception:
|
||||||
|
return float(default)
|
||||||
|
if raw in ("", None):
|
||||||
|
return float(default)
|
||||||
|
try:
|
||||||
|
return float(raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return float(default)
|
||||||
|
|
||||||
|
def get_int_setting(self, var, default: int, minimum: int = 1) -> int:
|
||||||
|
try:
|
||||||
|
raw = var.get()
|
||||||
|
except Exception:
|
||||||
|
return max(minimum, int(default))
|
||||||
|
if raw in ("", None):
|
||||||
|
return max(minimum, int(default))
|
||||||
|
try:
|
||||||
|
return max(minimum, int(raw))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return max(minimum, int(default))
|
||||||
|
|
||||||
|
def _format_active_file_label(self, file_path: str) -> str:
|
||||||
|
p = Path(file_path)
|
||||||
|
parts = p.parts
|
||||||
|
if len(parts) >= 2:
|
||||||
|
return str(Path(parts[-2]) / parts[-1])
|
||||||
|
return p.name or file_path
|
||||||
|
|
||||||
def set_status_text(self, text: str):
|
def set_status_text(self, text: str):
|
||||||
|
with self.active_files_lock:
|
||||||
|
has_active = bool(self.active_files)
|
||||||
|
if has_active:
|
||||||
|
self.update_status_with_active_files()
|
||||||
|
return
|
||||||
self.root.after(0, lambda: self.status_bar.config(text=text))
|
self.root.after(0, lambda: self.status_bar.config(text=text))
|
||||||
|
|
||||||
|
def update_status_with_active_files(self):
|
||||||
|
with self.active_files_lock:
|
||||||
|
file_paths = sorted(self.active_files.keys())
|
||||||
|
if not file_paths:
|
||||||
|
text = "Идет копирование..."
|
||||||
|
else:
|
||||||
|
labels = [self._format_active_file_label(p) for p in file_paths]
|
||||||
|
if len(labels) <= 3:
|
||||||
|
text = "Идет копирование: " + ", ".join(labels)
|
||||||
|
else:
|
||||||
|
text = f"Идет копирование: {', '.join(labels[:3])} ... (+{len(labels) - 3})"
|
||||||
|
self.root.after(0, lambda t=text: self.status_bar.config(text=t))
|
||||||
|
|
||||||
|
def begin_active_file(self, file_path: str):
|
||||||
|
with self.active_files_lock:
|
||||||
|
self.active_files[file_path] = self.active_files.get(file_path, 0) + 1
|
||||||
|
self.update_status_with_active_files()
|
||||||
|
|
||||||
|
def end_active_file(self, file_path: str):
|
||||||
|
with self.active_files_lock:
|
||||||
|
count = self.active_files.get(file_path, 0)
|
||||||
|
if count <= 1:
|
||||||
|
self.active_files.pop(file_path, None)
|
||||||
|
else:
|
||||||
|
self.active_files[file_path] = count - 1
|
||||||
|
self.update_status_with_active_files()
|
||||||
|
|
||||||
def set_progress_total(self, total: int):
|
def set_progress_total(self, total: int):
|
||||||
def _set():
|
def _set():
|
||||||
self.progress.config(maximum=max(1, total))
|
self.progress.config(maximum=max(1, total))
|
||||||
@@ -1757,16 +1850,22 @@ class BackgroundFileCopyApp:
|
|||||||
self.log_message(f"❌ Нет целевого файла для проверки: {target.name}", "error")
|
self.log_message(f"❌ Нет целевого файла для проверки: {target.name}", "error")
|
||||||
return False
|
return False
|
||||||
if not should_copy_file(source, target):
|
if not should_copy_file(source, target):
|
||||||
|
ok, src_hash, dst_hash = compare_file_checksums(source, target)
|
||||||
|
self.log_message(f"🔐 SHA256 src={src_hash} dest={dst_hash} file={target.name}", "info")
|
||||||
|
if not ok:
|
||||||
|
self.log_message(f"⚠️ SHA256 отличается у пропущенного файла: {target.name}", "warning")
|
||||||
self.log_message(f"⏭️ Проверка пропущена (не изменен): {target.name}", "warning")
|
self.log_message(f"⏭️ Проверка пропущена (не изменен): {target.name}", "warning")
|
||||||
return True
|
return True
|
||||||
if verify_copy(source, target):
|
ok, src_hash, dst_hash = compare_file_checksums(source, target)
|
||||||
|
self.log_message(f"🔐 SHA256 src={src_hash} dest={dst_hash} file={target.name}", "info")
|
||||||
|
if ok:
|
||||||
self.log_message(f"✅ Проверка OK: {target.name}", "success")
|
self.log_message(f"✅ Проверка OK: {target.name}", "success")
|
||||||
return True
|
return True
|
||||||
self.log_message(f"❌ Проверка не пройдена: {target.name}", "error")
|
self.log_message(f"❌ Проверка не пройдена: {target.name}", "error")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def has_enough_space(self, dest_path: Path, size_bytes: int) -> bool:
|
def has_enough_space(self, dest_path: Path, size_bytes: int) -> bool:
|
||||||
min_gb = float(self.min_free_gb_var.get() or DEFAULT_MIN_FREE_GB)
|
min_gb = self.get_float_setting(self.min_free_gb_var, DEFAULT_MIN_FREE_GB)
|
||||||
free_gb = get_free_space_gb(dest_path)
|
free_gb = get_free_space_gb(dest_path)
|
||||||
if free_gb < min_gb:
|
if free_gb < min_gb:
|
||||||
self.log_message(f"❌ Недостаточно места на диске: свободно {free_gb:.2f} ГБ, минимум {min_gb} ГБ", "error")
|
self.log_message(f"❌ Недостаточно места на диске: свободно {free_gb:.2f} ГБ, минимум {min_gb} ГБ", "error")
|
||||||
@@ -1776,6 +1875,34 @@ class BackgroundFileCopyApp:
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def cleanup_destination_files(self, dest_path: Path, include_masks: List[str], exclude_masks: List[str]) -> None:
|
||||||
|
try:
|
||||||
|
keep_count = max(1, int(self.keep_files_var.get() or DEFAULT_KEEP_FILES))
|
||||||
|
except Exception:
|
||||||
|
keep_count = DEFAULT_KEEP_FILES
|
||||||
|
|
||||||
|
candidates: List[Path] = []
|
||||||
|
for item in dest_path.iterdir():
|
||||||
|
if not item.is_file():
|
||||||
|
continue
|
||||||
|
if include_masks or exclude_masks:
|
||||||
|
if not matches_masks(item.name, include_masks, exclude_masks):
|
||||||
|
continue
|
||||||
|
candidates.append(item)
|
||||||
|
|
||||||
|
if len(candidates) <= keep_count:
|
||||||
|
return
|
||||||
|
|
||||||
|
candidates.sort(key=lambda p: (p.stat().st_mtime, p.name), reverse=True)
|
||||||
|
for item in candidates[keep_count:]:
|
||||||
|
try:
|
||||||
|
item.unlink()
|
||||||
|
self.log_message(f"🧹 Удален старый файл: {item.name}", "info")
|
||||||
|
except Exception as e:
|
||||||
|
msg = f"❌ Не удалось удалить файл {item.name}: {e}"
|
||||||
|
self.log_message(msg, "warning")
|
||||||
|
self.record_error_detail(msg)
|
||||||
|
|
||||||
def copy_full_folder(self, source: Path, dest: Path) -> tuple:
|
def copy_full_folder(self, source: Path, dest: Path) -> tuple:
|
||||||
copied = 0
|
copied = 0
|
||||||
skipped = 0
|
skipped = 0
|
||||||
@@ -1791,6 +1918,7 @@ class BackgroundFileCopyApp:
|
|||||||
break
|
break
|
||||||
src_file = Path(root) / fname
|
src_file = Path(root) / fname
|
||||||
dst_file = dest_root / fname
|
dst_file = dest_root / fname
|
||||||
|
self.begin_active_file(str(src_file))
|
||||||
try:
|
try:
|
||||||
self.set_status_text(f"Копируется: {src_file.name}")
|
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
|
||||||
@@ -1806,14 +1934,24 @@ class BackgroundFileCopyApp:
|
|||||||
errors += 1
|
errors += 1
|
||||||
self.step_progress()
|
self.step_progress()
|
||||||
continue
|
continue
|
||||||
if self.copy_file_with_retries(src_file, dst_file) and verify_copy(src_file, dst_file):
|
if self.copy_file_with_retries(src_file, dst_file):
|
||||||
copied += 1
|
ok, src_hash, dst_hash = compare_file_checksums(src_file, dst_file)
|
||||||
self.last_hashes[str(dst_file)] = compute_file_checksum(dst_file)
|
self.log_message(f"🔐 SHA256 src={src_hash} dest={dst_hash} file={dst_file.name}", "info")
|
||||||
|
if ok:
|
||||||
|
copied += 1
|
||||||
|
self.last_hashes[str(dst_file)] = dst_hash
|
||||||
|
else:
|
||||||
|
errors += 1
|
||||||
else:
|
else:
|
||||||
errors += 1
|
errors += 1
|
||||||
self.step_progress()
|
self.step_progress()
|
||||||
else:
|
else:
|
||||||
skipped += 1
|
skipped += 1
|
||||||
|
if dst_file.exists():
|
||||||
|
ok, src_hash, dst_hash = compare_file_checksums(src_file, dst_file)
|
||||||
|
self.log_message(f"🔐 SHA256 src={src_hash} dest={dst_hash} file={dst_file.name}", "info")
|
||||||
|
if not ok:
|
||||||
|
self.log_message(f"⚠️ SHA256 отличается у пропущенного файла: {dst_file.name}", "warning")
|
||||||
self.step_progress()
|
self.step_progress()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors += 1
|
errors += 1
|
||||||
@@ -1821,6 +1959,8 @@ class BackgroundFileCopyApp:
|
|||||||
msg = f"❌ Ошибка при копировании {src_file.name}: {e}"
|
msg = f"❌ Ошибка при копировании {src_file.name}: {e}"
|
||||||
self.log_message(msg, "error")
|
self.log_message(msg, "error")
|
||||||
self.record_error_detail(msg)
|
self.record_error_detail(msg)
|
||||||
|
finally:
|
||||||
|
self.end_active_file(str(src_file))
|
||||||
return copied, skipped, errors
|
return copied, skipped, errors
|
||||||
|
|
||||||
def process_pair(self, source: str, dest: str, full_copy: bool, background: bool) -> tuple:
|
def process_pair(self, source: str, dest: str, full_copy: bool, background: bool) -> tuple:
|
||||||
@@ -1884,6 +2024,7 @@ class BackgroundFileCopyApp:
|
|||||||
return copied_files, skipped_files, error_files + 1
|
return copied_files, skipped_files, error_files + 1
|
||||||
|
|
||||||
target_file = dest_path / latest_file.name
|
target_file = dest_path / latest_file.name
|
||||||
|
self.begin_active_file(str(latest_file))
|
||||||
try:
|
try:
|
||||||
self.set_status_text(f"Копируется: {latest_file.name}")
|
self.set_status_text(f"Копируется: {latest_file.name}")
|
||||||
if target_file.exists():
|
if target_file.exists():
|
||||||
@@ -1901,31 +2042,24 @@ class BackgroundFileCopyApp:
|
|||||||
if not self.has_enough_space(dest_path, latest_file.stat().st_size):
|
if not self.has_enough_space(dest_path, latest_file.stat().st_size):
|
||||||
self.step_progress()
|
self.step_progress()
|
||||||
return copied_files, skipped_files, error_files + 1
|
return copied_files, skipped_files, error_files + 1
|
||||||
if self.copy_file_with_retries(latest_file, target_file) and verify_copy(latest_file, target_file):
|
if self.copy_file_with_retries(latest_file, target_file):
|
||||||
copied_files += 1
|
copied_files += 1
|
||||||
self.last_hashes[str(target_file)] = compute_file_checksum(target_file)
|
src_hash = compute_file_checksum(latest_file)
|
||||||
if target_existed:
|
dst_hash = compute_file_checksum(target_file)
|
||||||
|
self.log_message(f"🔐 SHA256 src={src_hash} dest={dst_hash} file={target_file.name}", "info")
|
||||||
|
self.last_hashes[str(target_file)] = dst_hash
|
||||||
|
if src_hash != dst_hash:
|
||||||
|
error_files += 1
|
||||||
|
copied_files -= 1
|
||||||
|
msg = f"❌ Контрольная сумма не совпала: {latest_file.name}"
|
||||||
|
self.log_message(msg, "error")
|
||||||
|
self.record_error_detail(msg)
|
||||||
|
if src_hash == dst_hash and target_existed:
|
||||||
self.log_message(f"✅ Обновлен: {latest_file.name} (новее)", "success")
|
self.log_message(f"✅ Обновлен: {latest_file.name} (новее)", "success")
|
||||||
else:
|
elif src_hash == dst_hash:
|
||||||
self.log_message(f"✅ Скопирован: {latest_file.name}", "success")
|
self.log_message(f"✅ Скопирован: {latest_file.name}", "success")
|
||||||
if self.cleanup_old_var.get():
|
if src_hash == dst_hash and self.cleanup_old_var.get():
|
||||||
include_masks = parse_masks(self.include_masks_var.get())
|
self.cleanup_destination_files(dest_path, include_masks, exclude_masks)
|
||||||
exclude_masks = parse_masks(self.exclude_masks_var.get())
|
|
||||||
for item in dest_path.iterdir():
|
|
||||||
if not item.is_file():
|
|
||||||
continue
|
|
||||||
if item.name == latest_file.name:
|
|
||||||
continue
|
|
||||||
if include_masks or exclude_masks:
|
|
||||||
if not matches_masks(item.name, include_masks, exclude_masks):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
item.unlink()
|
|
||||||
self.log_message(f"🧹 Удален старый файл: {item.name}", "info")
|
|
||||||
except Exception as e:
|
|
||||||
msg = f"❌ Не удалось удалить файл {item.name}: {e}"
|
|
||||||
self.log_message(msg, "warning")
|
|
||||||
self.record_error_detail(msg)
|
|
||||||
else:
|
else:
|
||||||
error_files += 1
|
error_files += 1
|
||||||
msg = f"❌ Контрольная сумма не совпала: {latest_file.name}"
|
msg = f"❌ Контрольная сумма не совпала: {latest_file.name}"
|
||||||
@@ -1934,6 +2068,11 @@ class BackgroundFileCopyApp:
|
|||||||
self.step_progress()
|
self.step_progress()
|
||||||
else:
|
else:
|
||||||
skipped_files += 1
|
skipped_files += 1
|
||||||
|
if target_file.exists():
|
||||||
|
ok, src_hash, dst_hash = compare_file_checksums(latest_file, target_file)
|
||||||
|
self.log_message(f"🔐 SHA256 src={src_hash} dest={dst_hash} file={target_file.name}", "info")
|
||||||
|
if not ok:
|
||||||
|
self.log_message(f"⚠️ SHA256 отличается у пропущенного файла: {target_file.name}", "warning")
|
||||||
self.log_message(f"⏭️ Пропущен: {latest_file.name} (уже актуален)", "warning")
|
self.log_message(f"⏭️ Пропущен: {latest_file.name} (уже актуален)", "warning")
|
||||||
self.step_progress()
|
self.step_progress()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1942,6 +2081,8 @@ class BackgroundFileCopyApp:
|
|||||||
msg = f"❌ Ошибка при копировании {latest_file.name}: {e}"
|
msg = f"❌ Ошибка при копировании {latest_file.name}: {e}"
|
||||||
self.log_message(msg, "error")
|
self.log_message(msg, "error")
|
||||||
self.record_error_detail(msg)
|
self.record_error_detail(msg)
|
||||||
|
finally:
|
||||||
|
self.end_active_file(str(latest_file))
|
||||||
else:
|
else:
|
||||||
error_files += 1
|
error_files += 1
|
||||||
self.step_progress()
|
self.step_progress()
|
||||||
@@ -1984,7 +2125,7 @@ class BackgroundFileCopyApp:
|
|||||||
self.log_message("\n" + "=" * 50, "info")
|
self.log_message("\n" + "=" * 50, "info")
|
||||||
self.log_message("🚀 Начало копирования последних файлов", "info")
|
self.log_message("🚀 Начало копирования последних файлов", "info")
|
||||||
|
|
||||||
max_workers = max(1, int(self.max_workers_var.get() or DEFAULT_MAX_WORKERS))
|
max_workers = self.get_int_setting(self.max_workers_var, DEFAULT_MAX_WORKERS, minimum=1)
|
||||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
futures = [executor.submit(self.process_pair, source, dest, full_copy, background) for source, dest, full_copy in pairs]
|
futures = [executor.submit(self.process_pair, source, dest, full_copy, background) for source, dest, full_copy in pairs]
|
||||||
for future in as_completed(futures):
|
for future in as_completed(futures):
|
||||||
@@ -2032,6 +2173,8 @@ class BackgroundFileCopyApp:
|
|||||||
self.run_on_ui(lambda: messagebox.showerror("Ошибка", f"Произошла ошибка:\n{e}"))
|
self.run_on_ui(lambda: messagebox.showerror("Ошибка", f"Произошла ошибка:\n{e}"))
|
||||||
finally:
|
finally:
|
||||||
self.is_copying = False
|
self.is_copying = False
|
||||||
|
with self.active_files_lock:
|
||||||
|
self.active_files.clear()
|
||||||
self.copy_lock.release()
|
self.copy_lock.release()
|
||||||
self.root.after(0, lambda: self.status_bar.config(text="Готов к работе"))
|
self.root.after(0, lambda: self.status_bar.config(text="Готов к работе"))
|
||||||
self.root.after(0, lambda: self.progress_var.set(0))
|
self.root.after(0, lambda: self.progress_var.set(0))
|
||||||
|
|||||||
Reference in New Issue
Block a user