SFM/app/samba_access.py
2026-09-02 10:51:54 +03:00

233 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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}"