_managed_files owns the file snapshot reader and public metadata helpers used by pinned directories, durable copies, and opaque snapshots; transaction staging owns read_target and same_identity_and_contents; dead helpers removed; hook_template no longer re-exports the guard generator; mypy targets win32 so _windows_security type-checks. Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
124 lines
4.3 KiB
Python
124 lines
4.3 KiB
Python
"""Durable, identity-bound file copy and replacement primitives."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import stat
|
|
from typing import Any, Optional, Tuple
|
|
|
|
from ._managed_files import same_file_metadata
|
|
from ._pinned_directory import PinnedDirectory
|
|
|
|
|
|
_COPY_CHUNK_BYTES = 1024 * 1024
|
|
FileIdentity = Tuple[int, int]
|
|
|
|
|
|
def _regular_identity(
|
|
metadata: os.stat_result,
|
|
expected: FileIdentity,
|
|
path: str,
|
|
label: str,
|
|
) -> None:
|
|
if (
|
|
not stat.S_ISREG(metadata.st_mode)
|
|
or stat.S_ISLNK(metadata.st_mode)
|
|
or (metadata.st_dev, metadata.st_ino) != expected
|
|
):
|
|
raise OSError(f"{label} changed while copying: {path}")
|
|
|
|
|
|
def copy_into_reserved(
|
|
directory: PinnedDirectory,
|
|
source_path: str,
|
|
destination_path: str,
|
|
source_identity: FileIdentity,
|
|
destination_identity: FileIdentity,
|
|
mode: int,
|
|
label: str,
|
|
maximum_bytes: Optional[int] = None,
|
|
) -> None:
|
|
"""Copy one physical file into an existing private reservation and fsync it."""
|
|
source_name = directory.name_for(source_path)
|
|
destination_name = directory.name_for(destination_path)
|
|
read_flags = os.O_RDONLY | getattr(os, "O_BINARY", 0)
|
|
read_flags |= getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
write_flags = os.O_WRONLY | getattr(os, "O_BINARY", 0)
|
|
write_flags |= getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
source_descriptor = -1
|
|
destination_descriptor = -1
|
|
try:
|
|
source_descriptor = directory.open_file(source_name, read_flags)
|
|
before = os.fstat(source_descriptor)
|
|
_regular_identity(before, source_identity, source_path, label)
|
|
destination_descriptor = directory.open_file(destination_name, write_flags)
|
|
destination = os.fstat(destination_descriptor)
|
|
_regular_identity(
|
|
destination,
|
|
destination_identity,
|
|
destination_path,
|
|
f"{label} destination",
|
|
)
|
|
os.ftruncate(destination_descriptor, 0)
|
|
total = 0
|
|
while True:
|
|
chunk = os.read(source_descriptor, _COPY_CHUNK_BYTES)
|
|
if not chunk:
|
|
break
|
|
total += len(chunk)
|
|
if maximum_bytes is not None and total > maximum_bytes:
|
|
raise ValueError(
|
|
f"{label} exceeds {maximum_bytes} bytes: {source_path}"
|
|
)
|
|
offset = 0
|
|
while offset < len(chunk):
|
|
written = os.write(destination_descriptor, chunk[offset:])
|
|
if written <= 0:
|
|
raise OSError(f"Copy {label} made no progress: {destination_path}")
|
|
offset += written
|
|
directory.chmod_descriptor(destination_descriptor, mode)
|
|
os.fsync(destination_descriptor)
|
|
after = os.fstat(source_descriptor)
|
|
if not same_file_metadata(after, before):
|
|
raise OSError(f"{label} changed while copying: {source_path}")
|
|
finally:
|
|
for descriptor in (destination_descriptor, source_descriptor):
|
|
if descriptor >= 0:
|
|
os.close(descriptor)
|
|
visible = directory.stat_file(source_name)
|
|
_regular_identity(visible, source_identity, source_path, label)
|
|
directory.sync()
|
|
|
|
|
|
def atomic_replace(
|
|
directory: PinnedDirectory,
|
|
source_path: str,
|
|
destination_path: str,
|
|
) -> None:
|
|
"""Atomically replace one same-directory name and persist its entry."""
|
|
if os.name == "nt":
|
|
import ctypes as ctypes_module
|
|
from ctypes import wintypes
|
|
|
|
ctypes: Any = ctypes_module
|
|
source = directory.path_for(directory.name_for(source_path))
|
|
destination = directory.path_for(directory.name_for(destination_path))
|
|
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
move = kernel32.MoveFileExW
|
|
move.argtypes = [wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.DWORD]
|
|
move.restype = wintypes.BOOL
|
|
replace_existing = 0x00000001
|
|
write_through = 0x00000008
|
|
if not move(source, destination, replace_existing | write_through):
|
|
error = ctypes.get_last_error()
|
|
raise OSError(f"Atomically replace {destination}: {ctypes.WinError(error)}")
|
|
return
|
|
directory.replace_file(
|
|
directory.name_for(source_path),
|
|
directory.name_for(destination_path),
|
|
)
|
|
directory.sync()
|
|
|
|
|
|
__all__ = ["atomic_replace", "copy_into_reserved"]
|