diff --git a/src/aiokv/aiokv.py b/src/aiokv/aiokv.py index 2e0ce4f..2a8ad04 100644 --- a/src/aiokv/aiokv.py +++ b/src/aiokv/aiokv.py @@ -121,7 +121,7 @@ class AioKV: target = await asyncio.to_thread(os.path.realpath, self.file) directory = os.path.dirname(target) or "." await asyncio.to_thread(os.makedirs, directory, exist_ok=True) - await self._sweep_stale_tmp(directory) + await self._sweep_stale_tmp(directory, os.path.basename(target)) payload = await asyncio.to_thread(json.dumps, cache, allow_nan=False) tmp = f"{target}.{os.getpid()}.tmp" @@ -137,14 +137,15 @@ class AioKV: log.exception("aiokv: failed to clean up temp file %s", tmp) raise - async def _sweep_stale_tmp(self, directory: str) -> None: + async def _sweep_stale_tmp(self, directory: str, base: str) -> None: """remove orphaned ..tmp files left by a hard crash of a different, dead process matches only the exact shape _save() creates (..tmp), via a re.escape'd regex over a directory listing rather than a raw glob - a glob pattern both crosses unrelated dots (matching non-aiokv neighbors like foo.backup.999.tmp) and - mistreats glob metacharacters in the store's own filename (e.g. state[prod].json)""" - base = os.path.basename(await asyncio.to_thread(os.path.realpath, self.file)) + mistreats glob metacharacters in the store's own filename (e.g. state[prod].json). + `base` is the realpath'd basename _save() already resolved - passed in rather than + re-resolved here so a save does one realpath call, not two.""" candidate = re.compile(rf"^{re.escape(base)}\.(\d+)\.tmp$") names = await asyncio.to_thread(os.listdir, directory) for name in names: