Fastwhisper / app /fsutil.py
Mbonea's picture
Deploy Habit Journal backend S0-S10 to Hugging Face Space.
990895d
Raw
History Blame Contribute Delete
3.53 kB
"""Provide locked JSON/JSONL persistence and atomic file replacement.
Mutations use a sibling lock file so append and full rewrites cannot overlap.
"""
import json
import os
from contextlib import contextmanager
from pathlib import Path
from typing import Any, Iterator
@contextmanager
def file_lock(path: Path) -> Iterator[None]:
"""Hold an exclusive process lock for a data path."""
lock_path = path.with_name(f"{path.name}.lock")
lock_path.parent.mkdir(parents=True, exist_ok=True)
with lock_path.open("a+b") as handle:
if os.name == "nt":
import msvcrt
if handle.tell() == 0:
handle.write(b"\0")
handle.flush()
handle.seek(0)
msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
else:
import fcntl
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
try:
yield
finally:
if os.name == "nt":
handle.seek(0)
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
else:
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
def atomic_write_text(path: Path, text: str) -> None:
"""Replace a file atomically after flushing its contents to disk."""
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(f"{path.name}.tmp")
with temporary.open("w", encoding="utf-8", newline="\n") as handle:
handle.write(text)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
def read_json(path: Path) -> dict[str, Any] | None:
"""Read a JSON object, returning None when the file is absent."""
if not path.exists():
return None
with path.open("r", encoding="utf-8") as handle:
value = json.load(handle)
if not isinstance(value, dict):
raise ValueError(f"{path.name} must contain a JSON object")
return value
def write_json(path: Path, value: dict[str, Any]) -> None:
"""Write a JSON object under a lock using atomic replacement."""
payload = json.dumps(value, ensure_ascii=False, indent=2) + "\n"
with file_lock(path):
atomic_write_text(path, payload)
def read_jsonl(path: Path) -> list[dict[str, Any]]:
"""Read non-empty JSONL records in file order."""
if not path.exists():
return []
records: list[dict[str, Any]] = []
with path.open("r", encoding="utf-8") as handle:
for line in handle:
if line.strip():
value = json.loads(line)
if not isinstance(value, dict):
raise ValueError(f"{path.name} contains a non-object record")
records.append(value)
return records
def append_jsonl(path: Path, value: dict[str, Any]) -> None:
"""Append one durable JSONL record while holding the file lock."""
path.parent.mkdir(parents=True, exist_ok=True)
line = json.dumps(value, ensure_ascii=False, separators=(",", ":")) + "\n"
with file_lock(path):
with path.open("a", encoding="utf-8", newline="\n") as handle:
handle.write(line)
handle.flush()
os.fsync(handle.fileno())
def rewrite_jsonl(path: Path, values: list[dict[str, Any]]) -> None:
"""Atomically replace a JSONL file while holding its lock."""
text = "".join(
json.dumps(value, ensure_ascii=False, separators=(",", ":")) + "\n"
for value in values
)
with file_lock(path):
atomic_write_text(path, text)