Add interactive navctl menu for install, uninstall, and update.
Provide navctl.py with a numbered menu, persist install path in config, and support curl bootstrap without pre-cloning the repo. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# 复制本文件为 .env 后按需修改(.env 勿提交到 Git)
|
# 复制本文件为 .env 后按需修改(.env 勿提交到 Git)
|
||||||
# 与 app.py 同目录;程序启动时自动加载。
|
# 与 app.py 同目录;程序启动时自动加载。
|
||||||
# 也可运行 python scripts/install.py 自动克隆并部署(无需预先 git clone)
|
# 也可运行 python scripts/navctl.py 进入交互菜单(无需预先 git clone)
|
||||||
|
|
||||||
# 必填(长期运行):随机字符串,用于会话与 CSRF。
|
# 必填(长期运行):随机字符串,用于会话与 CSRF。
|
||||||
# 一键部署脚本会自动生成;手动生成:openssl rand -hex 32
|
# 一键部署脚本会自动生成;手动生成:openssl rand -hex 32
|
||||||
|
|||||||
@@ -10,44 +10,57 @@ Flask + SQLite 的局域网导航聚合:左侧分组与服务列表,右侧 i
|
|||||||
|
|
||||||
## 一键部署(无需预先克隆)
|
## 一键部署(无需预先克隆)
|
||||||
|
|
||||||
在**空目录或新服务器**上,只需运行安装脚本,按提示输入 Git 仓库地址即可自动克隆并完成部署:
|
运行安装脚本后会进入**交互菜单**:
|
||||||
|
|
||||||
|
```
|
||||||
|
==================================================
|
||||||
|
LocalNav 部署管理
|
||||||
|
==================================================
|
||||||
|
安装目录: /opt/LocalNav
|
||||||
|
服务状态: pm2 online
|
||||||
|
--------------------------------------------------
|
||||||
|
1 一键部署安装
|
||||||
|
2 一键卸载
|
||||||
|
3 更新
|
||||||
|
0 退出
|
||||||
|
--------------------------------------------------
|
||||||
|
请选择:
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Linux / macOS(推荐:一条命令,无需先 git clone)
|
# Linux / macOS(推荐:一条命令,无需先 git clone)
|
||||||
curl -fsSL https://git.bz121.com/dekun/LocalNav/raw/main/scripts/install.sh | bash
|
curl -fsSL https://git.bz121.com/dekun/LocalNav/raw/main/scripts/install.sh | bash
|
||||||
|
|
||||||
# 或本地已有 install 脚本时
|
# 或本地执行
|
||||||
bash scripts/install.sh
|
bash scripts/install.sh
|
||||||
python scripts/install.py
|
python scripts/navctl.py
|
||||||
|
|
||||||
# Windows PowerShell
|
# Windows PowerShell
|
||||||
.\scripts\install.ps1
|
.\scripts\install.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
运行后会提示:
|
**菜单功能:**
|
||||||
|
|
||||||
1. **Git 仓库地址**(默认 `https://git.bz121.com/dekun/LocalNav.git`)
|
| 选项 | 说明 |
|
||||||
2. **安装目录**(Linux 默认 `/opt/LocalNav`,Windows 默认 `./LocalNav`)
|
|------|------|
|
||||||
|
| 1 安装 | 输入仓库地址 → 自动 `git clone` → 安装依赖 → 生成 `.env` → pm2 启动 |
|
||||||
|
| 2 卸载 | 停止 pm2/systemd 服务;可选删除整个安装目录(含数据库) |
|
||||||
|
| 3 更新 | `git pull` → 重新部署 → 自动重启服务 |
|
||||||
|
| 0 退出 | 退出菜单 |
|
||||||
|
|
||||||
|
非交互命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/navctl.py install -y
|
||||||
|
python scripts/navctl.py update --dir /opt/LocalNav
|
||||||
|
python scripts/navctl.py uninstall --dir /opt/LocalNav
|
||||||
|
```
|
||||||
|
|
||||||
随后自动:`git clone` → 创建虚拟环境 → 安装依赖 → 生成 `.env` 与 `NAV_SECRET_KEY` → **自动 pm2 启动**(若已安装 pm2)。
|
随后自动:`git clone` → 创建虚拟环境 → 安装依赖 → 生成 `.env` 与 `NAV_SECRET_KEY` → **自动 pm2 启动**(若已安装 pm2)。
|
||||||
|
|
||||||
非交互安装(使用默认值):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python scripts/install.py -y
|
|
||||||
# 或指定参数
|
|
||||||
python scripts/install.py --repo https://git.bz121.com/dekun/LocalNav.git --dir /opt/LocalNav
|
|
||||||
```
|
|
||||||
|
|
||||||
**默认登录:** `admin` / `admin123`(生产环境请尽快在「系统设置」中修改密码)
|
**默认登录:** `admin` / `admin123`(生产环境请尽快在「系统设置」中修改密码)
|
||||||
|
|
||||||
**已安装后的更新**(在项目目录内):
|
**已安装后的更新:** 重新运行 `install.sh` 选菜单 **3**,或执行 `python scripts/navctl.py update`
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/LocalNav
|
|
||||||
git pull
|
|
||||||
python scripts/deploy.py # 或 bash scripts/deploy.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
配置:将 `.env.example` 复制为 `.env` 并填写变量(与 `app.py` 同目录);详见说明文档「环境变量与 `.env`」一节。
|
配置:将 `.env.example` 复制为 `.env` 并填写变量(与 `app.py` 同目录);详见说明文档「环境变量与 `.env`」一节。
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -315,8 +315,8 @@ def main() -> None:
|
|||||||
args = parse_args()
|
args = parse_args()
|
||||||
if not (ROOT / "app.py").is_file():
|
if not (ROOT / "app.py").is_file():
|
||||||
_fail(
|
_fail(
|
||||||
"请在 LocalNav 项目目录内运行,或使用 scripts/install.py 自动克隆安装:\n"
|
"请在 LocalNav 项目目录内运行,或使用部署管理脚本:\n"
|
||||||
" python scripts/install.py"
|
" python scripts/navctl.py"
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"LocalNav 项目部署 · {platform.system()} {platform.release()}")
|
print(f"LocalNav 项目部署 · {platform.system()} {platform.release()}")
|
||||||
|
|||||||
+14
-35
@@ -1,9 +1,9 @@
|
|||||||
# LocalNav 一键安装(无需预先克隆)
|
# LocalNav 交互式部署管理(无需预先克隆)
|
||||||
# 用法:在 PowerShell 中执行 .\scripts\install.ps1
|
# 用法:.\scripts\install.ps1
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$DefaultRepo = "https://git.bz121.com/dekun/LocalNav.git"
|
$DefaultRepo = "https://git.bz121.com/dekun/LocalNav.git"
|
||||||
$DefaultDir = Join-Path (Get-Location) "LocalNav"
|
$RawBase = "$($DefaultRepo -replace '\.git$','')/raw/main/scripts"
|
||||||
|
|
||||||
function Need-Cmd($name) {
|
function Need-Cmd($name) {
|
||||||
if (-not (Get-Command $name -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command $name -ErrorAction SilentlyContinue)) {
|
||||||
@@ -12,41 +12,20 @@ function Need-Cmd($name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Need-Cmd python
|
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
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
$installPy = Join-Path $scriptDir "install.py"
|
$navctlPy = Join-Path $scriptDir "navctl.py"
|
||||||
|
|
||||||
if (Test-Path $installPy) {
|
if (-not (Test-Path $navctlPy)) {
|
||||||
python $installPy --repo $repo --dir $dest
|
$tmpDir = Join-Path $env:TEMP ("localnav-" + [guid]::NewGuid().ToString())
|
||||||
|
New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null
|
||||||
|
Write-Host "[OK] 正在下载部署管理脚本..."
|
||||||
|
Invoke-WebRequest -Uri "$RawBase/navctl.py" -OutFile (Join-Path $tmpDir "navctl.py")
|
||||||
|
Invoke-WebRequest -Uri "$RawBase/nav_common.py" -OutFile (Join-Path $tmpDir "nav_common.py")
|
||||||
|
$env:PYTHONPATH = "$tmpDir;$env:PYTHONPATH"
|
||||||
|
python (Join-Path $tmpDir "navctl.py") @args
|
||||||
|
Remove-Item -Recurse -Force $tmpDir
|
||||||
exit $LASTEXITCODE
|
exit $LASTEXITCODE
|
||||||
}
|
}
|
||||||
|
|
||||||
$destPath = Resolve-Path -LiteralPath $dest -ErrorAction SilentlyContinue
|
python $navctlPy @args
|
||||||
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")
|
|
||||||
|
|||||||
+10
-153
@@ -1,163 +1,20 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""LocalNav 一键安装:无需预先克隆,输入仓库地址后自动克隆并部署。"""
|
"""兼容入口:等价于 navctl.py install(保留 --repo / --dir / -y 参数)。"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import platform
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
DEFAULT_REPO = "https://git.bz121.com/dekun/LocalNav.git"
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
MIN_PYTHON = (3, 10)
|
if str(SCRIPT_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SCRIPT_DIR))
|
||||||
|
|
||||||
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}")
|
|
||||||
|
|
||||||
|
from navctl import action_install, main, parse_args # noqa: E402
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
args = parse_args()
|
||||||
|
if args.command in (None, "install"):
|
||||||
|
action_install(repo=args.repo, dest=args.dir, yes=args.yes)
|
||||||
|
else:
|
||||||
|
main()
|
||||||
|
|||||||
+14
-34
@@ -1,12 +1,12 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# LocalNav 一键安装(无需预先克隆)
|
# LocalNav 交互式部署管理(无需预先克隆)
|
||||||
# 用法:
|
# 用法:
|
||||||
# curl -fsSL https://git.bz121.com/dekun/LocalNav/raw/main/scripts/install.sh | bash
|
# curl -fsSL https://git.bz121.com/dekun/LocalNav/raw/main/scripts/install.sh | bash
|
||||||
# bash install.sh
|
# bash install.sh
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
DEFAULT_REPO="https://git.bz121.com/dekun/LocalNav.git"
|
DEFAULT_REPO="https://git.bz121.com/dekun/LocalNav.git"
|
||||||
DEFAULT_DIR="/opt/LocalNav"
|
RAW_BASE="${DEFAULT_REPO%.git}/raw/main/scripts"
|
||||||
|
|
||||||
need_cmd() {
|
need_cmd() {
|
||||||
command -v "$1" >/dev/null 2>&1 || {
|
command -v "$1" >/dev/null 2>&1 || {
|
||||||
@@ -16,39 +16,19 @@ need_cmd() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
need_cmd python3
|
need_cmd python3
|
||||||
need_cmd git
|
|
||||||
|
|
||||||
REPO="${NAV_INSTALL_REPO:-}"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || true)"
|
||||||
DEST="${NAV_INSTALL_DIR:-}"
|
NAVCTL_PY="${SCRIPT_DIR}/navctl.py"
|
||||||
|
|
||||||
if [ -z "$REPO" ]; then
|
if [ ! -f "$NAVCTL_PY" ]; then
|
||||||
read -rp "Git 仓库地址 [${DEFAULT_REPO}]: " REPO
|
TMP_DIR="$(mktemp -d)"
|
||||||
REPO="${REPO:-$DEFAULT_REPO}"
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
echo "[OK] 正在下载部署管理脚本..."
|
||||||
|
curl -fsSL "${RAW_BASE}/navctl.py" -o "${TMP_DIR}/navctl.py"
|
||||||
|
curl -fsSL "${RAW_BASE}/nav_common.py" -o "${TMP_DIR}/nav_common.py"
|
||||||
|
export PYTHONPATH="${TMP_DIR}:${PYTHONPATH:-}"
|
||||||
|
exec python3 "${TMP_DIR}/navctl.py" "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$DEST" ]; then
|
cd "$(dirname "$NAVCTL_PY")"
|
||||||
read -rp "安装目录 [${DEFAULT_DIR}]: " DEST
|
exec python3 "$NAVCTL_PY" "$@"
|
||||||
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"
|
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
"""LocalNav 部署脚本公共工具。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DEFAULT_REPO = "https://git.bz121.com/dekun/LocalNav.git"
|
||||||
|
MIN_PYTHON = (3, 10)
|
||||||
|
APP_NAME = "nav-site"
|
||||||
|
|
||||||
|
|
||||||
|
def config_path() -> Path:
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
base = Path(os.environ.get("USERPROFILE", Path.home())) / ".localnav"
|
||||||
|
else:
|
||||||
|
base = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "localnav"
|
||||||
|
base.mkdir(parents=True, exist_ok=True)
|
||||||
|
return base / "install.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load_install_config() -> dict:
|
||||||
|
path = config_path()
|
||||||
|
if not path.is_file():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def save_install_config(project_dir: Path, repo: str) -> None:
|
||||||
|
data = {
|
||||||
|
"dir": str(project_dir.resolve()),
|
||||||
|
"repo": repo.strip(),
|
||||||
|
}
|
||||||
|
config_path().write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def default_install_dir() -> Path:
|
||||||
|
cfg = load_install_config()
|
||||||
|
if cfg.get("dir"):
|
||||||
|
return Path(cfg["dir"]).expanduser()
|
||||||
|
if os.environ.get("NAV_INSTALL_DIR"):
|
||||||
|
return Path(os.environ["NAV_INSTALL_DIR"]).expanduser()
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
return Path.cwd() / "LocalNav"
|
||||||
|
return Path("/opt/LocalNav")
|
||||||
|
|
||||||
|
|
||||||
|
def default_repo() -> str:
|
||||||
|
cfg = load_install_config()
|
||||||
|
return (cfg.get("repo") or os.environ.get("NAV_INSTALL_REPO") or DEFAULT_REPO).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def is_localnav_project(path: Path) -> bool:
|
||||||
|
return (path / "app.py").is_file() and (path / "scripts" / "deploy.py").is_file()
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_project_dir(explicit: str | None = None) -> Path | None:
|
||||||
|
if explicit and explicit.strip():
|
||||||
|
p = Path(explicit.strip()).expanduser().resolve()
|
||||||
|
return p if is_localnav_project(p) else None
|
||||||
|
cfg_dir = load_install_config().get("dir", "").strip()
|
||||||
|
if cfg_dir and is_localnav_project(Path(cfg_dir)):
|
||||||
|
return Path(cfg_dir).resolve()
|
||||||
|
here = Path.cwd()
|
||||||
|
if is_localnav_project(here):
|
||||||
|
return here.resolve()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def pm2_available() -> str | None:
|
||||||
|
return shutil.which("pm2")
|
||||||
|
|
||||||
|
|
||||||
|
def pm2_has_app(name: str = APP_NAME) -> bool:
|
||||||
|
pm2 = pm2_available()
|
||||||
|
if not pm2:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(
|
||||||
|
[pm2, "jlist"],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
apps = json.loads(proc.stdout or "[]")
|
||||||
|
return any(app.get("name") == name for app in apps)
|
||||||
|
except (subprocess.CalledProcessError, json.JSONDecodeError, OSError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def pm2_app_status(name: str = APP_NAME) -> str | None:
|
||||||
|
pm2 = pm2_available()
|
||||||
|
if not pm2:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(
|
||||||
|
[pm2, "jlist"],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
for app in json.loads(proc.stdout or "[]"):
|
||||||
|
if app.get("name") == name:
|
||||||
|
return str(app.get("pm2_env", {}).get("status") or "unknown")
|
||||||
|
except (subprocess.CalledProcessError, json.JSONDecodeError, OSError):
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def systemd_unit_ready(name: str = APP_NAME) -> bool:
|
||||||
|
systemctl = shutil.which("systemctl")
|
||||||
|
if not systemctl:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(
|
||||||
|
[systemctl, "list-unit-files", f"{name}.service", "--no-pager", "--no-legend"],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
return bool((proc.stdout or "").strip())
|
||||||
|
except (subprocess.CalledProcessError, OSError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def systemd_active(name: str = APP_NAME) -> bool:
|
||||||
|
systemctl = shutil.which("systemctl")
|
||||||
|
if not systemctl:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(
|
||||||
|
[systemctl, "is-active", f"{name}.service"],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
return (proc.stdout or "").strip() == "active"
|
||||||
|
except (subprocess.CalledProcessError, OSError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def service_status_text(project_dir: Path | None) -> str:
|
||||||
|
if not project_dir:
|
||||||
|
return "未安装"
|
||||||
|
pm2_state = pm2_app_status()
|
||||||
|
if pm2_state:
|
||||||
|
return f"pm2 {pm2_state}"
|
||||||
|
if systemd_unit_ready() and systemd_active():
|
||||||
|
return "systemd active"
|
||||||
|
if systemd_unit_ready():
|
||||||
|
return "systemd 已配置但未运行"
|
||||||
|
return "未运行"
|
||||||
|
|
||||||
|
|
||||||
|
def stop_service() -> list[str]:
|
||||||
|
"""停止并移除进程管理中的 nav-site,返回已执行操作说明。"""
|
||||||
|
actions: list[str] = []
|
||||||
|
pm2 = pm2_available()
|
||||||
|
if pm2 and pm2_has_app():
|
||||||
|
subprocess.run([pm2, "stop", APP_NAME], check=False)
|
||||||
|
subprocess.run([pm2, "delete", APP_NAME], check=False)
|
||||||
|
subprocess.run([pm2, "save"], check=False)
|
||||||
|
actions.append("pm2 stop/delete nav-site")
|
||||||
|
|
||||||
|
systemctl = shutil.which("systemctl")
|
||||||
|
if systemctl and systemd_unit_ready():
|
||||||
|
subprocess.run([systemctl, "stop", f"{APP_NAME}.service"], check=False)
|
||||||
|
subprocess.run([systemctl, "disable", f"{APP_NAME}.service"], check=False)
|
||||||
|
actions.append("systemctl stop/disable nav-site")
|
||||||
|
|
||||||
|
return actions
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
Set-Location (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
||||||
|
python navctl.py @args
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""LocalNav 交互式部署管理:安装 / 卸载 / 更新。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from nav_common import (
|
||||||
|
APP_NAME,
|
||||||
|
MIN_PYTHON,
|
||||||
|
default_install_dir,
|
||||||
|
default_repo,
|
||||||
|
is_localnav_project,
|
||||||
|
load_install_config,
|
||||||
|
pm2_app_status,
|
||||||
|
resolve_project_dir,
|
||||||
|
save_install_config,
|
||||||
|
service_status_text,
|
||||||
|
stop_service,
|
||||||
|
)
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
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 _pause() -> None:
|
||||||
|
try:
|
||||||
|
input("\n按回车返回菜单...")
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
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 confirm(prompt: str, *, default_no: bool = True) -> bool:
|
||||||
|
suffix = " [y/N]: " if default_no else " [Y/n]: "
|
||||||
|
try:
|
||||||
|
raw = input(prompt + suffix).strip().lower()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print()
|
||||||
|
return False
|
||||||
|
if not raw:
|
||||||
|
return not default_no
|
||||||
|
return raw in ("y", "yes", "是")
|
||||||
|
|
||||||
|
|
||||||
|
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}")
|
||||||
|
return sys.executable
|
||||||
|
|
||||||
|
|
||||||
|
def check_git() -> str:
|
||||||
|
git = shutil.which("git")
|
||||||
|
if not git:
|
||||||
|
_fail("未检测到 git,请先安装:sudo apt install -y git")
|
||||||
|
return git
|
||||||
|
|
||||||
|
|
||||||
|
def clone_project(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 项目: {dest}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if dest.exists():
|
||||||
|
if (dest / ".git").is_dir():
|
||||||
|
_fail(f"目录已存在且不是 LocalNav 项目: {dest}")
|
||||||
|
_fail(f"目录已存在: {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 git_pull(project_dir: Path) -> None:
|
||||||
|
git = check_git()
|
||||||
|
if not (project_dir / ".git").is_dir():
|
||||||
|
_fail(f"目录不是 Git 仓库,无法更新: {project_dir}")
|
||||||
|
_ok(f"正在拉取最新代码: {project_dir}")
|
||||||
|
subprocess.run([git, "-C", str(project_dir), "pull", "--ff-only"], check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def run_deploy(project_dir: Path, py_exe: str) -> None:
|
||||||
|
deploy_script = project_dir / "scripts" / "deploy.py"
|
||||||
|
if not deploy_script.is_file():
|
||||||
|
_fail(f"未找到部署脚本: {deploy_script}")
|
||||||
|
subprocess.run([py_exe, str(deploy_script)], cwd=project_dir, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def action_install(*, repo: str = "", dest: str = "", yes: bool = False) -> None:
|
||||||
|
print()
|
||||||
|
print("=" * 50)
|
||||||
|
print(" 一键部署安装")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
py_exe = check_python()
|
||||||
|
git_exe = check_git()
|
||||||
|
|
||||||
|
repo = repo.strip() or default_repo()
|
||||||
|
if yes:
|
||||||
|
project_dir = Path(dest).expanduser() if dest.strip() else default_install_dir()
|
||||||
|
else:
|
||||||
|
repo = prompt_value("Git 仓库地址", repo)
|
||||||
|
dest_str = dest.strip() or prompt_value("安装目录", str(default_install_dir()))
|
||||||
|
project_dir = Path(dest_str).expanduser()
|
||||||
|
|
||||||
|
project_dir = project_dir.resolve()
|
||||||
|
print()
|
||||||
|
print(f" 仓库: {repo}")
|
||||||
|
print(f" 目录: {project_dir}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
clone_project(git_exe, repo, project_dir)
|
||||||
|
run_deploy(project_dir, py_exe)
|
||||||
|
save_install_config(project_dir, repo)
|
||||||
|
_ok(f"安装完成: {project_dir}")
|
||||||
|
|
||||||
|
|
||||||
|
def action_update(*, dest: str = "") -> None:
|
||||||
|
print()
|
||||||
|
print("=" * 50)
|
||||||
|
print(" 更新")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
project_dir = resolve_project_dir(dest)
|
||||||
|
if not project_dir:
|
||||||
|
dest_str = dest.strip() or prompt_value("安装目录", str(default_install_dir()))
|
||||||
|
project_dir = Path(dest_str).expanduser().resolve()
|
||||||
|
if not is_localnav_project(project_dir):
|
||||||
|
_fail(f"未找到 LocalNav 安装: {project_dir}")
|
||||||
|
|
||||||
|
py_exe = check_python()
|
||||||
|
git_pull(project_dir)
|
||||||
|
run_deploy(project_dir, py_exe)
|
||||||
|
cfg = load_install_config()
|
||||||
|
save_install_config(project_dir, cfg.get("repo") or default_repo())
|
||||||
|
_ok(f"更新完成: {project_dir}")
|
||||||
|
|
||||||
|
|
||||||
|
def action_uninstall(*, dest: str = "") -> None:
|
||||||
|
print()
|
||||||
|
print("=" * 50)
|
||||||
|
print(" 一键卸载")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
project_dir = resolve_project_dir(dest)
|
||||||
|
if not project_dir:
|
||||||
|
dest_str = dest.strip() or prompt_value("安装目录", str(default_install_dir()))
|
||||||
|
candidate = Path(dest_str).expanduser().resolve()
|
||||||
|
if candidate.exists() and is_localnav_project(candidate):
|
||||||
|
project_dir = candidate
|
||||||
|
elif not candidate.exists():
|
||||||
|
_warn(f"目录不存在: {candidate}")
|
||||||
|
project_dir = None
|
||||||
|
else:
|
||||||
|
_fail(f"目录不是 LocalNav 项目: {candidate}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
if project_dir:
|
||||||
|
print(f" 安装目录: {project_dir}")
|
||||||
|
print(f" 服务状态: {service_status_text(project_dir)}")
|
||||||
|
else:
|
||||||
|
print(" 未检测到已安装的 LocalNav 项目")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if not confirm("确认停止 nav-site 服务?", default_no=True):
|
||||||
|
_warn("已取消卸载")
|
||||||
|
return
|
||||||
|
|
||||||
|
actions = stop_service()
|
||||||
|
if actions:
|
||||||
|
for item in actions:
|
||||||
|
_ok(item)
|
||||||
|
else:
|
||||||
|
_warn("未发现 pm2 / systemd 中的 nav-site 进程")
|
||||||
|
|
||||||
|
if not project_dir:
|
||||||
|
_ok("卸载完成(仅停止服务)")
|
||||||
|
return
|
||||||
|
|
||||||
|
if confirm("是否删除整个安装目录(含数据库,不可恢复)?", default_no=True):
|
||||||
|
if not confirm("再次确认:删除 " + str(project_dir) + " ?", default_no=True):
|
||||||
|
_warn("已保留安装目录")
|
||||||
|
else:
|
||||||
|
shutil.rmtree(project_dir)
|
||||||
|
_ok(f"已删除目录: {project_dir}")
|
||||||
|
cfg = load_install_config()
|
||||||
|
if cfg.get("dir") == str(project_dir):
|
||||||
|
from nav_common import config_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
config_path().unlink(missing_ok=True)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
_warn(f"已保留安装目录: {project_dir}")
|
||||||
|
|
||||||
|
_ok("卸载完成")
|
||||||
|
|
||||||
|
|
||||||
|
def render_menu(project_dir: Path | None) -> None:
|
||||||
|
print()
|
||||||
|
print("=" * 50)
|
||||||
|
print(" LocalNav 部署管理")
|
||||||
|
print("=" * 50)
|
||||||
|
if project_dir:
|
||||||
|
print(f" 安装目录: {project_dir}")
|
||||||
|
else:
|
||||||
|
print(f" 安装目录: 未安装(默认 {default_install_dir()})")
|
||||||
|
print(f" 服务状态: {service_status_text(project_dir)}")
|
||||||
|
if pm2_app_status():
|
||||||
|
print(f" PM2 进程: {APP_NAME}")
|
||||||
|
print("-" * 50)
|
||||||
|
print(" 1 一键部署安装")
|
||||||
|
print(" 2 一键卸载")
|
||||||
|
print(" 3 更新")
|
||||||
|
print(" 0 退出")
|
||||||
|
print("-" * 50)
|
||||||
|
|
||||||
|
|
||||||
|
def interactive_menu() -> None:
|
||||||
|
print(f"LocalNav 部署管理 · {platform.system()} {platform.release()}")
|
||||||
|
while True:
|
||||||
|
project_dir = resolve_project_dir()
|
||||||
|
render_menu(project_dir)
|
||||||
|
try:
|
||||||
|
choice = input("请选择: ").strip()
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\n再见。")
|
||||||
|
return
|
||||||
|
|
||||||
|
if choice == "1":
|
||||||
|
try:
|
||||||
|
action_install()
|
||||||
|
except subprocess.CalledProcessError as exc:
|
||||||
|
_fail(f"安装失败: {exc}")
|
||||||
|
_pause()
|
||||||
|
elif choice == "2":
|
||||||
|
try:
|
||||||
|
action_uninstall()
|
||||||
|
except subprocess.CalledProcessError as exc:
|
||||||
|
_fail(f"卸载失败: {exc}")
|
||||||
|
_pause()
|
||||||
|
elif choice == "3":
|
||||||
|
try:
|
||||||
|
action_update()
|
||||||
|
except subprocess.CalledProcessError as exc:
|
||||||
|
_fail(f"更新失败: {exc}")
|
||||||
|
_pause()
|
||||||
|
elif choice == "0":
|
||||||
|
print("再见。")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
_warn("无效选项,请输入 0-3")
|
||||||
|
_pause()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="LocalNav 部署管理")
|
||||||
|
parser.add_argument(
|
||||||
|
"command",
|
||||||
|
nargs="?",
|
||||||
|
choices=("install", "uninstall", "update", "menu"),
|
||||||
|
help="直接执行指定操作;省略则进入交互菜单",
|
||||||
|
)
|
||||||
|
parser.add_argument("--repo", default="", help="Git 仓库地址")
|
||||||
|
parser.add_argument("--dir", default="", help="安装目录")
|
||||||
|
parser.add_argument("-y", "--yes", action="store_true", help="安装时使用默认路径,减少提问")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_args()
|
||||||
|
if args.command == "install":
|
||||||
|
action_install(repo=args.repo, dest=args.dir, yes=args.yes)
|
||||||
|
elif args.command == "uninstall":
|
||||||
|
action_uninstall(dest=args.dir)
|
||||||
|
elif args.command == "update":
|
||||||
|
action_update(dest=args.dir)
|
||||||
|
elif args.command == "menu":
|
||||||
|
interactive_menu()
|
||||||
|
else:
|
||||||
|
interactive_menu()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
exec python3 navctl.py "$@"
|
||||||
+31
-32
@@ -54,10 +54,12 @@
|
|||||||
├── .env # 本地配置(自建,勿提交 Git)
|
├── .env # 本地配置(自建,勿提交 Git)
|
||||||
├── ecosystem.config.cjs # PM2 守护进程配置
|
├── ecosystem.config.cjs # PM2 守护进程配置
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ ├── install.py # 一键安装(输入仓库地址,自动克隆 + 部署)
|
│ ├── navctl.py # 交互式部署管理(安装 / 卸载 / 更新)
|
||||||
│ ├── install.sh # Linux/macOS 安装封装(支持 curl | bash)
|
│ ├── nav_common.py # 部署脚本公共工具
|
||||||
│ ├── install.ps1 # Windows 安装封装
|
│ ├── install.sh # Linux 入口(支持 curl | bash)
|
||||||
│ ├── deploy.py # 项目内更新部署(需已在仓库目录)
|
│ ├── install.ps1 # Windows 入口
|
||||||
|
│ ├── install.py # 兼容:等价 navctl install
|
||||||
|
│ ├── deploy.py # 项目内安装逻辑(由 navctl 调用)
|
||||||
│ ├── deploy.sh # 项目内更新封装
|
│ ├── deploy.sh # 项目内更新封装
|
||||||
│ ├── deploy.ps1 # 项目内更新封装
|
│ ├── deploy.ps1 # 项目内更新封装
|
||||||
│ └── cleanup_gate_scout.py # 可选:清理旧「Gate 扫单」分组
|
│ └── cleanup_gate_scout.py # 可选:清理旧「Gate 扫单」分组
|
||||||
@@ -79,46 +81,42 @@
|
|||||||
|
|
||||||
## 五、一键部署(推荐,无需预先克隆)
|
## 五、一键部署(推荐,无需预先克隆)
|
||||||
|
|
||||||
在**新机器**上无需先 `git clone`,运行安装脚本后按提示输入仓库地址即可自动克隆并完成部署。
|
运行安装脚本后进入**交互式部署菜单**,无需预先 `git clone`。
|
||||||
|
|
||||||
### 5.1 首次安装
|
### 5.1 启动菜单
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Linux / macOS(一条命令,从远程拉取安装脚本)
|
# Linux / macOS(一条命令)
|
||||||
curl -fsSL https://git.bz121.com/dekun/LocalNav/raw/main/scripts/install.sh | bash
|
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
|
bash scripts/install.sh
|
||||||
|
python3 scripts/navctl.py
|
||||||
```
|
```
|
||||||
|
|
||||||
**Windows PowerShell:**
|
**Windows:** `.\scripts\install.ps1`
|
||||||
|
|
||||||
```powershell
|
**菜单:**
|
||||||
.\scripts\install.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
**交互提示:**
|
| 按键 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| **1** | 一键部署安装:输入仓库地址 → 克隆 → 安装依赖 → 生成 `.env` → 自动 pm2 启动 |
|
||||||
|
| **2** | 一键卸载:停止 pm2/systemd;可选删除安装目录(含数据库,需二次确认) |
|
||||||
|
| **3** | 更新:`git pull` + 重新部署 + 自动重启 |
|
||||||
|
| **0** | 退出 |
|
||||||
|
|
||||||
| 提示项 | 默认值 |
|
安装信息会保存到 `~/.config/localnav/install.json`,下次打开菜单自动识别安装目录与服务状态。
|
||||||
|--------|--------|
|
|
||||||
| Git 仓库地址 | `https://git.bz121.com/dekun/LocalNav.git` |
|
|
||||||
| 安装目录 | Linux:`/opt/LocalNav`;Windows:`./LocalNav` |
|
|
||||||
|
|
||||||
**非交互(使用默认值):**
|
### 5.2 非交互命令
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 scripts/install.py -y
|
python3 scripts/navctl.py install -y
|
||||||
python3 scripts/install.py --repo https://git.bz121.com/dekun/LocalNav.git --dir /opt/LocalNav
|
python3 scripts/navctl.py install --repo https://git.bz121.com/dekun/LocalNav.git --dir /opt/LocalNav
|
||||||
|
python3 scripts/navctl.py update --dir /opt/LocalNav
|
||||||
|
python3 scripts/navctl.py uninstall --dir /opt/LocalNav
|
||||||
```
|
```
|
||||||
|
|
||||||
环境变量可跳过提问:`NAV_INSTALL_REPO`、`NAV_INSTALL_DIR`。
|
### 5.3 自动完成的步骤(安装 / 更新)
|
||||||
|
|
||||||
### 5.2 自动完成的步骤
|
|
||||||
|
|
||||||
| 步骤 | 说明 |
|
| 步骤 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
@@ -132,14 +130,15 @@ python3 scripts/install.py --repo https://git.bz121.com/dekun/LocalNav.git --dir
|
|||||||
| 内网 Cookie | 默认 `NAV_COOKIES_INSECURE_HTTP=1`(便于 `http://IP:端口` 访问) |
|
| 内网 Cookie | 默认 `NAV_COOKIES_INSECURE_HTTP=1`(便于 `http://IP:端口` 访问) |
|
||||||
| **自动启动** | 若检测到 **pm2**:`pm2 start` 或 `pm2 restart nav-site`;否则尝试 **systemd** `nav-site` 服务 |
|
| **自动启动** | 若检测到 **pm2**:`pm2 start` 或 `pm2 restart nav-site`;否则尝试 **systemd** `nav-site` 服务 |
|
||||||
|
|
||||||
**注意:** 脚本不会覆盖 `.env` 中已有的 `NAV_SECRET_KEY` 等配置。生产环境部署后请尽快登录并在「系统设置」中修改密码。仅安装不启动可加 `--no-start`。
|
**注意:** 脚本不会覆盖 `.env` 中已有的 `NAV_SECRET_KEY` 等配置。生产环境部署后请尽快登录并在「系统设置」中修改密码。仅安装不启动可在 `deploy.py` 加 `--no-start`。
|
||||||
|
|
||||||
### 5.3 已安装后的更新
|
### 5.4 已安装后的更新
|
||||||
|
|
||||||
|
也可在菜单选 **3**,或:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/LocalNav
|
cd /opt/LocalNav
|
||||||
git pull
|
python3 scripts/navctl.py update
|
||||||
python3 scripts/deploy.py # 或 bash scripts/deploy.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
部署完成后启动:
|
部署完成后启动:
|
||||||
|
|||||||
Reference in New Issue
Block a user