From 400a218fc79ac2285c78436fc79326ec5b8dc448 Mon Sep 17 00:00:00 2001 From: Raykov-MS Date: Mon, 6 Jul 2026 22:42:25 +0300 Subject: [PATCH] fix smb --- app/pipeline/file_manager.py | 57 +++++++++++++++++++++++++++++++++ app/pipeline/service.py | 24 ++++++++++++++ tests/unit/test_file_manager.py | 39 ++++++++++++++++++++++ 3 files changed, 120 insertions(+) diff --git a/app/pipeline/file_manager.py b/app/pipeline/file_manager.py index 6ac8892..b3d6b14 100644 --- a/app/pipeline/file_manager.py +++ b/app/pipeline/file_manager.py @@ -196,6 +196,42 @@ def check_network_dir_access( return _check_samba_dir_access(str(path), samba_conn, "Сетевая папка") +def check_dir_write_access( + target_dir: str, samba_conn, source_name: str +) -> tuple[bool, str | None]: + probe_prefix = f".sfm_write_probe_{int(datetime.now().timestamp() * 1000)}" + probe_src = join_path(target_dir, f"{probe_prefix}.tmp") + probe_dst = join_path(target_dir, f"{probe_prefix}_renamed.tmp") + + try: + with samba_conn: + if not samba_conn.path_exists(target_dir): + return False, f"{source_name} не найдена: {target_dir}" + _write_probe_file(probe_src, samba_conn) + if hasattr(samba_conn, "copy") and hasattr(samba_conn, "delete"): + samba_conn.copy(path_from=probe_src, path_to=probe_dst, replace=False) + samba_conn.delete(probe_src) + samba_conn.delete(probe_dst) + elif hasattr(samba_conn, "delete"): + samba_conn.delete(probe_src) + return True, None + except Exception as exc: # noqa: BLE001 + # Best-effort cleanup for noisy probes. + with samba_conn: + for probe_path in (probe_src, probe_dst): + try: + if samba_conn.path_exists(probe_path) and hasattr( + samba_conn, "delete" + ): + samba_conn.delete(probe_path) + except Exception: # noqa: BLE001 + pass + return ( + False, + f"Недостаточно прав на запись/переименование в папке: {target_dir}. {exc}", + ) + + def create_run_context( input_dir: str, samba_conn, now: datetime | None = None ) -> RunContext: @@ -369,6 +405,27 @@ def _samba_move_file(path_from: str, path_to: str, samba_conn) -> None: samba_conn.delete(path_from) +def _write_probe_file(remote_path: str, samba_conn) -> None: + if hasattr(samba_conn, "open"): + with samba_conn.open(remote_path, mode="wb") as remote_file: + remote_file.write(b"sfm-probe") + return + if hasattr(samba_conn, "upload"): + with tempfile.TemporaryDirectory() as temp_dir: + local_probe = Path(temp_dir) / "probe.tmp" + local_probe.write_bytes(b"sfm-probe") + try: + samba_conn.upload( + remote_file_path=remote_path, + local_file_path=str(local_probe), + replace=False, + ) + except TypeError: + samba_conn.upload(path_from=str(local_probe), path_to=remote_path) + return + raise RuntimeError("Коннектор SAMBA не поддерживает проверку записи (open/upload).") + + def _extract_server(smb_base_path: str) -> str: server = urlparse(smb_base_path).netloc if not server: diff --git a/app/pipeline/service.py b/app/pipeline/service.py index e96a7a8..a40987c 100644 --- a/app/pipeline/service.py +++ b/app/pipeline/service.py @@ -12,6 +12,7 @@ from .config import ( join_path, ) from .file_manager import ( + check_dir_write_access, check_input_dir_access, connect_samba, create_run_context, @@ -80,6 +81,29 @@ def run_batch( ) run_context = create_run_context(input_dir=input_dir, samba_conn=samba_conn) + checks = [ + (run_context.run_dir, "Папка Out (текущий запуск)"), + (run_context.processed_dir, "Папка Processed"), + (run_context.error_dir, "Папка Err"), + ] + for target_dir, label in checks: + is_ok, write_error = check_dir_write_access( + target_dir=target_dir, + samba_conn=samba_conn, + source_name=label, + ) + if not is_ok: + return BatchResult( + status="error", + processed=0, + partial=0, + errors=1, + report_path="", + protocol_path="", + log_path="", + messages=[write_error or f"Нет прав на запись в: {target_dir}"], + ) + protocol_rows: list[ProtocolRow] = [] log_lines: list[str] = [] diff --git a/tests/unit/test_file_manager.py b/tests/unit/test_file_manager.py index d195c50..91797d8 100644 --- a/tests/unit/test_file_manager.py +++ b/tests/unit/test_file_manager.py @@ -4,6 +4,7 @@ import shutil from pathlib import Path from app.pipeline.file_manager import ( + check_dir_write_access, create_run_context, move_file_to_error, next_run_number, @@ -57,6 +58,11 @@ class FakeSamba: if target.exists(): target.unlink() + def open(self, path: str, mode: str = "rb"): + target = self._to_local(path) + target.parent.mkdir(parents=True, exist_ok=True) + return target.open(mode) + def test_scan_input_files_splits_valid_and_invalid(tmp_path: Path) -> None: samba = FakeSamba(tmp_path) @@ -112,3 +118,36 @@ def test_move_file_to_error_adds_suffix_on_collision(tmp_path: Path) -> None: ) assert target.endswith("file_1.xml") assert (error_dir / "file_1.xml").exists() + + +def test_check_dir_write_access_success(tmp_path: Path) -> None: + samba = FakeSamba(tmp_path) + target_dir = tmp_path / "Exchange" / "SMBDEMO" / "Out" + target_dir.mkdir(parents=True) + + ok, error = check_dir_write_access( + target_dir="Exchange\\SMBDEMO\\Out", + samba_conn=samba, + source_name="Out", + ) + + assert ok is True + assert error is None + + +def test_check_dir_write_access_fails_without_open_or_upload(tmp_path: Path) -> None: + class NoWriteSamba(FakeSamba): + open = None # type: ignore[assignment] + + samba = NoWriteSamba(tmp_path) + target_dir = tmp_path / "Exchange" / "SMBDEMO" / "Out" + target_dir.mkdir(parents=True) + + ok, error = check_dir_write_access( + target_dir="Exchange\\SMBDEMO\\Out", + samba_conn=samba, + source_name="Out", + ) + + assert ok is False + assert error is not None