Source code for fractal.util.filesystem
"""Functions for saving and loading on the file system."""
from __future__ import annotations
import os
import pathlib
import tempfile
from typing import Union
__all__ = []
# probe the process umask once at import, before threads exist -- os.umask is
# process-global, so a per-write zero-and-restore round-trip would race file
# creation in other threads
_UMASK = os.umask(0)
os.umask(_UMASK)
[docs]
def write_atomic(path: pathlib.Path, data: Union[str, bytes]) -> None:
"""Write ``data`` to ``path`` atomically (unique temp + ``os.replace``).
A plain ``write_text`` truncates before writing, so a concurrent
reader can observe an empty or partial file and a crash mid-write
leaves a torn one. Staging to a dot-prefixed temp file in the
target's directory and swapping it into place makes every read
all-or-nothing. Text writes land utf-8 with LF endings verbatim.
A symlinked destination resolves first, so the swap updates the
link's target and the link itself survives.
``mkstemp`` creates the temp ``0600`` and ``os.replace`` carries
that mode onto the target, so an existing target's mode is
preserved explicitly and a fresh file honors the umask.
Args:
path: Destination file.
data: Payload -- ``str`` lands utf-8 with LF endings, ``bytes`` verbatim.
"""
# resolve so a symlinked destination is written through, not replaced:
# os.replace swaps the final path component, which would swap out the
# link itself and strand its target stale
path = path.resolve()
fd, tmp = tempfile.mkstemp(
dir=path.parent,
prefix=f'.{path.name}-',
suffix='.tmp',
)
try:
if isinstance(data, bytes):
with os.fdopen(fd, 'wb') as handle:
handle.write(data)
else:
with os.fdopen(fd, 'w', encoding='utf-8', newline='\n') as handle:
handle.write(data)
if path.exists():
os.chmod(tmp, path.stat().st_mode & 0o777)
else:
os.chmod(tmp, 0o666 & ~_UMASK)
os.replace(tmp, path)
finally:
if os.path.exists(tmp):
os.unlink(tmp)