233 lines
7.5 KiB
Python
233 lines
7.5 KiB
Python
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}"
|