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 <dev@disqualifier.me>
This commit is contained in:
dsql committed 2026-10-16 04:01:21 -04:00
1 parent 4ba768a3bf
commit 8607046fe9
1 file changed
+6 -1
+6 -1
View File
@@ -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: