Files
VPSMaster/proxydoctor/reports.py
T

122 lines
4.1 KiB
Python

"""Local, exclusive report files and durable per-module recovery snapshots."""
from datetime import datetime, timezone
import json
import os
from pathlib import Path
import uuid
def utc_now():
return datetime.now(timezone.utc).isoformat()
def exclusive_text(path):
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, 'O_NOFOLLOW', 0)
fd = os.open(path, flags, 0o600)
return os.fdopen(fd, 'w', encoding='utf-8')
class ReportStore:
"""JSONL is authoritative for recovery; a final JSON is created on completion.
Only complete newline-terminated events are recovery records. No exception
strings are stored. Callers supply their redactor, including for checkpoints.
"""
def __init__(self, destination, redact):
if destination is None:
directory = Path('reports')
directory.mkdir(mode=0o700, exist_ok=True)
destination = directory / (datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
+ '-' + uuid.uuid4().hex + '.json')
self.path = Path(destination)
if os.path.lexists(self.path):
raise FileExistsError('Report destination exists')
self.journal_path = self.path.with_name(self.path.name + '.events.jsonl')
self.redact = redact
self.stream = exclusive_text(self.journal_path)
self.failed = False
self.bytes_written = 0
def checkpoint(self, report):
if self.failed:
return False
try:
event = {'event_version': 1, 'saved_at': utc_now(),
'snapshot': self.redact(report)}
body=json.dumps(event, ensure_ascii=False) + '\n'
size=len(body.encode('utf-8'))
if size>4*1024**2 or self.bytes_written+size>64*1024**2:
self.failed=True
return False
self.stream.write(body)
self.stream.flush()
os.fsync(self.stream.fileno())
self.bytes_written+=size
return True
except (OSError, ValueError, TypeError):
self.failed = True
return False
def finish(self, report):
saved = self.checkpoint(report)
try:
# Never replace a file created by another process during detection.
with exclusive_text(self.path) as output:
json.dump(self.redact(report), output, ensure_ascii=False, indent=2)
output.flush()
os.fsync(output.fileno())
except (OSError, ValueError, TypeError):
self.failed = True
return False
return saved
def close(self):
try:
self.stream.close()
except OSError:
self.failed = True
def recover_latest(path, max_bytes=64*1024**2, max_line_bytes=4*1024**2):
"""Read the last complete checkpoint, ignoring an interrupted final write.
Recovery files are local trusted inputs; this function does not contact any
service or execute content. A malformed line is not a successful checkpoint.
"""
latest = None
consumed = 0
with Path(path).open('rb') as stream:
while True:
line = stream.readline(max_line_bytes+1)
if not line:
break
consumed += len(line)
if consumed > max_bytes or len(line)>max_line_bytes:
raise ValueError('Recovery journal exceeds read budget')
if not line.endswith(b'\n'):
break
try:
event = json.loads(line)
if event.get('event_version') == 1 and isinstance(event.get('snapshot'), dict):
latest = event['snapshot']
except (ValueError, AttributeError):
continue
return latest
class UnavailableStore:
"""Continue terminal checks when local storage cannot be opened."""
def __init__(self, destination):
self.path = Path(destination) if destination else Path('reports')
self.journal_path = None
self.failed = True
def checkpoint(self, report):
return False
def finish(self, report):
return False
def close(self):
pass