215 lines
6.7 KiB
Python
215 lines
6.7 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
from app.core.config import settings
|
|
|
|
|
|
def domain_root() -> Path:
|
|
return Path(settings.domain_root)
|
|
|
|
|
|
def resolve_domain_path(*relative_paths: str) -> Path | None:
|
|
root = domain_root()
|
|
candidates: list[Path] = []
|
|
seen: set[str] = set()
|
|
for relative_path in relative_paths:
|
|
text = str(relative_path or "").strip()
|
|
if not text:
|
|
continue
|
|
candidate = root / text
|
|
candidate_key = str(candidate)
|
|
if candidate_key not in seen:
|
|
seen.add(candidate_key)
|
|
candidates.append(candidate)
|
|
if "/" not in text and "\\" not in text:
|
|
nested_candidate = root / "logs" / text
|
|
nested_key = str(nested_candidate)
|
|
if nested_key not in seen:
|
|
seen.add(nested_key)
|
|
candidates.append(nested_candidate)
|
|
for candidate in candidates:
|
|
if candidate.exists():
|
|
return candidate
|
|
return candidates[0] if candidates else None
|
|
|
|
|
|
def read_json(relative_path: str, default: dict | list | None = None):
|
|
path = domain_root() / relative_path
|
|
if not path.exists():
|
|
return {} if default is None else default
|
|
try:
|
|
with path.open("r", encoding="utf-8") as handle:
|
|
return json.load(handle)
|
|
except (json.JSONDecodeError, OSError):
|
|
return {} if default is None else default
|
|
|
|
|
|
def write_json(relative_path: str, payload) -> None:
|
|
path = domain_root() / relative_path
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
with path.open("w", encoding="utf-8") as handle:
|
|
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
|
|
|
|
|
def tail_lines(relative_path: str, max_lines: int = 120) -> list[str]:
|
|
path = resolve_domain_path(relative_path)
|
|
if path is None or not path.exists():
|
|
return []
|
|
with path.open("r", encoding="utf-8", errors="replace") as handle:
|
|
return handle.read().splitlines()[-max_lines:]
|
|
|
|
|
|
def runtime_root() -> Path:
|
|
env_override = str(os.getenv("DOMAIN_API_RUNTIME_ROOT", "") or "").strip()
|
|
candidates: list[Path] = []
|
|
if env_override:
|
|
candidates.append(Path(env_override))
|
|
|
|
base_dir = Path(__file__).resolve().parents[2]
|
|
for parent in base_dir.parents:
|
|
if parent.name != "releases":
|
|
continue
|
|
# Released builds live under /opt/domaincheck/releases/<release>/domain-api.
|
|
# Runtime state must not be written back into the immutable release tree,
|
|
# otherwise sync-agent / detect runtime snapshots fail with permission errors.
|
|
candidates.append(parent.parent / "runtime" / "domain-api")
|
|
break
|
|
|
|
candidates.append(base_dir / "runtime")
|
|
|
|
last_error: OSError | None = None
|
|
for candidate in candidates:
|
|
try:
|
|
candidate.mkdir(parents=True, exist_ok=True)
|
|
return candidate
|
|
except OSError as exc:
|
|
last_error = exc
|
|
continue
|
|
|
|
if last_error is not None:
|
|
raise last_error
|
|
raise RuntimeError("failed to resolve runtime root")
|
|
|
|
|
|
def read_runtime_json(filename: str, default: dict | list | None = None):
|
|
path = runtime_root() / filename
|
|
if not path.exists():
|
|
return {} if default is None else default
|
|
try:
|
|
with path.open("r", encoding="utf-8") as handle:
|
|
return json.load(handle)
|
|
except (json.JSONDecodeError, OSError):
|
|
return {} if default is None else default
|
|
|
|
|
|
def write_runtime_json(filename: str, payload) -> None:
|
|
path = runtime_root() / filename
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
with path.open("w", encoding="utf-8") as handle:
|
|
json.dump(payload, handle, ensure_ascii=False, indent=2, default=str)
|
|
|
|
|
|
def exports_root() -> Path:
|
|
path = runtime_root() / "exports"
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
return path
|
|
|
|
|
|
def import_root() -> Path:
|
|
path = runtime_root() / "imports"
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
return path
|
|
|
|
|
|
def settings_backup_root() -> Path:
|
|
path = runtime_root() / "settings_backups"
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
return path
|
|
|
|
|
|
def load_export_records() -> list[dict]:
|
|
path = runtime_root() / "export_tasks.json"
|
|
if not path.exists():
|
|
return []
|
|
try:
|
|
with path.open("r", encoding="utf-8") as handle:
|
|
return json.load(handle)
|
|
except (json.JSONDecodeError, OSError):
|
|
return []
|
|
|
|
|
|
def save_export_record(record: dict) -> None:
|
|
records = load_export_records()
|
|
records.insert(0, record)
|
|
path = runtime_root() / "export_tasks.json"
|
|
with path.open("w", encoding="utf-8") as handle:
|
|
json.dump(records[:200], handle, ensure_ascii=False, indent=2, default=str)
|
|
|
|
|
|
def load_import_records() -> list[dict]:
|
|
path = runtime_root() / "import_tasks.json"
|
|
if not path.exists():
|
|
return []
|
|
try:
|
|
with path.open("r", encoding="utf-8") as handle:
|
|
return json.load(handle)
|
|
except (json.JSONDecodeError, OSError):
|
|
return []
|
|
|
|
|
|
def save_import_records(records: list[dict]) -> None:
|
|
path = runtime_root() / "import_tasks.json"
|
|
with path.open("w", encoding="utf-8") as handle:
|
|
json.dump(records[:200], handle, ensure_ascii=False, indent=2, default=str)
|
|
|
|
|
|
def load_juming_records() -> list[dict]:
|
|
path = runtime_root() / "juming_tasks.json"
|
|
if not path.exists():
|
|
return []
|
|
try:
|
|
with path.open("r", encoding="utf-8") as handle:
|
|
return json.load(handle)
|
|
except (json.JSONDecodeError, OSError):
|
|
return []
|
|
|
|
|
|
def save_juming_records(records: list[dict]) -> None:
|
|
path = runtime_root() / "juming_tasks.json"
|
|
with path.open("w", encoding="utf-8") as handle:
|
|
json.dump(records[:200], handle, ensure_ascii=False, indent=2, default=str)
|
|
|
|
|
|
def load_detect_records() -> list[dict]:
|
|
path = runtime_root() / "detect_runs.json"
|
|
if not path.exists():
|
|
return []
|
|
try:
|
|
with path.open("r", encoding="utf-8") as handle:
|
|
return json.load(handle)
|
|
except (json.JSONDecodeError, UnicodeDecodeError, OSError):
|
|
try:
|
|
raw = path.read_bytes()
|
|
text = raw.decode("utf-8", errors="replace")
|
|
decoder = json.JSONDecoder()
|
|
payload, _ = decoder.raw_decode(text)
|
|
if isinstance(payload, list):
|
|
return payload
|
|
except Exception:
|
|
pass
|
|
return []
|
|
|
|
|
|
def save_detect_records(records: list[dict]) -> None:
|
|
path = runtime_root() / "detect_runs.json"
|
|
with path.open("w", encoding="utf-8") as handle:
|
|
json.dump(records[:200], handle, ensure_ascii=False, indent=2, default=str)
|
|
|
|
|
|
def timestamp_filename(prefix: str, ext: str) -> str:
|
|
return f"{prefix}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.{ext}"
|