feat: 一键安装脚本 + PHP完全移除 + config.php→config.json
新增文件: - deploy/install.sh: 7步一键安装(自动检测宝塔/标准模式) - deploy/upgrade.sh: git pull + pip install + restart + 健康检查 - deploy/uninstall.sh: 停服务+删配置, 可选--purge删部署目录 - docs/install-guide.md: 完整中文安装教程(宝塔+手动) 核心改动: - install.py: 宝塔Supervisor路径自适应, init-db返回bt_panel标记, config.php→config.json, 移除PHP锁文件, 生成Fernet加密密钥 - install.html: Step 5根据btPanel显示不同Supervisor/Nginx/SSL指引 - crypto.py: 移除PHP AES-CBC兼容层, 纯Fernet加密 - 删除 web/install.php (1192行PHP, 功能已完全迁移到Python) PHP清理: - Nginx配置: config\.php→config\.json deny规则 - config.py/main.py/migrations.py: install.php注释→install wizard - CLAUDE.md/AGENTS.md: config.php→config.json, 移除PHP引用 - firefox_server.py: login.php→login.html默认URL Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
+58
-64
@@ -29,10 +29,9 @@ router = APIRouter(prefix="/api/install", tags=["install"])
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
ENV_FILE = ROOT_DIR / ".env"
|
||||
LOCK_FILE = ROOT_DIR / "web" / "install.php.locked" # Legacy compat — primary lock is .install_locked
|
||||
INSTALL_LOCK = ROOT_DIR / ".install_locked"
|
||||
CONFIG_PHP_DIR = ROOT_DIR / "web" / "data"
|
||||
CONFIG_PHP = CONFIG_PHP_DIR / "config.php"
|
||||
CONFIG_DIR = ROOT_DIR / "web" / "data"
|
||||
CONFIG_JSON = CONFIG_DIR / "config.json"
|
||||
STATE_FILE = ROOT_DIR / ".install_state.json"
|
||||
|
||||
|
||||
@@ -41,7 +40,7 @@ def _is_installed() -> bool:
|
||||
|
||||
|
||||
def _is_locked() -> bool:
|
||||
return INSTALL_LOCK.exists() or LOCK_FILE.exists()
|
||||
return INSTALL_LOCK.exists()
|
||||
|
||||
|
||||
# ── Pydantic Models ──
|
||||
@@ -76,16 +75,6 @@ class CreateAdminRequest(BaseModel):
|
||||
|
||||
# ── Helpers ──
|
||||
|
||||
def _escape_php_string(s: str) -> str:
|
||||
"""Escape a string for safe insertion into a PHP single-quoted string.
|
||||
|
||||
In PHP single-quoted strings, only ``\\`` and ``'`` are special escapes.
|
||||
A user value containing ``'`` could break out of the define() and inject
|
||||
arbitrary PHP code (RCE). This function neutralises that vector.
|
||||
"""
|
||||
return s.replace("\\", "\\\\").replace("'", "\\'")
|
||||
|
||||
|
||||
def _make_db_url(req: InitDbRequest | CreateAdminRequest) -> str:
|
||||
return (
|
||||
f"mysql+aiomysql://{req.db_user}:{quote_plus(req.db_pass)}"
|
||||
@@ -171,42 +160,48 @@ def _write_env(req: InitDbRequest, secret_key: str, api_key: str, encryption_key
|
||||
logger.info(f".env written to {ENV_FILE}")
|
||||
|
||||
|
||||
def _write_config_php(req: InitDbRequest, api_key: str, site_url: str) -> None:
|
||||
CONFIG_PHP_DIR.mkdir(parents=True, exist_ok=True)
|
||||
def _write_config_json(req: InitDbRequest, api_key: str, site_url: str) -> None:
|
||||
"""Write shared configuration as JSON (replaces legacy config.php)."""
|
||||
import json
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
install_dir = str(ROOT_DIR)
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
lines = [
|
||||
"<?php",
|
||||
"// Nexus Configuration — Auto-generated by installer",
|
||||
f"// {now}",
|
||||
"",
|
||||
f"define('API_BASE_URL', '{_escape_php_string(site_url)}');",
|
||||
f"define('API_KEY', '{_escape_php_string(api_key)}');",
|
||||
f"define('DB_HOST', '{_escape_php_string(req.db_host)}');",
|
||||
f"define('DB_PORT', '{_escape_php_string(req.db_port)}');",
|
||||
f"define('DB_NAME', '{_escape_php_string(req.db_name)}');",
|
||||
f"define('DB_USER', '{_escape_php_string(req.db_user)}');",
|
||||
f"define('DB_PASS', '{_escape_php_string(req.db_pass)}');",
|
||||
"define('APP_NAME', 'Nexus');",
|
||||
"define('APP_VERSION', '6.0.0');",
|
||||
"define('SESSION_LIFETIME', 28800);",
|
||||
f"define('DEPLOY_ROOT', '{_escape_php_string(install_dir)}');",
|
||||
"",
|
||||
f"date_default_timezone_set('{_escape_php_string(req.timezone)}');",
|
||||
"",
|
||||
]
|
||||
config = {
|
||||
"_generated": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC"),
|
||||
"_generator": "Nexus 6.0 Installer",
|
||||
"api_base_url": site_url,
|
||||
"api_key": api_key,
|
||||
"db_host": req.db_host,
|
||||
"db_port": req.db_port,
|
||||
"db_name": req.db_name,
|
||||
"db_user": req.db_user,
|
||||
"db_pass": req.db_pass,
|
||||
"app_name": "Nexus",
|
||||
"app_version": "6.0.0",
|
||||
"session_lifetime": 28800,
|
||||
"deploy_root": install_dir,
|
||||
"timezone": req.timezone,
|
||||
}
|
||||
|
||||
CONFIG_PHP.write_text("\n".join(lines))
|
||||
logger.info(f"config.php written to {CONFIG_PHP}")
|
||||
CONFIG_JSON.write_text(json.dumps(config, indent=2, ensure_ascii=False))
|
||||
logger.info(f"config.json written to {CONFIG_JSON}")
|
||||
|
||||
|
||||
def _detect_bt_panel() -> bool:
|
||||
"""Detect if BT Panel (宝塔面板) is installed on this system."""
|
||||
return Path("/www/server/panel/class/common.py").exists()
|
||||
|
||||
|
||||
def _configure_guardian(install_dir: str, api_port: str) -> list[str]:
|
||||
"""Best-effort: configure Supervisor, crontab, health_monitor.sh"""
|
||||
results: list[str] = []
|
||||
bt_panel = _detect_bt_panel()
|
||||
|
||||
# 1. Log directory
|
||||
log_dir = Path("/var/log/nexus")
|
||||
if bt_panel:
|
||||
log_dir = Path("/www/wwwlogs")
|
||||
else:
|
||||
log_dir = Path("/var/log/nexus")
|
||||
try:
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
results.append(f"✓ 日志目录已创建 {log_dir}")
|
||||
@@ -214,6 +209,11 @@ def _configure_guardian(install_dir: str, api_port: str) -> list[str]:
|
||||
results.append(f"✗ 日志目录创建失败(需 root 权限)")
|
||||
|
||||
# 2. Supervisor config
|
||||
if bt_panel:
|
||||
conf_path = Path("/www/server/panel/plugin/supervisor/profile/nexus.ini")
|
||||
else:
|
||||
conf_path = Path("/etc/supervisor/conf.d/nexus.conf")
|
||||
|
||||
supervisor_conf = (
|
||||
f"[program:nexus]\n"
|
||||
f"command={install_dir}/venv/bin/uvicorn server.main:app --host 0.0.0.0 --port {api_port}\n"
|
||||
@@ -226,18 +226,18 @@ def _configure_guardian(install_dir: str, api_port: str) -> list[str]:
|
||||
f"stopwaitsecs=10\n"
|
||||
f"stopsignal=INT\n"
|
||||
f"environment=PATH=\"{install_dir}/venv/bin:/usr/local/bin:%(ENV_PATH)s\",HOME=\"/root\"\n"
|
||||
f"stderr_logfile=/var/log/nexus/error.log\n"
|
||||
f"stdout_logfile=/var/log/nexus/access.log\n"
|
||||
f"stderr_logfile={log_dir}/nexus_error.log\n"
|
||||
f"stdout_logfile={log_dir}/nexus_access.log\n"
|
||||
f"stderr_logfile_maxbytes=50MB\n"
|
||||
f"stdout_logfile_maxbytes=50MB\n"
|
||||
f"stderr_logfile_backups=5\n"
|
||||
f"stdout_logfile_backups=5\n"
|
||||
)
|
||||
conf_path = Path("/etc/supervisor/conf.d/nexus.conf")
|
||||
try:
|
||||
conf_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
conf_path.write_text(supervisor_conf)
|
||||
results.append(f"✓ Supervisor 配置已写入 {conf_path}")
|
||||
mode_label = "宝塔" if bt_panel else "标准"
|
||||
results.append(f"✓ Supervisor 配置已写入 {conf_path}({mode_label}模式)")
|
||||
except OSError:
|
||||
results.append("✗ Supervisor 配置写入失败(需 root 权限)")
|
||||
|
||||
@@ -379,7 +379,7 @@ async def env_check():
|
||||
})
|
||||
|
||||
# Write permissions
|
||||
data_writable = CONFIG_PHP_DIR.exists() and os.access(CONFIG_PHP_DIR, os.W_OK) or \
|
||||
data_writable = CONFIG_DIR.exists() and os.access(CONFIG_DIR, os.W_OK) or \
|
||||
os.access(ROOT_DIR / "web", os.W_OK)
|
||||
root_writable = os.access(ROOT_DIR, os.W_OK)
|
||||
checks.append({
|
||||
@@ -498,8 +498,8 @@ async def init_db(req: InitDbRequest):
|
||||
site_url = req.site_url.rstrip("/") if req.site_url else f"http://127.0.0.1:{req.api_port}"
|
||||
_write_env(req, secret_key, api_key, encryption_key, pool_size, max_overflow, redis_url, site_url)
|
||||
|
||||
# Write config.php
|
||||
_write_config_php(req, api_key, site_url)
|
||||
# Write config.json
|
||||
_write_config_json(req, api_key, site_url)
|
||||
|
||||
# Configure process guardian (best-effort)
|
||||
install_dir = str(ROOT_DIR)
|
||||
@@ -530,6 +530,7 @@ async def init_db(req: InitDbRequest):
|
||||
"max_overflow": max_overflow,
|
||||
"tables_created": 14,
|
||||
"guardian_results": guardian_results,
|
||||
"bt_panel": _detect_bt_panel(),
|
||||
}
|
||||
|
||||
|
||||
@@ -588,16 +589,15 @@ async def create_admin(req: CreateAdminRequest):
|
||||
except Exception as e:
|
||||
raise HTTPException(400, f"创建管理员失败: {e}")
|
||||
|
||||
# Update config.php with brand
|
||||
if CONFIG_PHP.exists():
|
||||
import re
|
||||
content = CONFIG_PHP.read_text()
|
||||
content = re.sub(
|
||||
r"define\('APP_NAME', '.*?'\);",
|
||||
f"define('APP_NAME', '{_escape_php_string(req.system_name)}');",
|
||||
content,
|
||||
)
|
||||
CONFIG_PHP.write_text(content)
|
||||
# Update config.json with brand
|
||||
if CONFIG_JSON.exists():
|
||||
import json
|
||||
try:
|
||||
config = json.loads(CONFIG_JSON.read_text())
|
||||
config["app_name"] = req.system_name
|
||||
CONFIG_JSON.write_text(json.dumps(config, indent=2, ensure_ascii=False))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Update .env with brand
|
||||
if ENV_FILE.exists():
|
||||
@@ -654,17 +654,11 @@ async def lock_install():
|
||||
logger.warning("Failed to verify admin account before locking: %s", e)
|
||||
# If DB is unreachable, allow lock (admin may have been created but DB is temporarily down)
|
||||
|
||||
# Create lock marker files
|
||||
# Create lock marker file
|
||||
INSTALL_LOCK.write_text(
|
||||
f"Nexus installation locked at {datetime.now(timezone.utc).isoformat()}\n"
|
||||
)
|
||||
|
||||
# Also create the legacy lock file (install.php.locked) for compatibility
|
||||
try:
|
||||
LOCK_FILE.write_text("locked\n")
|
||||
except OSError as e:
|
||||
logger.warning("Failed to write legacy lock file: %s", e)
|
||||
|
||||
# Clean up state file
|
||||
if STATE_FILE.exists():
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user