Files
AI_Printer/tools/esptool-factory/factory_private_config.py

247 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
from typing import Any, Dict, Iterable, Optional, Tuple
from factory_common.signing import SignatureError, verify_file_signature
DEFAULT_CONFIG_NAME = "factory_private.json"
CONF_DIR_NAME = "conf"
CONFIG_ENV_KEYS: Tuple[str, ...] = ("FACTORY_PRIVATE_CONFIG", "FACTORY_PRIVATE_CONFIG_PATH")
CACHE_DIR_NAME = "talkingq-factory"
CACHE_FILENAME = "factory_private_path.txt"
_MISSING = object()
_ENC_MAGIC = b"FENC"
class EncryptedConfigError(Exception):
pass
def resolve_private_config_path(base_dir: Path, allow_plain: bool = True) -> Optional[Path]:
for key in CONFIG_ENV_KEYS:
value = os.getenv(key)
if value:
return Path(value).expanduser()
candidate = base_dir / CONF_DIR_NAME / DEFAULT_CONFIG_NAME
enc_candidate = Path(str(candidate) + ".enc")
if allow_plain and candidate.exists():
return candidate
if enc_candidate.exists():
return enc_candidate
return None
def resolve_private_config_source_path(base_dir: Path) -> Optional[Path]:
for key in CONFIG_ENV_KEYS:
value = os.getenv(key)
if value:
path = Path(value).expanduser()
if path.suffix == ".enc":
plain = Path(str(path)[:-4])
if plain.exists():
return plain
return path
candidate = base_dir / CONF_DIR_NAME / DEFAULT_CONFIG_NAME
if candidate.exists():
return candidate
enc_candidate = Path(str(candidate) + ".enc")
if enc_candidate.exists():
return enc_candidate
return None
def is_frozen() -> bool:
if getattr(sys, "frozen", False):
return True
if getattr(sys, "nuitka_version", None):
return True
if globals().get("__compiled__") is not None:
return True
return "__compiled__" in sys.modules
def _cache_dir() -> Path:
if os.name == "nt":
base = os.getenv("APPDATA") or os.getenv("LOCALAPPDATA")
if base:
return Path(base) / CACHE_DIR_NAME
return Path.home() / "AppData" / "Roaming" / CACHE_DIR_NAME
if sys.platform == "darwin":
return Path.home() / "Library" / "Application Support" / CACHE_DIR_NAME
base = os.getenv("XDG_STATE_HOME") or os.getenv("XDG_CONFIG_HOME")
if base:
return Path(base) / CACHE_DIR_NAME
return Path.home() / ".config" / CACHE_DIR_NAME
def _cache_path() -> Path:
return _cache_dir() / CACHE_FILENAME
def _load_cached_path() -> Optional[Path]:
path = _cache_path()
try:
raw = path.read_text(encoding="utf-8").strip()
except OSError:
return None
if not raw:
return None
return Path(raw).expanduser()
def _save_cached_path(path: Path) -> None:
cache_dir = _cache_dir()
try:
cache_dir.mkdir(parents=True, exist_ok=True)
_cache_path().write_text(str(path), encoding="utf-8")
except OSError:
return
def _prompt_private_config_path(title: str, initial_dir: Optional[Path] = None) -> Optional[Path]:
try:
import tkinter as tk
from tkinter import filedialog
except Exception:
return None
root = tk.Tk()
root.withdraw()
try:
root.attributes("-topmost", True)
except Exception:
pass
dialog_args = {
"title": title,
"filetypes": [("JSON", "*.json"), ("All", "*.*")],
}
if initial_dir:
dialog_args["initialdir"] = str(initial_dir)
path = filedialog.askopenfilename(**dialog_args)
root.destroy()
if not path:
return None
return Path(path).expanduser()
def ensure_private_config_path(base_dir: Path, prompt_title: str = "选择工厂私有配置文件") -> Path:
path = resolve_private_config_path(base_dir)
if path is not None and path.exists():
_save_cached_path(path)
return path
cached = _load_cached_path()
if cached is not None and cached.exists():
os.environ["FACTORY_PRIVATE_CONFIG"] = str(cached)
return cached
initial_dir = cached.parent if cached else None
selected = _prompt_private_config_path(prompt_title, initial_dir=initial_dir)
if selected is None:
raise FileNotFoundError(
"缺少工厂私有配置文件。请设置环境变量 "
f"{'/'.join(CONFIG_ENV_KEYS)} 或在弹窗中选择配置文件。"
)
os.environ["FACTORY_PRIVATE_CONFIG"] = str(selected)
_save_cached_path(selected)
return selected
def _is_encrypted_payload(payload: bytes) -> bool:
return payload.startswith(_ENC_MAGIC)
def _load_private_config_from_path(
path: Path,
verify_signature: bool = True,
) -> Dict[str, Any]:
if verify_signature:
try:
verify_file_signature(path)
except SignatureError as exc:
raise PermissionError(f"私有配置签名校验失败:{exc}") from exc
try:
payload = path.read_bytes()
except OSError as exc: # noqa: BLE001
raise FileNotFoundError(f"无法读取私有配置文件:{path}") from exc
if _is_encrypted_payload(payload):
raise EncryptedConfigError("私有配置已加密。当前版本仅支持明文配置文件。")
else:
try:
raw = payload.decode("utf-8")
except UnicodeDecodeError as exc:
raise ValueError(f"私有配置文件格式错误:{path}") from exc
try:
data = json.loads(raw)
except json.JSONDecodeError as exc:
raise ValueError(f"私有配置文件格式错误:{path}") from exc
if not isinstance(data, dict):
raise ValueError("私有配置文件根节点必须是对象。")
return data
def load_private_config(base_dir: Path) -> Dict[str, Any]:
path = resolve_private_config_path(base_dir, allow_plain=True)
if path is None or not path.exists():
raise FileNotFoundError(
"缺少工厂私有配置文件。请设置环境变量 "
f"{'/'.join(CONFIG_ENV_KEYS)} 指定配置路径。"
)
return _load_private_config_from_path(path)
def load_private_config_from_path(path: Path) -> Dict[str, Any]:
return _load_private_config_from_path(path)
def load_private_config_source(base_dir: Path) -> Dict[str, Any]:
path = resolve_private_config_source_path(base_dir)
if path is None or not path.exists():
raise FileNotFoundError(
"缺少工厂私有配置文件。请设置环境变量 "
f"{'/'.join(CONFIG_ENV_KEYS)} 指定配置路径。"
)
if path.suffix == ".enc":
raise EncryptedConfigError(
"私有配置源需要明文 factory_private.json打包用。请提供明文配置文件。"
)
return _load_private_config_from_path(
path,
verify_signature=False,
)
def _walk(data: Dict[str, Any], keys: Iterable[str]) -> Any:
current: Any = data
for key in keys:
if not isinstance(current, dict) or key not in current:
return _MISSING
current = current[key]
return current
def get_required(
data: Dict[str, Any],
keys: Iterable[str],
expected_type: Optional[tuple[type, ...]] = None,
allow_empty: bool = False,
) -> Any:
value = _walk(data, keys)
if value is _MISSING:
raise KeyError("缺少私有配置项: " + ".".join(keys))
if expected_type and not isinstance(value, expected_type):
raise TypeError("私有配置类型错误: " + ".".join(keys))
if not allow_empty and isinstance(value, str) and not value.strip():
raise ValueError("私有配置不能为空: " + ".".join(keys))
return value
def get_optional(data: Dict[str, Any], keys: Iterable[str], default: Any = None) -> Any:
value = _walk(data, keys)
if value is _MISSING:
return default
return value