Harden JSON storage (atomic writes + crash-safe loads) + suggestions lightbulb
- storage.py: atomic save_json (temp+fsync+os.replace) and load_json that survives missing/corrupt files (moves corrupt aside). Closes #11/#12. - route all data modules + app.py helpers through storage (closes #31 core); fixes mutable DEFAULT_FUNDS/DEFAULT_CARS return (#23). - gunicorn --workers 1 --threads 4 to remove the write race (#13); strip Dockerfile template scaffolding (#40). - base.html: 💡 suggestions link in the shared nav (shows on every page). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
50
storage.py
Normal file
50
storage.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Crash-safe JSON persistence shared across all data modules.
|
||||
|
||||
- load_json: returns `default` if the file is missing, empty, or corrupt
|
||||
(e.g. a partial/interrupted write). A corrupt file is moved aside to
|
||||
`<path>.corrupt` so it can be inspected rather than silently overwritten.
|
||||
- save_json: atomic write — serialize to a temp file in the same directory,
|
||||
fsync, then os.replace() (atomic on POSIX). An interrupted write can never
|
||||
leave a truncated or empty target file.
|
||||
|
||||
This is the durability backbone for the app's JSON storage (issues #11/#12).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
DATA_DIR = os.environ.get("DATA_DIR", os.path.join(os.path.dirname(__file__), "data"))
|
||||
|
||||
|
||||
def load_json(path, default):
|
||||
if not os.path.exists(path):
|
||||
return default
|
||||
try:
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
# Corrupt/partial file — preserve it for inspection, fall back to default
|
||||
# instead of crashing the request.
|
||||
try:
|
||||
os.replace(path, path + ".corrupt")
|
||||
except OSError:
|
||||
pass
|
||||
return default
|
||||
|
||||
|
||||
def save_json(path, data):
|
||||
directory = os.path.dirname(path) or "."
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
fd, tmp = tempfile.mkstemp(dir=directory, prefix=".tmp-", suffix=".json")
|
||||
try:
|
||||
with os.fdopen(fd, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
os.replace(tmp, path)
|
||||
except BaseException:
|
||||
try:
|
||||
os.remove(tmp)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
Reference in New Issue
Block a user