From 6897f88afd834223b5f31b38c3e6cb3aaac72576 Mon Sep 17 00:00:00 2001 From: dekun Date: Sun, 12 Jul 2026 11:52:57 +0800 Subject: [PATCH] Add install script for clone-first one-click deployment. Prompt for repo URL and install directory, auto git clone, then run project setup. Keep deploy.py for in-repo updates only. Co-authored-by: Cursor --- .env.example | 2 +- README.md | 40 ++++++++--- scripts/deploy.ps1 | 2 + scripts/deploy.py | 23 +++++-- scripts/deploy.sh | 2 + scripts/install.ps1 | 52 ++++++++++++++ scripts/install.py | 163 ++++++++++++++++++++++++++++++++++++++++++++ scripts/install.sh | 54 +++++++++++++++ 部署与使用说明.md | 109 +++++++++++++++++++++-------- 9 files changed, 403 insertions(+), 44 deletions(-) create mode 100644 scripts/install.ps1 create mode 100644 scripts/install.py create mode 100644 scripts/install.sh diff --git a/.env.example b/.env.example index 8b109f5..9c25dad 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ # 复制本文件为 .env 后按需修改(.env 勿提交到 Git) # 与 app.py 同目录;程序启动时自动加载。 -# 也可运行 python scripts/deploy.py 自动生成本文件并填入默认值。 +# 也可运行 python scripts/install.py 自动克隆并部署(无需预先 git clone) # 必填(长期运行):随机字符串,用于会话与 CSRF。 # 一键部署脚本会自动生成;手动生成:openssl rand -hex 32 diff --git a/README.md b/README.md index a302223..a3fcd7e 100644 --- a/README.md +++ b/README.md @@ -8,23 +8,47 @@ Flask + SQLite 的局域网导航聚合:左侧分组与服务列表,右侧 i **默认端口:** `5070`(可通过环境变量 `NAV_PORT` 修改) -## 一键部署 +## 一键部署(无需预先克隆) + +在**空目录或新服务器**上,只需运行安装脚本,按提示输入 Git 仓库地址即可自动克隆并完成部署: ```bash -# Linux / macOS -bash scripts/deploy.sh +# Linux / macOS(推荐:一条命令,无需先 git clone) +curl -fsSL https://git.bz121.com/dekun/LocalNav/raw/main/scripts/install.sh | bash + +# 或本地已有 install 脚本时 +bash scripts/install.sh +python scripts/install.py # Windows PowerShell -.\scripts\deploy.ps1 - -# 或直接 -python scripts/deploy.py +.\scripts\install.ps1 ``` -脚本会自动:检测 Python 环境、创建虚拟环境、安装依赖、从 `.env.example` 生成 `.env`、写入 `NAV_SECRET_KEY` 与默认账号。 +运行后会提示: + +1. **Git 仓库地址**(默认 `https://git.bz121.com/dekun/LocalNav.git`) +2. **安装目录**(Linux 默认 `/opt/LocalNav`,Windows 默认 `./LocalNav`) + +随后自动:`git clone` → 创建虚拟环境 → 安装依赖 → 生成 `.env` 与 `NAV_SECRET_KEY`。 + +非交互安装(使用默认值): + +```bash +python scripts/install.py -y +# 或指定参数 +python scripts/install.py --repo https://git.bz121.com/dekun/LocalNav.git --dir /opt/LocalNav +``` **默认登录:** `admin` / `admin123`(生产环境请尽快在「系统设置」中修改密码) +**已安装后的更新**(在项目目录内): + +```bash +cd /opt/LocalNav +git pull +python scripts/deploy.py # 或 bash scripts/deploy.sh +``` + 配置:将 `.env.example` 复制为 `.env` 并填写变量(与 `app.py` 同目录);详见说明文档「环境变量与 `.env`」一节。 进程守护(可选):`pm2 start ecosystem.config.cjs`(需已安装 PM2),详见 [部署与使用说明.md](./部署与使用说明.md) 中「9.7 使用 PM2」。 diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 index 72c2b75..5e4907e 100644 --- a/scripts/deploy.ps1 +++ b/scripts/deploy.ps1 @@ -1,3 +1,5 @@ +# 已在项目目录内时:更新依赖与配置(不会克隆代码) +# 首次安装请用: .\scripts\install.ps1 $ErrorActionPreference = "Stop" Set-Location (Split-Path -Parent $PSScriptRoot) python scripts/deploy.py diff --git a/scripts/deploy.py b/scripts/deploy.py index 5786105..9529bae 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""LocalNav 一键部署:环境检测、生成 .env、安装依赖。""" +"""LocalNav 项目内部署:环境检测、生成 .env、安装依赖(需已在项目目录内)。""" from __future__ import annotations @@ -62,11 +62,14 @@ def check_pip(py_exe: str) -> None: def optional_tools() -> None: - for name in ("git", "pm2"): - if shutil.which(name): - _ok(f"可选工具 {name} 已安装") - else: - _warn(f"未检测到 {name}(可选)") + if shutil.which("git"): + _ok("可选工具 git 已安装") + else: + _warn("未检测到 git(可选,首次安装请用 scripts/install.py)") + if shutil.which("pm2"): + _ok("可选工具 pm2 已安装") + else: + _warn("未检测到 pm2(可选)") def read_env_lines(path: Path) -> list[str]: @@ -199,7 +202,13 @@ def print_summary() -> None: def main() -> None: - print(f"LocalNav 一键部署 · {platform.system()} {platform.release()}") + if not (ROOT / "app.py").is_file(): + _fail( + "请在 LocalNav 项目目录内运行,或使用 scripts/install.py 自动克隆安装:\n" + " python scripts/install.py" + ) + + print(f"LocalNav 项目部署 · {platform.system()} {platform.release()}") print(f"项目目录: {ROOT}") print() diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 75be68c..28b767d 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -1,4 +1,6 @@ #!/usr/bin/env bash +# 已在项目目录内时:更新依赖与配置(不会克隆代码) +# 首次安装请用: bash scripts/install.sh set -euo pipefail cd "$(dirname "$0")/.." python3 scripts/deploy.py diff --git a/scripts/install.ps1 b/scripts/install.ps1 new file mode 100644 index 0000000..b10617b --- /dev/null +++ b/scripts/install.ps1 @@ -0,0 +1,52 @@ +# LocalNav 一键安装(无需预先克隆) +# 用法:在 PowerShell 中执行 .\scripts\install.ps1 + +$ErrorActionPreference = "Stop" +$DefaultRepo = "https://git.bz121.com/dekun/LocalNav.git" +$DefaultDir = Join-Path (Get-Location) "LocalNav" + +function Need-Cmd($name) { + if (-not (Get-Command $name -ErrorAction SilentlyContinue)) { + Write-Error "[FAIL] 未找到 $name,请先安装" + } +} + +Need-Cmd python +Need-Cmd git + +$repo = $env:NAV_INSTALL_REPO +if (-not $repo) { + $inputRepo = Read-Host "Git 仓库地址 [$DefaultRepo]" + $repo = if ($inputRepo) { $inputRepo } else { $DefaultRepo } +} + +$dest = $env:NAV_INSTALL_DIR +if (-not $dest) { + $inputDir = Read-Host "安装目录 [$DefaultDir]" + $dest = if ($inputDir) { $inputDir } else { $DefaultDir } +} + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$installPy = Join-Path $scriptDir "install.py" + +if (Test-Path $installPy) { + python $installPy --repo $repo --dir $dest + exit $LASTEXITCODE +} + +$destPath = Resolve-Path -LiteralPath $dest -ErrorAction SilentlyContinue +if ($destPath -and (Test-Path (Join-Path $destPath "app.py"))) { + Write-Host "[OK] 目录已是 LocalNav,执行 git pull" + git -C $destPath pull --ff-only +} elseif (Test-Path $dest) { + Write-Error "[FAIL] 目录已存在且不是 LocalNav 仓库: $dest" +} else { + Write-Host "[OK] 正在克隆 $repo -> $dest" + $parent = Split-Path -Parent $dest + if ($parent -and -not (Test-Path $parent)) { + New-Item -ItemType Directory -Path $parent -Force | Out-Null + } + git clone $repo $dest +} + +python (Join-Path $dest "scripts\deploy.py") diff --git a/scripts/install.py b/scripts/install.py new file mode 100644 index 0000000..68bf5d0 --- /dev/null +++ b/scripts/install.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +"""LocalNav 一键安装:无需预先克隆,输入仓库地址后自动克隆并部署。""" + +from __future__ import annotations + +import argparse +import os +import platform +import shutil +import subprocess +import sys +from pathlib import Path + +DEFAULT_REPO = "https://git.bz121.com/dekun/LocalNav.git" +MIN_PYTHON = (3, 10) + + +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 default_install_dir() -> Path: + if platform.system() == "Windows": + return Path.cwd() / "LocalNav" + return Path("/opt/LocalNav") + + +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_git() -> str: + git = shutil.which("git") + if not git: + _fail("未检测到 git,请先安装:Ubuntu 执行 apt install -y git") + _ok(f"git 可用 ({git})") + return git + + +def prompt_value(label: str, default: str) -> str: + try: + raw = input(f"{label} [{default}]: ").strip() + except (EOFError, KeyboardInterrupt): + print() + _fail("已取消") + return raw or default + + +def resolve_paths(repo: str, dest: Path) -> tuple[str, Path]: + dest = dest.expanduser().resolve() + repo = repo.strip() + if not repo: + _fail("仓库地址不能为空") + if dest.name == "" or str(dest).endswith(("/", "\\")): + _fail("安装目录无效") + return repo, dest + + +def is_localnav_project(path: Path) -> bool: + return (path / "app.py").is_file() and (path / "scripts" / "deploy.py").is_file() + + +def clone_or_update(git_exe: str, repo: str, dest: Path) -> None: + if dest.exists() and not dest.is_dir(): + _fail(f"安装路径已存在且不是目录: {dest}") + + if is_localnav_project(dest): + _ok(f"目录已是 LocalNav 项目,执行 git pull: {dest}") + subprocess.run([git_exe, "-C", str(dest), "pull", "--ff-only"], check=True) + return + + if dest.exists(): + if (dest / ".git").is_dir(): + _ok(f"目录为 Git 仓库,执行 git pull: {dest}") + subprocess.run([git_exe, "-C", str(dest), "pull", "--ff-only"], check=True) + if not is_localnav_project(dest): + _fail(f"目录 {dest} 不是 LocalNav 项目(缺少 app.py)") + return + _fail(f"目录已存在且不是 Git 仓库: {dest}") + + dest.parent.mkdir(parents=True, exist_ok=True) + _ok(f"正在克隆 {repo} -> {dest}") + subprocess.run([git_exe, "clone", repo, str(dest)], check=True) + if not is_localnav_project(dest): + _fail("克隆完成但未找到 app.py,请检查仓库地址是否正确") + + +def run_project_setup(py_exe: str, project_dir: Path) -> None: + deploy_script = project_dir / "scripts" / "deploy.py" + if not deploy_script.is_file(): + _fail(f"未找到部署脚本: {deploy_script}") + _ok(f"开始项目内安装: {project_dir}") + subprocess.run([py_exe, str(deploy_script)], cwd=project_dir, check=True) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="LocalNav 一键安装(自动克隆 + 部署)") + parser.add_argument( + "--repo", + default="", + help=f"Git 仓库地址(默认 {DEFAULT_REPO})", + ) + parser.add_argument( + "--dir", + default="", + help="安装目录(Linux 默认 /opt/LocalNav,Windows 默认 ./LocalNav)", + ) + parser.add_argument( + "-y", + "--yes", + action="store_true", + help="使用默认仓库与安装目录,不交互提问", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + print(f"LocalNav 一键安装 · {platform.system()} {platform.release()}") + print("无需预先克隆,将自动拉取代码并完成部署。") + print() + + py_exe = check_python() + git_exe = check_git() + + default_dest = default_install_dir() + if args.yes: + repo = args.repo.strip() or DEFAULT_REPO + dest = Path(args.dir).expanduser() if args.dir.strip() else default_dest + else: + repo = args.repo.strip() or prompt_value("Git 仓库地址", DEFAULT_REPO) + dest_str = args.dir.strip() or prompt_value("安装目录", str(default_dest)) + dest = Path(dest_str) + + repo, dest = resolve_paths(repo, dest) + print() + print(f" 仓库: {repo}") + print(f" 目录: {dest}") + print() + + clone_or_update(git_exe, repo, dest) + run_project_setup(py_exe, dest) + + print() + print(f"安装目录: {dest}") + print(f"进入目录: cd {dest}") + + +if __name__ == "__main__": + main() diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100644 index 0000000..6992716 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# LocalNav 一键安装(无需预先克隆) +# 用法: +# curl -fsSL https://git.bz121.com/dekun/LocalNav/raw/main/scripts/install.sh | bash +# bash install.sh +set -euo pipefail + +DEFAULT_REPO="https://git.bz121.com/dekun/LocalNav.git" +DEFAULT_DIR="/opt/LocalNav" + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "[FAIL] 未找到 $1,请先安装" >&2 + exit 1 + } +} + +need_cmd python3 +need_cmd git + +REPO="${NAV_INSTALL_REPO:-}" +DEST="${NAV_INSTALL_DIR:-}" + +if [ -z "$REPO" ]; then + read -rp "Git 仓库地址 [${DEFAULT_REPO}]: " REPO + REPO="${REPO:-$DEFAULT_REPO}" +fi + +if [ -z "$DEST" ]; then + read -rp "安装目录 [${DEFAULT_DIR}]: " DEST + DEST="${DEST:-$DEFAULT_DIR}" +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +INSTALL_PY="${SCRIPT_DIR}/install.py" + +if [ -f "$INSTALL_PY" ]; then + exec python3 "$INSTALL_PY" --repo "$REPO" --dir "$DEST" +fi + +# 通过 curl 管道执行时,本机没有 install.py,内联克隆 + 部署 +if [ -d "$DEST/.git" ] && [ -f "$DEST/app.py" ]; then + echo "[OK] 目录已是 LocalNav,执行 git pull" + git -C "$DEST" pull --ff-only +elif [ -d "$DEST" ]; then + echo "[FAIL] 目录已存在且不是 LocalNav 仓库: $DEST" >&2 + exit 1 +else + echo "[OK] 正在克隆 $REPO -> $DEST" + mkdir -p "$(dirname "$DEST")" + git clone "$REPO" "$DEST" +fi + +exec python3 "$DEST/scripts/deploy.py" diff --git a/部署与使用说明.md b/部署与使用说明.md index db44e79..60af8af 100644 --- a/部署与使用说明.md +++ b/部署与使用说明.md @@ -54,9 +54,12 @@ ├── .env # 本地配置(自建,勿提交 Git) ├── ecosystem.config.cjs # PM2 守护进程配置 ├── scripts/ -│ ├── deploy.py # 一键部署(环境检测、生成 .env) -│ ├── deploy.sh # Linux/macOS 封装 -│ ├── deploy.ps1 # Windows 封装 +│ ├── install.py # 一键安装(输入仓库地址,自动克隆 + 部署) +│ ├── install.sh # Linux/macOS 安装封装(支持 curl | bash) +│ ├── install.ps1 # Windows 安装封装 +│ ├── deploy.py # 项目内更新部署(需已在仓库目录) +│ ├── deploy.sh # 项目内更新封装 +│ ├── deploy.ps1 # 项目内更新封装 │ └── cleanup_gate_scout.py # 可选:清理旧「Gate 扫单」分组 ├── nav_local.db # SQLite 数据库(首次成功运行后生成,勿手误提交到公开仓库) ├── static/ @@ -74,26 +77,54 @@ --- -## 五、一键部署(推荐) +## 五、一键部署(推荐,无需预先克隆) -项目提供跨平台部署脚本,自动完成环境检测、虚拟环境创建、依赖安装、`.env` 生成与 `NAV_SECRET_KEY` 写入。 +在**新机器**上无需先 `git clone`,运行安装脚本后按提示输入仓库地址即可自动克隆并完成部署。 + +### 5.1 首次安装 ```bash -# Linux / macOS -bash scripts/deploy.sh - -# Windows PowerShell -.\scripts\deploy.ps1 - -# 或直接 -python scripts/deploy.py +# Linux / macOS(一条命令,从远程拉取安装脚本) +curl -fsSL https://git.bz121.com/dekun/LocalNav/raw/main/scripts/install.sh | bash ``` -**脚本行为:** +也可将 `install.sh` / `install.py` 拷贝到本机后执行: + +```bash +bash scripts/install.sh +# 或 +python3 scripts/install.py +``` + +**Windows PowerShell:** + +```powershell +.\scripts\install.ps1 +``` + +**交互提示:** + +| 提示项 | 默认值 | +|--------|--------| +| Git 仓库地址 | `https://git.bz121.com/dekun/LocalNav.git` | +| 安装目录 | Linux:`/opt/LocalNav`;Windows:`./LocalNav` | + +**非交互(使用默认值):** + +```bash +python3 scripts/install.py -y +python3 scripts/install.py --repo https://git.bz121.com/dekun/LocalNav.git --dir /opt/LocalNav +``` + +环境变量可跳过提问:`NAV_INSTALL_REPO`、`NAV_INSTALL_DIR`。 + +### 5.2 自动完成的步骤 | 步骤 | 说明 | |------|------| -| 环境检测 | Python 3.10+、pip;可选检测 git、pm2 | +| 环境检测 | Python 3.10+、git(必填)、pip | +| 克隆代码 | `git clone` 到指定目录;若目录已是本项目则 `git pull` | +| 虚拟环境 | 创建 `.venv` 并 `pip install -r requirements.txt` | | 生成 `.env` | 从 `.env.example` 复制(若不存在) | | `NAV_SECRET_KEY` | 若为空则自动生成 64 位 hex | | 默认账号 | `NAV_ADMIN_USERNAME=admin`、`NAV_ADMIN_PASSWORD=admin123` | @@ -102,6 +133,14 @@ python scripts/deploy.py **注意:** 脚本不会覆盖 `.env` 中已有的 `NAV_SECRET_KEY` 等配置。生产环境部署后请尽快登录并在「系统设置」中修改密码。 +### 5.3 已安装后的更新 + +```bash +cd /opt/LocalNav +git pull +python3 scripts/deploy.py # 或 bash scripts/deploy.sh +``` + 部署完成后启动: ```bash @@ -226,7 +265,7 @@ http://<本机局域网IP>:5070 - 密码:**`admin123`** - 一个名为 **「默认分组」** 的空分组。 2. 控制台会打印一行提示(内容大意:默认账号仅内网使用,请尽快修改)。 -3. 一键部署脚本 `scripts/deploy.py` 会写入相同的默认账号;若 `.env` 中已有 `NAV_ADMIN_USERNAME` / `NAV_ADMIN_PASSWORD` 则以其为准。 +3. 一键安装脚本 `scripts/install.py` 会写入相同的默认账号;若 `.env` 中已有 `NAV_ADMIN_USERNAME` / `NAV_ADMIN_PASSWORD` 则以其为准。 **安全建议(强烈)**: @@ -360,17 +399,19 @@ sudo apt install -y python3 python3-venv python3-pip git (若已安装 Python 3、venv 与 git,可跳过。) -### 10.2 克隆项目并安装依赖 +### 10.2 一键安装(推荐) ```bash -sudo mkdir -p /opt -cd /opt -sudo git clone https://git.bz121.com/dekun/LocalNav.git -cd /opt/LocalNav -python3 -m venv .venv -source .venv/bin/activate -pip install -U pip -pip install -r requirements.txt -i https://pypi.org/simple +sudo apt install -y python3 python3-venv python3-pip git # 若 10.1 已装可跳过 +curl -fsSL https://git.bz121.com/dekun/LocalNav/raw/main/scripts/install.sh | bash +# 按提示输入仓库地址(直接回车用默认)与安装目录(建议 /opt/LocalNav) +``` + +或手动执行: + +```bash +curl -fsO https://git.bz121.com/dekun/LocalNav/raw/main/scripts/install.py +sudo python3 install.py --repo https://git.bz121.com/dekun/LocalNav.git --dir /opt/LocalNav ``` 后续更新代码: @@ -378,11 +419,23 @@ pip install -r requirements.txt -i https://pypi.org/simple ```bash cd /opt/LocalNav git pull -source .venv/bin/activate -pip install -r requirements.txt -i https://pypi.org/simple -sudo systemctl restart nav-site +python3 scripts/deploy.py +pm2 restart nav-site # 或 systemctl restart nav-site ``` +
+手动克隆安装(旧方式,不推荐) + +```bash +sudo mkdir -p /opt +cd /opt +sudo git clone https://git.bz121.com/dekun/LocalNav.git +cd /opt/LocalNav +python3 scripts/deploy.py +``` + +
+ ### 10.3 配置密钥(必做) ```bash