From 9b3ca8f5ff38caf474ba9df7f0ebe869da03664c Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 14 Sep 2026 08:47:06 -0300 Subject: [PATCH] fix: support staged imports on Docker bind mounts --- ingestion/guided_imports.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/ingestion/guided_imports.py b/ingestion/guided_imports.py index d5f8e83..26c3b0b 100644 --- a/ingestion/guided_imports.py +++ b/ingestion/guided_imports.py @@ -1,6 +1,7 @@ """Safe discovery of local source documents for the guided import workflow.""" import ctypes +import errno import os import stat from contextlib import contextmanager @@ -159,19 +160,35 @@ def _validate_name(name: str) -> None: 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. + # Use the Linux atomic no-replace rename where the mounted filesystem supports + # it. Docker Desktop bind mounts can reject that flag, so preserve the same + # no-overwrite guarantee by linking before removing the source. try: - rename = ctypes.CDLL(None).renameat2 + library = ctypes.CDLL(None, use_errno=True) + rename = library.renameat2 except AttributeError: - raise ParseError("UNREADABLE_FILE") from None + rename = None + if rename is None: + _link_then_unlink(name, source_fd, destination_fd) + return 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 ParseError("UNREADABLE_FILE") + if rename(source_fd, encoded_name, destination_fd, encoded_name, 1) == 0: # RENAME_NOREPLACE + return + if ctypes.get_errno() in {errno.EINVAL, errno.ENOSYS, errno.EOPNOTSUPP}: + _link_then_unlink(name, source_fd, destination_fd) + return + raise ParseError("UNREADABLE_FILE") +def _link_then_unlink(name: str, source_fd: int, destination_fd: int) -> None: + try: + os.link(name, name, src_dir_fd=source_fd, dst_dir_fd=destination_fd, follow_symlinks=False) + os.unlink(name, dir_fd=source_fd) + except OSError: + raise ParseError("UNREADABLE_FILE") from None + 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: