Files
MeshTerm/meshterm/core/atomicwrite.py
T
Jean-Pierre MartineauandClaude Fable 5.1 c9f5a71ce7 Every Python file names its licence with an SPDX identifier
Apache-2.0 does not require per-file headers, but a file lifted out of the tree carried no marking at all. One comment line, first (or right after a shebang), on every .py under meshterm/, tests/, packaging/ and scripts/, plus the extensionless SPI bridge; tests/test_spdx.py keeps a new file from landing without one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 13:09:18 -04:00

56 lines
2.6 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""THE way MeshTerm replaces a file it owns: all of the new contents, or none of them.
Every store under ``core/`` keeps its state in one file that it rewrites whole. Writing
in place means a crash — or a laptop lid closing — halfway through leaves a truncated
file where a contact list used to be, so each write goes to a neighbouring temporary file
first and lands with a single rename, which the filesystem either does or doesn't.
The neighbouring file's name carries the writing process's id. That is not fussiness:
MeshTerm's data directory is shared by every copy on the machine — a checkout, a
downloaded build, a second instance for a second radio — and a fixed ``.tmp`` name means
two of them writing at the same moment take turns clobbering one scratch file, then each
rename whatever is in it over the real one. The rename is atomic; the thing being renamed
was not. Per-process names make the two writes independent again, and the last rename
simply wins, which is a lost update rather than a corrupted file.
``MESHTERM_HOME`` is the way to avoid sharing the directory in the first place — see
:func:`meshterm.core.config.default_config_dir`.
"""
from __future__ import annotations
import os
import stat
from pathlib import Path
__all__ = ["write_atomically"]
def write_atomically(path: Path, text: str, *, owner_only: bool = False) -> None:
"""Replace ``path`` with ``text`` in one step, creating parent directories as needed.
Args:
path: The file to replace. Its directory is created if it does not exist.
text: The complete new contents, written as UTF-8.
owner_only: Restrict the file to the owner (``0600``) before it is put in place,
for anything holding a credential. Applied to the temporary file rather than
the destination, so there is no moment where the real file exists with wider
permissions. Best-effort: not every platform or filesystem honours it.
"""
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(f"{path.suffix}.{os.getpid()}.tmp")
try:
tmp.write_text(text, encoding="utf-8")
if owner_only:
try:
os.chmod(tmp, stat.S_IRUSR | stat.S_IWUSR)
except OSError: # pragma: no cover - platform-dependent
pass
tmp.replace(path)
except BaseException:
# A failed write leaves no litter behind. `missing_ok` because the failure may
# well have been the write that would have created it.
tmp.unlink(missing_ok=True)
raise