Files
LocalNav/scripts/deploy.py
T
dekun 3149770887 Add system settings, one-click deploy, and simplify embed UI.
Remove Gate scout auto-seeding and the manual hub login button while keeping Gate login and instance SSO. Add password change and database backup/restore, plus deploy scripts with env auto-generation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 11:44:12 +08:00

218 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""LocalNav 一键部署:环境检测、生成 .env、安装依赖。"""
from __future__ import annotations
import os
import platform
import secrets
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
ENV_EXAMPLE = ROOT / ".env.example"
ENV_FILE = ROOT / ".env"
VENV_DIR = ROOT / ".venv"
REQUIREMENTS = ROOT / "requirements.txt"
MIN_PYTHON = (3, 10)
DEFAULTS = {
"NAV_ADMIN_USERNAME": "admin",
"NAV_ADMIN_PASSWORD": "admin123",
"NAV_HUB_AUTO_LOGIN": "1",
"NAV_COOKIES_INSECURE_HTTP": "1",
}
def _ok(msg: str) -> None:
print(f"[OK] {msg}")
def _warn(msg: str) -> None:
print(f"[WARN] {msg}")
def _fail(msg: str) -> None:
print(f"[FAIL] {msg}", file=sys.stderr)
sys.exit(1)
def check_python() -> str:
ver = sys.version_info
if ver < MIN_PYTHON:
_fail(f"需要 Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]}+,当前 {ver.major}.{ver.minor}")
_ok(f"Python {ver.major}.{ver.minor}.{ver.micro}")
return sys.executable
def check_pip(py_exe: str) -> None:
try:
subprocess.run(
[py_exe, "-m", "pip", "--version"],
check=True,
capture_output=True,
text=True,
)
_ok("pip 可用")
except (subprocess.CalledProcessError, FileNotFoundError):
_fail("pip 不可用,请先安装 pip")
def optional_tools() -> None:
for name in ("git", "pm2"):
if shutil.which(name):
_ok(f"可选工具 {name} 已安装")
else:
_warn(f"未检测到 {name}(可选)")
def read_env_lines(path: Path) -> list[str]:
if not path.is_file():
return []
return path.read_text(encoding="utf-8-sig").splitlines()
def parse_env_keys(lines: list[str]) -> dict[str, str]:
out: dict[str, str] = {}
for line in lines:
s = line.strip()
if not s or s.startswith("#") or "=" not in s:
continue
key, _, val = s.partition("=")
out[key.strip()] = val.strip()
return out
def write_env(lines: list[str], updates: dict[str, str]) -> None:
existing = parse_env_keys(lines)
merged = {**existing, **updates}
out_lines: list[str] = []
written: set[str] = set()
for line in lines:
s = line.strip()
if not s or s.startswith("#") or "=" not in s:
out_lines.append(line)
continue
key, _, _ = s.partition("=")
key = key.strip()
if key in merged and key not in written:
out_lines.append(f"{key}={merged[key]}")
written.add(key)
else:
out_lines.append(line)
for key, val in merged.items():
if key not in written:
out_lines.append(f"{key}={val}")
ENV_FILE.write_text("\n".join(out_lines) + "\n", encoding="utf-8")
def ensure_env() -> None:
if not ENV_FILE.is_file():
if ENV_EXAMPLE.is_file():
shutil.copy2(ENV_EXAMPLE, ENV_FILE)
_ok("已从 .env.example 创建 .env")
else:
ENV_FILE.write_text("", encoding="utf-8")
_ok("已创建空 .env")
lines = read_env_lines(ENV_FILE)
parsed = parse_env_keys(lines)
updates: dict[str, str] = {}
secret = parsed.get("NAV_SECRET_KEY", "").strip()
if not secret:
updates["NAV_SECRET_KEY"] = secrets.token_hex(32)
_ok("已自动生成 NAV_SECRET_KEY")
for key, val in DEFAULTS.items():
if not parsed.get(key, "").strip():
updates[key] = val
if updates:
write_env(lines, updates)
_ok(f"已写入 .env 配置项:{', '.join(updates.keys())}")
else:
_ok(".env 已完整,未覆盖现有配置")
def venv_python() -> str:
if platform.system() == "Windows":
py = VENV_DIR / "Scripts" / "python.exe"
else:
py = VENV_DIR / "bin" / "python"
return str(py)
def ensure_venv(base_py: str) -> str:
if not VENV_DIR.is_dir():
subprocess.run([base_py, "-m", "venv", str(VENV_DIR)], check=True)
_ok(f"已创建虚拟环境 {VENV_DIR}")
else:
_ok("虚拟环境已存在")
return venv_python()
def install_requirements(py_exe: str) -> None:
cmd = [py_exe, "-m", "pip", "install", "-r", str(REQUIREMENTS)]
try:
subprocess.run(cmd, check=True, cwd=ROOT)
except subprocess.CalledProcessError:
_warn("默认 pip 源失败,尝试 https://pypi.org/simple")
subprocess.run([*cmd, "-i", "https://pypi.org/simple"], check=True, cwd=ROOT)
_ok("依赖安装完成")
def smoke_test(py_exe: str) -> None:
subprocess.run(
[
py_exe,
"-c",
"from app import create_app; create_app(); print('app import ok')",
],
check=True,
cwd=ROOT,
)
_ok("应用启动检查通过")
def print_summary() -> None:
port = os.environ.get("NAV_PORT", "5070")
print()
print("=" * 50)
print("部署完成")
print(f" 访问地址: http://0.0.0.0:{port}")
print(" 默认账号: admin / admin123")
print(" 生产环境请尽快在「系统设置」中修改密码")
print()
print("启动方式(任选其一):")
if platform.system() == "Windows":
print(f" {venv_python()} app.py")
else:
print(f" {venv_python()} app.py")
print(" pm2 start ecosystem.config.cjs")
print("=" * 50)
def main() -> None:
print(f"LocalNav 一键部署 · {platform.system()} {platform.release()}")
print(f"项目目录: {ROOT}")
print()
base_py = check_python()
check_pip(base_py)
optional_tools()
ensure_env()
py_exe = ensure_venv(base_py)
install_requirements(py_exe)
smoke_test(py_exe)
print_summary()
if __name__ == "__main__":
main()