From 8607046fe994512b8f4178d0fd44e945955610cb Mon Sep 17 00:00:00 2001 From: disqualifier Date: Fri, 16 Oct 2026 04:01:21 -0400 Subject: [PATCH] fix: atomic save keeps the store's file permissions T1-9: _save wrote a fresh temp file with umask permissions and os.replace'd it over the target, so a chmod 600 store became 0644 after the first set(). copy the target's mode onto the temp file before the payload is written. Signed-off-by: disqualifier --- src/aiokv/aiokv.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/aiokv/aiokv.py b/src/aiokv/aiokv.py index d6edb4e..cb5fc9f 100644 --- a/src/aiokv/aiokv.py +++ b/src/aiokv/aiokv.py @@ -18,6 +18,7 @@ cross-process storage. import os import re +import shutil import json import time import asyncio @@ -117,7 +118,9 @@ class AioKV: """write atomically: temp file in the same dir, then os.replace over the realpath'd target (symlink-safe). process-crash safe (a crash mid-write leaves the prior good file intact), NOT power-loss safe - no fsync, so an OS/power failure can still lose - the last write (acceptable: reconstructible single-process state, not a db)""" + the last write (acceptable: reconstructible single-process state, not a db). an + existing target's permission bits are copied onto the temp file before the payload + is written, so a chmod 600 store stays 600 instead of taking the umask default""" 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) @@ -127,6 +130,8 @@ class AioKV: tmp = f"{target}.{os.getpid()}.tmp" try: async with aiofiles.open(tmp, mode="w", encoding="utf-8") as f: + if await asyncio.to_thread(os.path.exists, target): + await asyncio.to_thread(shutil.copymode, target, tmp) await f.write(payload) await asyncio.to_thread(os.replace, tmp, target) except Exception: