diff --git a/app/pipeline/file_manager.py b/app/pipeline/file_manager.py index f5fa34b..065c96f 100644 --- a/app/pipeline/file_manager.py +++ b/app/pipeline/file_manager.py @@ -41,6 +41,12 @@ class InputFileScanResult: skipped_files: list[RemoteFile] +class ProcessedCopySucceededDeleteFailedError(RuntimeError): + def __init__(self, message: str, target_path: str) -> None: + super().__init__(message) + self.target_path = target_path + + class SMBClientConnection: def __init__( self, smb_base_path: str, user_params: dict[str, str] | None = None @@ -455,11 +461,12 @@ def _samba_move_file(path_from: str, path_to: str, samba_conn) -> None: samba_conn.delete(path_from) except Exception as exc: # noqa: BLE001 target_exists = _safe_path_exists(path_to, samba_conn) - raise RuntimeError( + raise ProcessedCopySucceededDeleteFailedError( "SMB move failed at delete stage after successful copy: " f"source='{path_from}', target='{path_to}', " f"target_exists={target_exists}, {_format_smb_exception(exc)}" - f"{_format_rename_attempt(rename_error)}" + f"{_format_rename_attempt(rename_error)}", + target_path=path_to, ) from exc diff --git a/app/pipeline/service.py b/app/pipeline/service.py index a58030f..7ef539c 100644 --- a/app/pipeline/service.py +++ b/app/pipeline/service.py @@ -13,6 +13,7 @@ from .config import ( join_path, ) from .file_manager import ( + ProcessedCopySucceededDeleteFailedError, check_dir_write_access, check_input_dir_access, connect_samba, @@ -331,6 +332,16 @@ def _process_files( log_lines.append( f"{_now()} | success -> processed: {remote_file.path} => {moved_to}" ) + except ProcessedCopySucceededDeleteFailedError as exc: + moved_to = exc.target_path + message = ( + f"{message} Файл скопирован в Processed, " + "но исходник не удален из входной папки из-за ограничений доступа." + ) + log_lines.append( + f"{_now()} | success with delete warning: " + f"{remote_file.path} => {moved_to}. {exc}" + ) except Exception as exc: # noqa: BLE001 status = "partial" message = f"{message} Не удалось переместить в Processed: {exc}" diff --git a/tests/smoke/test_batch_smoke.py b/tests/smoke/test_batch_smoke.py index 78e9d6e..2e7a81d 100644 --- a/tests/smoke/test_batch_smoke.py +++ b/tests/smoke/test_batch_smoke.py @@ -62,6 +62,14 @@ class FakeSamba: shutil.copy2(source, target) +class FakeSambaDeleteDeniedForXml(FakeSamba): + def delete(self, path: str) -> None: + normalized = path.replace("/", "\\").lower() + if normalized.endswith(".xml") and "\\test\\" in normalized: + raise PermissionError("STATUS_ACCESS_DENIED") + super().delete(path) + + def _valid_xml() -> str: return """ <СообщОперКО xmlns="urn:test"> @@ -113,3 +121,30 @@ def test_run_batch_smoke_and_idempotency(tmp_path: Path, monkeypatch) -> None: ) assert second.status == "success" assert second.processed == 0 + + +def test_run_batch_success_when_delete_denied_after_copy( + tmp_path: Path, monkeypatch +) -> None: + samba = FakeSambaDeleteDeniedForXml(tmp_path) + in_dir_local = tmp_path / "Exchange" / "SMBDEMO" / "test" + in_dir_local.mkdir(parents=True) + file_name = "SKO115FZ_01_123456789_20260616_X00001.xml" + (in_dir_local / file_name).write_text(_valid_xml(), encoding="utf-8") + + from app.pipeline import service + + monkeypatch.setattr( + service, "connect_samba", lambda smb_base_path, user_params: samba + ) + + result = run_batch( + smb_base_path="//test-server/share", + input_dir="Exchange\\SMBDEMO\\test", + samba_user="user", + samba_password="pass", + launcher="tester", + ) + + assert result.status == "success" + assert result.processed == 1