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>
63 lines
2.9 KiB
Python
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
|