diff --git a/pyproject.toml b/pyproject.toml index 02cac6c..d44101f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "envelope_authorizer" -version = "0.1.0" +version = "0.1.1" description = "CLI key-authorization manager for envelope_crypto" requires-python = ">=3.10" dependencies = [ diff --git a/src/envelope_authorizer/__init__.py b/src/envelope_authorizer/__init__.py index 3dc1f76..485f44a 100644 --- a/src/envelope_authorizer/__init__.py +++ b/src/envelope_authorizer/__init__.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/src/envelope_authorizer/commands/list_keys.py b/src/envelope_authorizer/commands/list_keys.py index cfb569f..6ed66ac 100644 --- a/src/envelope_authorizer/commands/list_keys.py +++ b/src/envelope_authorizer/commands/list_keys.py @@ -19,11 +19,16 @@ def _can_authorize(crypto, doc) -> str: def _created(doc) -> str: - """format created_at as a UTC timestamp string, or '-' if absent""" + """format created_at as a UTC timestamp string, or '-' if absent/unparseable""" raw = doc.get("meta", {}).get("created_at") if not raw: return "-" - return datetime.fromtimestamp(int(raw), tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + try: + return datetime.fromtimestamp(int(raw), tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + except (TypeError, ValueError, OverflowError, OSError): + # one hand-edited/legacy doc with a bad created_at shouldn't abort the + # whole table render + return "-" def run(config, storage, args) -> None: diff --git a/src/envelope_authorizer/storage/json_store.py b/src/envelope_authorizer/storage/json_store.py index a0c0eb1..58e3c4e 100644 --- a/src/envelope_authorizer/storage/json_store.py +++ b/src/envelope_authorizer/storage/json_store.py @@ -8,6 +8,7 @@ setups; for dev->server flows across machines, use the mongo backend. import json import os +import tempfile from pathlib import Path from typing import List, Optional @@ -34,13 +35,27 @@ class JsonStore(StorageBackend): return data def _write(self, docs: List[dict]) -> None: - """atomically write the docs list (temp file then replace)""" + """atomically write the docs list (unique temp file then replace) + + the temp name is unique per write (tempfile.mkstemp in the same dir) so two + concurrent writers can't clobber a shared `.tmp`; os.replace is atomic on the + same filesystem. the temp is cleaned up if the write fails before replace. + """ self.path.parent.mkdir(parents=True, exist_ok=True) - tmp = self.path.with_suffix(self.path.suffix + ".tmp") - with open(tmp, "w", encoding="utf-8") as handle: - json.dump(docs, handle, indent=2) - handle.write("\n") - os.replace(tmp, self.path) + fd, tmp = tempfile.mkstemp( + dir=self.path.parent, prefix=self.path.name + ".", suffix=".tmp" + ) + try: + with os.fdopen(fd, "w", encoding="utf-8") as handle: + json.dump(docs, handle, indent=2) + handle.write("\n") + os.replace(tmp, self.path) + except BaseException: + try: + os.unlink(tmp) + except OSError: + pass + raise def get(self, fingerprint: str) -> Optional[dict]: """return the doc whose `_id` matches, or None"""