Files
radar_system/python_app/file_io.py
T
2026-10-06 12:32:26 +03:00

102 lines
3.5 KiB
Python

"""Filesystem-safe names and atomic writes shared by storage and pipeline settings."""
from __future__ import annotations
import json
import os
import ctypes
from contextlib import suppress
from pathlib import Path
import stat
import re
from uuid import uuid4
_renameat2 = ctypes.CDLL(None, use_errno=True).renameat2
_renameat2.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]
_renameat2.restype = ctypes.c_int
def rename_no_replace(source: Path, destination: Path) -> None:
"""Publish a file or directory atomically, refusing to replace an existing name."""
# Linux AT_FDCWD=-100 and RENAME_NOREPLACE=1; also supported by FAT filesystems.
if _renameat2(-100, os.fsencode(source), -100, os.fsencode(destination), 1):
error = ctypes.get_errno()
raise OSError(error, os.strerror(error), str(destination))
def temporary_file(destination: Path) -> Path:
"""Create a sibling file using the destination's permissions or the current umask."""
path = destination.with_name(f".{destination.name}.{uuid4().hex}.tmp")
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666)
try:
try:
mode = stat.S_IMODE(destination.stat().st_mode)
except FileNotFoundError:
pass
else:
os.fchmod(descriptor, mode)
except BaseException:
path.unlink()
raise
finally:
os.close(descriptor)
return path
def sync_directory(path: Path) -> None:
"""Make directory entry changes durable after replacing or removing files."""
descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(descriptor)
finally:
os.close(descriptor)
def make_directory(path: Path, *, durable: bool = False) -> None:
"""Create missing parents, optionally syncing each newly created directory entry."""
if not durable:
path.mkdir(parents=True, exist_ok=True)
elif not path.is_dir():
make_directory(path.parent, durable=True)
path.mkdir(exist_ok=True)
sync_directory(path.parent)
def atomic_write_json(path: Path, payload: object, *, durable: bool = False) -> Path:
"""Serialize strict JSON before opening any files, then atomically commit it."""
return atomic_write_text(path, json.dumps(payload, indent=2, allow_nan=False), durable=durable)
def atomic_write_text(path: Path, text: str, *, durable: bool = False) -> Path:
"""Replace a text file only after a complete write, cleaning up failed writes.
Each call owns its temporary file, so concurrent writers cannot truncate or
rename one another's work. Durable writes also sync the file and directory;
frequent live settings can omit those syncs to avoid unnecessary disk I/O.
"""
make_directory(path.parent, durable=durable)
temporary = None
try:
temporary = temporary_file(path)
with temporary.open("w", encoding="utf-8") as handle:
handle.write(text)
if durable:
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
if durable:
sync_directory(path.parent)
finally:
if temporary is not None:
with suppress(OSError):
temporary.unlink(missing_ok=True)
return path
def sanitize_path_component(name: str) -> str:
"""Convert arbitrary string into filesystem-safe component."""
cleaned = re.sub(r"[^A-Za-z0-9._-]+", "_", name.strip())
cleaned = cleaned.strip("._")
return cleaned or "snapshot"