Files
Jean-Pierre MartineauandClaude Haiku 5.5 723f7bdfef The identity key shows, imports from a file, and imports on an SPI radio
Show private key drew its warning and no key in the menu since 0.10.0.
On the uConsole and the Cardputer Zero, MeshTerm now writes an imported
key into the node's identity.key and restarts the node. An import also
reads a file (hex text or raw bytes), expands a 32-byte seed, refuses a
key of the wrong length before the confirmation, and names a firmware
that has its import turned off. The run log no longer stores the key of
config import-key (a migration removes the stored ones), and an exported
key file is written with mode 0600. Refer to #26.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-12 23:51:03 -04:00

63 lines
2.9 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""The only way that MeshTerm replaces a file that it owns: all the new contents, or none.
Each store in ``core/`` keeps its state in one file, and it writes that file again in full.
If a store writes in place, and a crash (or a laptop lid that closes) occurs during the
write, a truncated file is left where a contact list was. Thus each write goes to a
temporary file next to the real file first. Then one rename puts it in position. The
filesystem does the rename completely or not at all.
The name of the temporary file contains the id of the process that writes it. This detail
is necessary. All the copies of MeshTerm on the machine share its data directory: a
checkout, a downloaded build, or a second instance for a second companion. With a fixed
``.tmp`` name, when two copies write at the same time, each copy overwrites the same
temporary file in turn. Then each copy renames the contents of that file over the real
file. The rename is atomic, but the file that it renamed was not. With a name for each
process, the two writes are independent again, and the last rename wins. The result is a
lost change, not a corrupted file.
Use ``MESHTERM_HOME`` if you do not want copies to share the directory at all (refer to
: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 | bytes, *, owner_only: bool = False) -> None:
"""Replace ``path`` with ``text`` in one step, and make parent directories if necessary.
Args:
path: The file to replace. Its directory is created if it does not exist.
text: The complete new contents. A ``str`` is written as UTF-8, and ``bytes`` are
written as they are.
owner_only: Limit the file to the owner (``0600``) before it goes into position.
Use it for each file that holds a credential. The function applies it to the
temporary file, not to the destination. Thus the real file never exists with
wider permissions. This limit is best-effort: not all platforms or filesystems
obey it.
"""
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_suffix(f"{path.suffix}.{os.getpid()}.tmp")
try:
if isinstance(text, bytes):
tmp.write_bytes(text)
else:
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 temporary file behind. `missing_ok` is necessary,
# because the failure can be in the write that makes the temporary file.
tmp.unlink(missing_ok=True)
raise