70 lines
2.3 KiB
Python
70 lines
2.3 KiB
Python
import fcntl
|
|||
|
|
import json
|
||
|
|
import logging
|
||
|
|
import os
|
||
|
|
import tempfile
|
||
|
|
import time
|
||
|
|
from contextlib import contextmanager
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
|
||
|
|
class StorageError(Exception):
|
||
|
|
"""Файл данных повреждён или недоступен."""
|
||
|
|
|
||
|
|
|
||
|
|
def load_json(path, default):
|
||
|
|
"""Читает JSON. Битый файл переименовывается в *.corrupt-<ts>, чтобы его не затёрли."""
|
||
|
|
if not os.path.exists(path):
|
||
|
|
return default
|
||
|
|
try:
|
||
|
|
with open(path, 'r') as f:
|
||
|
|
return json.load(f)
|
||
|
|
except json.JSONDecodeError as e:
|
||
|
|
backup = f"{path}.corrupt-{int(time.time())}"
|
||
|
|
os.replace(path, backup)
|
||
|
|
logger.error("Corrupted JSON in %s (%s); moved to %s", path, e, backup)
|
||
|
|
raise StorageError(f"{path} is corrupted") from e
|
||
|
|
except OSError as e:
|
||
|
|
raise StorageError(f"Cannot read {path}: {e}") from e
|
||
|
|
|
||
|
|
|
||
|
|
def save_json_atomic(path, data):
|
||
|
|
"""Пишет во временный файл в том же каталоге и подменяет оригинал через os.replace."""
|
||
|
|
directory = os.path.dirname(os.path.abspath(path))
|
||
|
|
fd, tmp_path = tempfile.mkstemp(dir=directory, prefix=os.path.basename(path) + ".", suffix=".tmp")
|
||
|
|
try:
|
||
|
|
with os.fdopen(fd, 'w') as f:
|
||
|
|
json.dump(data, f, indent=4)
|
||
|
|
f.flush()
|
||
|
|
os.fsync(f.fileno())
|
||
|
|
os.replace(tmp_path, path)
|
||
|
|
except BaseException:
|
||
|
|
if os.path.exists(tmp_path):
|
||
|
|
os.unlink(tmp_path)
|
||
|
|
raise
|
||
|
|
|
||
|
|
|
||
|
|
@contextmanager
|
||
|
|
def file_lock(path):
|
||
|
|
"""Межпроцессная блокировка (сборщик, CLI и API) через <path>.lock."""
|
||
|
|
with open(path + ".lock", 'w') as lock_file:
|
||
|
|
fcntl.flock(lock_file, fcntl.LOCK_EX)
|
||
|
|
try:
|
||
|
|
yield
|
||
|
|
finally:
|
||
|
|
fcntl.flock(lock_file, fcntl.LOCK_UN)
|
||
|
|
|
||
|
|
|
||
|
|
def try_lock(path):
|
||
|
|
"""Неблокирующая эксклюзивная блокировка (singleton-процесс).
|
||
|
|
|
||
|
|
Возвращает открытый файл (блокировка держится, пока он жив) или None, если занято.
|
||
|
|
"""
|
||
|
|
lock_file = open(path, 'w')
|
||
|
|
try:
|
||
|
|
fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||
|
|
except BlockingIOError:
|
||
|
|
lock_file.close()
|
||
|
|
return None
|
||
|
|
return lock_file
|