"""프록시용 config 로더. API 서비스와 동일한 `data/config.json` 파일을 공유 볼륨으로 읽는다. atomic rename(tempfile + os.replace) 으로 갱신되기 때문에 mtime polling 방식으로 안전하게 hot reload 가 가능하다. """ from __future__ import annotations import json import os import threading from pathlib import Path CONFIG_PATH = Path(os.environ.get("MC_CONFIG_PATH", "/data/config.json")) DEFAULT_CONFIG = { "proxy": {"listen_port": 25565, "enabled": True}, "backend": {"host": "127.0.0.1", "port": 25565}, "allowed_domains": [ {"domain": "mc.tkrmagid.kr", "enabled": True, "note": "메인 도메인"} ], "block_message": "이 서버는 허용된 도메인에서만 접속 가능합니다.", } _lock = threading.Lock() def load() -> dict: if not CONFIG_PATH.exists(): CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) save(DEFAULT_CONFIG) return dict(DEFAULT_CONFIG) with _lock: with CONFIG_PATH.open("r", encoding="utf-8") as f: return json.load(f) def save(cfg: dict) -> None: CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) tmp = CONFIG_PATH.with_suffix(".json.tmp") with _lock: with tmp.open("w", encoding="utf-8") as f: json.dump(cfg, f, ensure_ascii=False, indent=2) os.replace(tmp, CONFIG_PATH) def mtime() -> float: try: return CONFIG_PATH.stat().st_mtime except FileNotFoundError: return 0.0 def allowed_domain_set(cfg: dict) -> set[str]: return { d["domain"].lower().strip() for d in cfg.get("allowed_domains", []) if d.get("enabled", True) }