From 27ff4012d41aea424bc58fa0bfc390a5d4901941 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 13 Sep 2026 22:14:03 -0300 Subject: [PATCH] fix: preserve competing import destinations --- ingestion/guided_imports.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/ingestion/guided_imports.py b/ingestion/guided_imports.py index 88b1fc8..435cf11 100644 --- a/ingestion/guided_imports.py +++ b/ingestion/guided_imports.py @@ -1,5 +1,6 @@ """Safe discovery of local source documents for the guided import workflow.""" +import ctypes import os import stat from contextlib import contextmanager @@ -157,6 +158,20 @@ def _validate_name(name: str) -> None: raise ParseError("UNREADABLE_FILE") +def _rename_without_replacement(name: str, source_fd: int, destination_fd: int) -> None: + # The supported Docker runtime is Linux. Fail closed if atomic no-replace + # rename is unavailable instead of falling back to a check-then-replace race. + try: + rename = ctypes.CDLL(None, use_errno=True).renameat2 + except AttributeError: + raise ParseError("UNREADABLE_FILE") from None + rename.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint] + rename.restype = ctypes.c_int + encoded_name = os.fsencode(name) + if rename(source_fd, encoded_name, destination_fd, encoded_name, 1) != 0: # RENAME_NOREPLACE + raise OSError(ctypes.get_errno(), "Cannot move import source") + + def _move_file(name: str, source: str | None, destination: str | None, *, overwrite=False) -> None: _validate_name(name) with _directory(source) as source_fd, _directory(destination) as destination_fd: @@ -175,7 +190,10 @@ def _move_file(name: str, source: str | None, destination: str | None, *, overwr else: if not overwrite or not stat.S_ISREG(target.st_mode): raise ParseError("UNREADABLE_FILE") - os.replace(name, name, src_dir_fd=source_fd, dst_dir_fd=destination_fd) + if overwrite: + os.replace(name, name, src_dir_fd=source_fd, dst_dir_fd=destination_fd) + else: + _rename_without_replacement(name, source_fd, destination_fd) def _recover_staging() -> None: