Rework radar system
This commit is contained in:
+67
-13
@@ -1,12 +1,66 @@
|
||||
"""Atomic writes for JSON settings shared by GUI and pipeline processes."""
|
||||
"""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 tempfile
|
||||
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:
|
||||
@@ -21,27 +75,27 @@ def atomic_write_text(path: Path, text: str, *, durable: bool = False) -> Path:
|
||||
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.
|
||||
"""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
make_directory(path.parent, durable=durable)
|
||||
temporary = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", encoding="utf-8", dir=path.parent,
|
||||
prefix=f".{path.name}.", suffix=".tmp", delete=False,
|
||||
) as handle:
|
||||
temporary = Path(handle.name)
|
||||
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:
|
||||
directory_fd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(directory_fd)
|
||||
finally:
|
||||
os.close(directory_fd)
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user