fix(config): atomic write for .env to prevent API key loss on crash

save_env_value() used bare open('w') which truncates .env immediately.
A crash or OOM kill between truncation and completed write silently
wipes every credential in the file.

Write now goes to a temp file first, then os.replace() swaps it
atomically. Either the old .env exists or the new one does — never
a truncated half-write. Same pattern used in cron/jobs.py.

Cherry-picked from PR #842 by alireza78a, rebased onto current main
with conflict resolution (_secure_file refactor).

Co-authored-by: alireza78a <alireza78a@users.noreply.github.com>
This commit is contained in:
alireza78a
2026-03-11 08:58:33 -07:00
committed by teknium1
parent 66c0b719de
commit 3667138d05

View File

@@ -17,6 +17,7 @@ import platform
import stat import stat
import subprocess import subprocess
import sys import sys
import tempfile
from pathlib import Path from pathlib import Path
from typing import Dict, Any, Optional, List, Tuple from typing import Dict, Any, Optional, List, Tuple
@@ -958,8 +959,19 @@ def save_env_value(key: str, value: str):
lines[-1] += "\n" lines[-1] += "\n"
lines.append(f"{key}={value}\n") lines.append(f"{key}={value}\n")
with open(env_path, 'w', **write_kw) as f: fd, tmp_path = tempfile.mkstemp(dir=str(env_path.parent), suffix='.tmp', prefix='.env_')
f.writelines(lines) try:
with os.fdopen(fd, 'w', **write_kw) as f:
f.writelines(lines)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_path, env_path)
except BaseException:
try:
os.unlink(tmp_path)
except OSError:
pass
raise
_secure_file(env_path) _secure_file(env_path)
# Restrict .env permissions to owner-only (contains API keys) # Restrict .env permissions to owner-only (contains API keys)