from __future__ import annotations import re from collections.abc import Callable from dataclasses import dataclass from pathlib import Path from typing import Literal from uuid import uuid4 from app.pipeline.config import join_path from app.pipeline.file_manager import ( check_input_dir_access, connect_samba, ensure_dir_exists, ) @dataclass(frozen=True) class SMBDiagnosticStep: stage: str status: Literal["ok", "error", "skipped"] details: str @dataclass(frozen=True) class SMBDiagnosticResult: test_path: str steps: tuple[SMBDiagnosticStep, ...] def build_user_params(samba_user: str, samba_password: str) -> dict[str, str]: return { "user_name": samba_user, "password": samba_password, } def check_access_and_list_files( smb_base_path: str, input_dir: str, samba_user: str, samba_password: str, ) -> tuple[bool, str, list[str]]: if not samba_user: return False, "Введите логин SAMBA.", [] user_params = build_user_params(samba_user, samba_password) try: samba_conn = connect_samba(smb_base_path=smb_base_path, user_params=user_params) except RuntimeError as exc: return False, _format_connect_error(str(exc)), [] # Early handshake to surface auth/network errors clearly. try: with samba_conn: pass except Exception as exc: # noqa: BLE001 return False, _format_connect_error(str(exc)), [] try: ensure_dir_exists(input_dir, samba_conn=samba_conn) except Exception as exc: # noqa: BLE001 return False, f"Не удалось создать входную папку: {input_dir}. {exc}", [] if not samba_conn.path_exists(input_dir): return ( False, ( "Не удалось создать входную папку: " f"{input_dir}. Проверьте права на родительский каталог." ), [], ) ok, error = check_input_dir_access(Path(input_dir), samba_conn=samba_conn) if not ok: return False, error or "Ошибка доступа к входной папке.", [] try: with samba_conn: entries = sorted(samba_conn.listdir(input_dir)) files: list[str] = [] for name in entries: full_path = input_dir.rstrip("/\\") + "\\" + name if samba_conn.is_file(full_path): files.append(name) except Exception as exc: # noqa: BLE001 return False, f"Ошибка чтения содержимого входной папки: {input_dir}. {exc}", [] return True, "", files def diagnose_smb_access( smb_base_path: str, parent_dir: str, samba_user: str, samba_password: str, ) -> SMBDiagnosticResult: test_path = join_path(parent_dir, f"Input_diag_{uuid4().hex[:8]}") steps: list[SMBDiagnosticStep] = [] if not samba_user: steps.append( SMBDiagnosticStep( stage="Подключение к SMB", status="error", details="Введите логин SAMBA.", ) ) return SMBDiagnosticResult(test_path=test_path, steps=tuple(steps)) try: samba_conn = connect_samba( smb_base_path=smb_base_path, user_params=build_user_params(samba_user, samba_password), ) with samba_conn: pass except Exception as exc: # noqa: BLE001 steps.append(_diagnostic_error("Подключение к SMB", exc)) return SMBDiagnosticResult(test_path=test_path, steps=tuple(steps)) steps.append(_diagnostic_ok("Подключение к SMB")) _run_diagnostic_step( steps, "Проверка родительской папки", lambda: samba_conn.stat(parent_dir), ) _run_diagnostic_step( steps, "Чтение родительской папки", lambda: samba_conn.listdir(parent_dir), ) try: created = _run_diagnostic_step( steps, "Создание тестовой папки", lambda: samba_conn.mkdir(test_path), ) if created: _run_diagnostic_step( steps, "Проверка созданной папки", lambda: samba_conn.stat(test_path), ) _run_diagnostic_step( steps, "Чтение созданной папки", lambda: samba_conn.listdir(test_path), ) _run_diagnostic_step( steps, "Проверка папки в родительском каталоге", lambda: _require_directory_in_parent( samba_conn.listdir(parent_dir), test_path, ), ) else: for stage in ( "Проверка созданной папки", "Чтение созданной папки", "Проверка папки в родительском каталоге", ): steps.append( SMBDiagnosticStep( stage=stage, status="skipped", details="Пропущено: создание тестовой папки завершилось ошибкой.", ) ) finally: _run_diagnostic_step( steps, "Удаление тестовой папки", lambda: samba_conn.rmdir(test_path), ) return SMBDiagnosticResult(test_path=test_path, steps=tuple(steps)) def _run_diagnostic_step( steps: list[SMBDiagnosticStep], stage: str, action: Callable[[], object], ) -> bool: try: action() except Exception as exc: # noqa: BLE001 steps.append(_diagnostic_error(stage, exc)) return False steps.append(_diagnostic_ok(stage)) return True def _require_directory_in_parent(entries: list[str], test_path: str) -> None: directory_name = test_path.rstrip("/\\").rsplit("\\", 1)[-1] if directory_name not in entries: raise FileNotFoundError( f"Созданная папка {directory_name} отсутствует в списке родителя." ) def _diagnostic_ok(stage: str) -> SMBDiagnosticStep: return SMBDiagnosticStep(stage=stage, status="ok", details="Успешно.") def _diagnostic_error(stage: str, exc: Exception) -> SMBDiagnosticStep: details = str(exc) status = getattr(exc, "ntstatus", None) or getattr(exc, "status", None) if status is None: match = re.search(r"\bSTATUS_[A-Z0-9_]+\b", details, flags=re.IGNORECASE) status = match.group(0).upper() if match else None suffix = f"; NTSTATUS: {status}" if status is not None else "" return SMBDiagnosticStep( stage=stage, status="error", details=f"{type(exc).__name__}: {details}{suffix}", ) def _format_connect_error(details: str) -> str: normalized = details.lower() auth_hints = ( "status_logon_failure", "logon failure", "authentication", "no username or password", ) if any(hint in normalized for hint in auth_hints): return f"Ошибка аутентификации SMB. {details}" return f"Ошибка подключения к SMB. {details}"