Files
update-server/app/services/admin_config_service.py
T

197 lines
8.9 KiB
Python

import os
from pathlib import Path
from fastapi import Request
from fastapi import HTTPException
PUBLIC_API_BASE_URL = os.getenv("PUBLIC_API_BASE_URL", "").strip().rstrip("/")
VALID_CLIENT_TOKEN = os.getenv("CLIENT_API_TOKEN")
TARGET_PLATFORM = os.getenv("TARGET_PLATFORM", "windows").strip().lower() or "windows"
TARGET_ARCH = os.getenv("TARGET_ARCH", "x64").strip() or "x64"
def platform_executable_path(value: str, fallback_base_name: str) -> str:
path = str(value or "").strip().replace(chr(92), "/").strip("/")
if not path:
path = fallback_base_name
parts = path.split("/")
leaf = parts[-1]
if TARGET_PLATFORM == "windows":
if "." not in leaf:
leaf += ".exe"
elif TARGET_PLATFORM == "linux":
if leaf.casefold().endswith(".exe"):
leaf = leaf[:-4]
parts[-1] = leaf
return "/".join(parts)
RELEASE_MAIN_EXECUTABLE = platform_executable_path(
os.getenv("RELEASE_MAIN_EXECUTABLE", ""),
"bin/SimCAE",
)
PUBLISH_MAX_REQUEST_BYTES = int(os.getenv("PUBLISH_MAX_REQUEST_MB", "4096")) * 1024 * 1024
PUBLISH_MAX_FILES = int(os.getenv("PUBLISH_MAX_FILES", "20000"))
PUBLISH_MAX_FIELDS = int(os.getenv("PUBLISH_MAX_FIELDS", str(PUBLISH_MAX_FILES + 100)))
def public_api_base_url(request: Request) -> str:
if PUBLIC_API_BASE_URL:
return PUBLIC_API_BASE_URL
forwarded_proto = request.headers.get("x-forwarded-proto", "").split(",")[0].strip()
forwarded_host = request.headers.get("x-forwarded-host", "").split(",")[0].strip()
scheme = forwarded_proto or request.url.scheme
host = forwarded_host or request.headers.get("host", "")
if host:
return f"{scheme}://{host}".rstrip("/")
return str(request.base_url).rstrip("/")
def default_client_main_executable() -> str:
path = RELEASE_MAIN_EXECUTABLE.strip().replace(chr(92), "/").strip("/")
if path.casefold().startswith("bin/"):
path = path[4:]
return platform_executable_path(path, "SimCAE")
def default_client_config_values(request: Request) -> dict:
return {
"client_token": VALID_CLIENT_TOKEN or "",
"launch_token": os.getenv("CLIENT_LAUNCH_TOKEN", "SimCAE_Launch_Token_2026_ChangeMe_32Bytes"),
"request_timeout_ms": os.getenv("CLIENT_REQUEST_TIMEOUT_MS", "5000"),
"temp_folder": os.getenv("CLIENT_TEMP_FOLDER", "update_temp"),
"install_root": os.getenv("CLIENT_INSTALL_ROOT", ".."),
"main_executable": platform_executable_path(
os.getenv("CLIENT_MAIN_EXECUTABLE", ""),
default_client_main_executable(),
),
"launcher_executable": platform_executable_path(os.getenv("CLIENT_LAUNCHER_EXECUTABLE", ""), "Launcher"),
"updater_executable": platform_executable_path(os.getenv("CLIENT_UPDATER_EXECUTABLE", ""), "Updater"),
"bootstrap_executable": platform_executable_path(os.getenv("CLIENT_BOOTSTRAP_EXECUTABLE", ""), "Bootstrap"),
"health_check_timeout_ms": os.getenv("CLIENT_HEALTH_CHECK_TIMEOUT_MS", "15000"),
"platform": TARGET_PLATFORM,
"arch": TARGET_ARCH,
}
def crash_report_test_values(api_base_url: str) -> dict:
base_url = api_base_url.rstrip("/")
crash_admin_token = os.getenv("CRASH_ADMIN_TOKEN", "").strip()
admin_token = os.getenv("ADMIN_TOKEN", "").strip()
crash_report_token = (
os.getenv("CRASH_REPORT_TOKEN")
or os.getenv("SIMCAE_CRASH_CLIENT_TOKEN")
or os.getenv("CLIENT_API_TOKEN")
or ""
)
crash_symbol_token = os.getenv("CRASH_SYMBOL_TOKEN") or os.getenv("SIMCAE_SYMBOL_TOKEN") or ""
effective_admin_token = crash_admin_token or admin_token
effective_admin_token_source = "CRASH_ADMIN_TOKEN" if crash_admin_token else "ADMIN_TOKEN"
return {
"说明": "仅用于后台管理员/测试人员联调崩溃报告接口,不要复制进客户端 app_config.json。",
"server_base_url": base_url,
"tokens": {
"CRASH_REPORT_TOKEN": {
"用途": "上传崩溃报告 /api/v1/crash-reports",
"token": crash_report_token,
},
"CRASH_SYMBOL_TOKEN": {
"用途": "上传符号包 /api/v1/symbols",
"token": crash_symbol_token,
},
"CRASH_ADMIN_TOKEN": {
"用途": "查询和下载崩溃报告",
"token": crash_admin_token,
"说明": "如果为空,查询/下载接口会使用 ADMIN_TOKEN。",
},
"ADMIN_TOKEN": {
"用途": "管理后台令牌;当 CRASH_ADMIN_TOKEN 为空时也用于查询/下载崩溃报告",
"token": admin_token,
},
},
"upload_crash_report": {
"url": f"{base_url}/api/v1/crash-reports",
"authorization_header": "Authorization: Bearer <CRASH_REPORT_TOKEN>",
"token_env": "CRASH_REPORT_TOKEN",
"token": crash_report_token,
"idempotency_key_rule": "Idempotency-Key 必须等于 metadata.clientReportId",
},
"upload_symbols": {
"url": f"{base_url}/api/v1/symbols",
"authorization_header": "Authorization: Bearer <CRASH_SYMBOL_TOKEN>",
"token_env": "CRASH_SYMBOL_TOKEN",
"token": crash_symbol_token,
},
"admin_query_and_download": {
"detail_url_template": f"{base_url}/api/v1/crash-reports/{{report_id}}",
"file_url_template": f"{base_url}/api/v1/crash-reports/{{report_id}}/files/{{file_name}}",
"authorization_header": f"Authorization: Bearer <{effective_admin_token_source}>",
"token_env": effective_admin_token_source,
"token": effective_admin_token,
"file_name_examples": ["metadata.json", "crash.dmp", "attachments.zip", "server.json"],
},
"limits": {
"CRASH_SERVICE_VERSION": os.getenv("CRASH_SERVICE_VERSION", "1.0.0"),
"CRASH_METADATA_MAX_KB": os.getenv("CRASH_METADATA_MAX_KB", "256"),
"CRASH_MINIDUMP_MAX_MB": os.getenv("CRASH_MINIDUMP_MAX_MB", "128"),
"CRASH_ATTACHMENTS_MAX_MB": os.getenv("CRASH_ATTACHMENTS_MAX_MB", "64"),
"CRASH_REQUEST_MAX_MB": os.getenv("CRASH_REQUEST_MAX_MB", "200"),
"CRASH_SYMBOLS_MAX_MB": os.getenv("CRASH_SYMBOLS_MAX_MB", "512"),
},
}
def crash_report_test_text(api_base_url: str) -> str:
values = crash_report_test_values(api_base_url)
tokens = values["tokens"]
lines = [
"# 说明:下面内容仅用于后台管理员/测试人员联调崩溃报告接口。",
"# 注意:不要复制进客户端 app_config.json,也不要放进客户端配置文件。",
"",
"# 服务端基础地址。curl 示例里的 SERVER 就填这个值。",
f"SERVER_BASE_URL={values['server_base_url']}",
"",
"# 上传崩溃报告使用的 Token。",
"# 用途:POST /api/v1/crash-reports",
"# 请求头写法:Authorization: Bearer <CRASH_REPORT_TOKEN>",
f"CRASH_REPORT_TOKEN={tokens['CRASH_REPORT_TOKEN']['token']}",
f"CRASH_REPORT_URL={values['upload_crash_report']['url']}",
"",
"# 上传符号包使用的 Token。",
"# 用途:POST /api/v1/symbols",
"# 请求头写法:Authorization: Bearer <CRASH_SYMBOL_TOKEN>",
f"CRASH_SYMBOL_TOKEN={tokens['CRASH_SYMBOL_TOKEN']['token']}",
f"CRASH_SYMBOL_URL={values['upload_symbols']['url']}",
"",
"# 查询和下载崩溃报告使用的 Token。",
"# 如果 CRASH_ADMIN_TOKEN 为空,就使用 ADMIN_TOKEN。",
"# 请求头写法:Authorization: Bearer <CRASH_ADMIN_TOKEN 或 ADMIN_TOKEN>",
f"CRASH_ADMIN_TOKEN={tokens['CRASH_ADMIN_TOKEN']['token']}",
f"ADMIN_TOKEN={tokens['ADMIN_TOKEN']['token']}",
f"CRASH_REPORT_DETAIL_URL_TEMPLATE={values['admin_query_and_download']['detail_url_template']}",
f"CRASH_REPORT_FILE_URL_TEMPLATE={values['admin_query_and_download']['file_url_template']}",
"",
"# 可下载文件名。",
"CRASH_REPORT_FILE_NAMES=metadata.json,crash.dmp,attachments.zip,server.json",
"",
"# 幂等规则:Idempotency-Key 必须等于 metadata.json 里的 clientReportId。",
"CRASH_IDEMPOTENCY_KEY_RULE=Idempotency-Key must equal metadata.clientReportId",
"",
"# 崩溃报告服务版本和大小限制。",
]
for key, value in values["limits"].items():
lines.append(f"{key}={value}")
return "\n".join(lines)
def normalize_executable_name(value: str) -> str:
normalized = str(value or "").strip().replace(chr(92), "/").strip("/")
if not normalized:
return ""
if normalized.startswith("/") or ":" in normalized or any(part in ("", ".", "..") for part in Path(normalized).parts):
raise HTTPException(status_code=400, detail="主程序文件名必须是安全的相对路径")
if not normalized.casefold().endswith(".exe"):
normalized += ".exe"
return normalized