Initial commit: RIPE CIDR/FQDN collector
Collector daemon, FastAPI server (addresses, diff, collect, sources), SQLite storage with change journal, Docker Compose deployment, tests, documentation and project rules. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
commit
bcf8156085
45 files changed
+3134
No files matched your search
+70
@@ -0,0 +1,70 @@
|
||||
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
|
||||
Reference in new issue
Block a user