mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
285 lines
9.9 KiB
Bash
285 lines
9.9 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# vocat install / update script for binary + systemd deployments.
|
|
#
|
|
# Usage:
|
|
# sudo bash install.sh [version] # install a specific version
|
|
# sudo bash install.sh # install latest release
|
|
# sudo bash install.sh --force # reinstall even at the same version
|
|
# curl -fsSL <raw url> | sudo bash # one-liner (latest)
|
|
#
|
|
# Behavior:
|
|
# - Prompts for script language (中文 / English) as soon as it runs.
|
|
# - If the installed version equals the target version, does nothing (unless --force).
|
|
# - On first install, generates a random 32-char admin password, writes it to
|
|
# /etc/vocat/env (0600, loaded by the systemd unit), and prints it ONCE.
|
|
# - On update, preserves the existing env file and credentials.
|
|
# - (Re)writes the systemd unit and restarts the service.
|
|
#
|
|
# Published script: must contain no secrets, IPs, or passwords.
|
|
|
|
set -euo pipefail
|
|
|
|
# --- Publisher configuration -------------------------------------------------
|
|
# Default GitHub repository in owner/name form. Publishers: set this to your
|
|
# own repo, or override per-run with VOCAT_REPO.
|
|
REPO="${VOCAT_REPO:-MengMengCode/VoCat}"
|
|
|
|
INSTALL_DIR="/opt/vocat/bin"
|
|
BINARY_PATH="${INSTALL_DIR}/vocat"
|
|
LINK_PATH="/usr/local/bin/vocat"
|
|
ENV_DIR="/etc/vocat"
|
|
ENV_FILE="${ENV_DIR}/env"
|
|
UNIT_PATH="/etc/systemd/system/vocat.service"
|
|
|
|
# --- Language ----------------------------------------------------------------
|
|
LANG_CHOICE=""
|
|
|
|
msg() {
|
|
# $1 = zh text, $2 = en text
|
|
if [ "$LANG_CHOICE" = "en" ]; then
|
|
printf '%s\n' "$2"
|
|
else
|
|
printf '%s\n' "$1"
|
|
fi
|
|
}
|
|
|
|
prompt_language() {
|
|
if ! ( : </dev/tty ) 2>/dev/null; then
|
|
case "${VOCAT_LANG:-en}" in
|
|
zh|zh-CN|cn) LANG_CHOICE="zh" ;;
|
|
*) LANG_CHOICE="en" ;;
|
|
esac
|
|
return
|
|
fi
|
|
while true; do
|
|
echo "选择语言 / Select language: 1) 中文 2) English" >/dev/tty
|
|
printf '> ' >/dev/tty
|
|
if ! read -r choice </dev/tty; then
|
|
LANG_CHOICE="en"
|
|
return
|
|
fi
|
|
case "$choice" in
|
|
1|"") LANG_CHOICE="zh"; return ;;
|
|
2) LANG_CHOICE="en"; return ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
die() {
|
|
msg "$1" "$2" >&2
|
|
exit 1
|
|
}
|
|
|
|
# --- Root --------------------------------------------------------------------
|
|
[ "$(id -u)" -eq 0 ] || die "请以 root 身份运行此脚本。" "Run this script as root."
|
|
|
|
prompt_language
|
|
|
|
# --- Parse args --------------------------------------------------------------
|
|
FORCE=0
|
|
TARGET_VERSION=""
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--force) FORCE=1 ;;
|
|
-h|--help)
|
|
msg "用法: sudo bash install.sh [--force] [版本]" "Usage: sudo bash install.sh [--force] [version]"
|
|
exit 0
|
|
;;
|
|
*) TARGET_VERSION="${arg#v}" ;;
|
|
esac
|
|
done
|
|
|
|
# --- Resolve target version --------------------------------------------------
|
|
resolve_target_version() {
|
|
if [ -n "$TARGET_VERSION" ]; then
|
|
TARGET_VERSION="${TARGET_VERSION#v}"
|
|
return
|
|
fi
|
|
local api_url="https://api.github.com/repos/${REPO}/releases/latest"
|
|
local auth_hdr=()
|
|
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
|
auth_hdr=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
|
|
fi
|
|
local resp
|
|
resp=$(curl -fsSL "${auth_hdr[@]}" "$api_url") || die "无法获取最新版本信息。检查网络或 REPO 设置。" "Failed to fetch latest release. Check network or REPO."
|
|
# Parse "tag_name": "vX.Y.Z" without jq.
|
|
local tag
|
|
tag=$(printf '%s\n' "$resp" | grep -m1 '"tag_name"' | sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
|
|
[ -n "$tag" ] || die "无法解析最新版本的 tag_name。" "Could not parse tag_name from the release response."
|
|
TARGET_VERSION="${tag#v}"
|
|
}
|
|
|
|
# --- Skip if already installed at the same version ---------------------------
|
|
skip_if_equal() {
|
|
[ -x "$BINARY_PATH" ] || return 0
|
|
[ "$FORCE" -eq 1 ] && return 0
|
|
local installed
|
|
installed=$("$BINARY_PATH" version 2>/dev/null | awk '{print $2}' | sed -E 's/[[:space:]]*\(.*$//') || return 0
|
|
[ -z "$installed" ] && return 0
|
|
if [ "$installed" = "$TARGET_VERSION" ]; then
|
|
install -d -m 0755 "$(dirname "$LINK_PATH")"
|
|
ln -sfn "$BINARY_PATH" "$LINK_PATH"
|
|
msg "已安装版本 $installed,与目标版本相同,跳过更新。" "Installed version $installed equals target; skipping."
|
|
exit 0
|
|
fi
|
|
msg "当前 $installed -> $TARGET_VERSION,开始更新。" "Updating $installed -> $TARGET_VERSION."
|
|
}
|
|
|
|
# --- Detect architecture -----------------------------------------------------
|
|
detect_arch() {
|
|
case "$(uname -m)" in
|
|
x86_64) ARCH="amd64" ;;
|
|
i386|i486|i586|i686) ARCH="386" ;;
|
|
aarch64|arm64) ARCH="arm64" ;;
|
|
armv7l|armv7*) ARCH="armv7" ;;
|
|
*) die "不支持的架构: $(uname -m)" "Unsupported architecture: $(uname -m)" ;;
|
|
esac
|
|
}
|
|
|
|
# --- Download + verify -------------------------------------------------------
|
|
VOCAT_TMP=""
|
|
download_and_verify() {
|
|
VOCAT_TMP=$(mktemp -d)
|
|
trap 'rm -rf "$VOCAT_TMP"' EXIT
|
|
local base="https://github.com/${REPO}/releases/download/v${TARGET_VERSION}"
|
|
local asset="vocat-linux-${ARCH}"
|
|
msg "下载 $asset ..." "Downloading $asset ..."
|
|
curl -fsSL -o "${VOCAT_TMP}/vocat" "${base}/${asset}" || die "下载二进制失败。" "Failed to download the binary."
|
|
curl -fsSL -o "${VOCAT_TMP}/SHA256SUMS" "${base}/SHA256SUMS" || die "下载 SHA256SUMS 失败。" "Failed to download SHA256SUMS."
|
|
|
|
local expected actual
|
|
# Match a line whose filename field equals the asset (with optional binary-mode * prefix).
|
|
expected=$(awk -v a="$asset" '$2 == a || $2 == ("*" a) {print $1; exit}' "${VOCAT_TMP}/SHA256SUMS")
|
|
[ -n "$expected" ] || die "SHA256SUMS 中找不到 $asset 的校验行。" "$asset not found in SHA256SUMS."
|
|
actual=$(sha256sum "${VOCAT_TMP}/vocat" | awk '{print $1}')
|
|
[ "$actual" = "$expected" ] || die "SHA-256 校验失败。" "SHA-256 verification failed."
|
|
}
|
|
|
|
# --- Install binary ----------------------------------------------------------
|
|
install_binary() {
|
|
install -d -m 0755 "$INSTALL_DIR"
|
|
install -m 0755 "${VOCAT_TMP}/vocat" "${BINARY_PATH}.new"
|
|
if [ -e "$BINARY_PATH" ]; then
|
|
cp -a "$BINARY_PATH" "${BINARY_PATH}.bak"
|
|
fi
|
|
mv -f "${BINARY_PATH}.new" "$BINARY_PATH"
|
|
install -d -m 0755 "$(dirname "$LINK_PATH")"
|
|
ln -sfn "$BINARY_PATH" "$LINK_PATH"
|
|
}
|
|
|
|
# --- Data directory ----------------------------------------------------------
|
|
ensure_data_dir() {
|
|
install -d -m 0755 /opt/vocat/data
|
|
chown -R root:root /opt/vocat
|
|
}
|
|
|
|
# --- Env file (first install only) -------------------------------------------
|
|
# Generates a random 32-char secret, stores it in the 0600 env file, and flags
|
|
# FIRST_INSTALL so we can print the secret once at the end.
|
|
FIRST_INSTALL=0
|
|
setup_env() {
|
|
if [ -f "$ENV_FILE" ]; then
|
|
return
|
|
fi
|
|
install -d -m 0755 "$ENV_DIR"
|
|
local secret
|
|
secret=$(od -An -N16 -tx1 /dev/urandom | tr -d ' \n')
|
|
[ -n "$secret" ] || die "生成随机密钥失败。" "Failed to generate a random secret."
|
|
printf 'VOCAT_ADMIN_PASSWORD=%s\n' "$secret" > "$ENV_FILE"
|
|
chmod 0600 "$ENV_FILE"
|
|
FIRST_INSTALL=1
|
|
}
|
|
|
|
# --- systemd unit ------------------------------------------------------------
|
|
write_unit() {
|
|
cat > "$UNIT_PATH" <<EOF
|
|
[Unit]
|
|
Description=vocat cellular and VoWiFi control service
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=root
|
|
Group=root
|
|
WorkingDirectory=/opt/vocat
|
|
EnvironmentFile=${ENV_FILE}
|
|
Environment=VOCAT_ADDR=0.0.0.0:7575
|
|
Environment=VOCAT_DATABASE_PATH=/opt/vocat/data/vocat.db
|
|
ExecStart=${BINARY_PATH}
|
|
Restart=on-failure
|
|
RestartSec=3s
|
|
TimeoutStartSec=30s
|
|
TimeoutStopSec=20s
|
|
|
|
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW
|
|
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW
|
|
NoNewPrivileges=true
|
|
PrivateTmp=true
|
|
PrivateDevices=false
|
|
ProtectSystem=strict
|
|
ProtectHome=true
|
|
ProtectKernelLogs=true
|
|
ProtectKernelModules=true
|
|
ProtectKernelTunables=true
|
|
ProtectControlGroups=true
|
|
ReadWritePaths=/opt/vocat/data
|
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
|
|
RestrictRealtime=true
|
|
LockPersonality=true
|
|
MemoryDenyWriteExecute=true
|
|
UMask=0077
|
|
LimitNOFILE=65536
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
chmod 0644 "$UNIT_PATH"
|
|
}
|
|
|
|
enable_and_start() {
|
|
systemctl daemon-reload
|
|
systemctl enable vocat
|
|
if systemctl restart vocat; then
|
|
return
|
|
fi
|
|
if [ -e "${BINARY_PATH}.bak" ]; then
|
|
msg "新版本启动失败,正在恢复旧二进制。" "The new version failed to start; restoring the previous binary."
|
|
cp -a "${BINARY_PATH}.bak" "$BINARY_PATH"
|
|
systemctl restart vocat || true
|
|
fi
|
|
die "vocat 服务启动失败。" "The vocat service failed to start."
|
|
}
|
|
|
|
# --- Main --------------------------------------------------------------------
|
|
resolve_target_version
|
|
detect_arch
|
|
skip_if_equal
|
|
download_and_verify
|
|
install_binary
|
|
ensure_data_dir
|
|
setup_env
|
|
write_unit
|
|
enable_and_start
|
|
|
|
if [ "$FIRST_INSTALL" -eq 1 ]; then
|
|
secret=$(grep -E '^VOCAT_ADMIN_PASSWORD=' "$ENV_FILE" | cut -d= -f2-)
|
|
echo
|
|
msg "================ 安装完成 ================" "================ Install complete ================"
|
|
msg "首次安装已生成管理员初始密码 (仅显示一次):" "First-install admin password (shown once):"
|
|
echo
|
|
echo " $secret"
|
|
echo
|
|
msg "用户名为 admin。请立即记录此密码。" "Username is admin. Record this password now."
|
|
msg "登录后或运行以下命令修改密码:" "Change it via the web UI or run:"
|
|
echo " sudo vocat menu"
|
|
msg "==========================================" "=============================================="
|
|
else
|
|
echo
|
|
msg "================ 更新完成 ================" "================ Update complete ================"
|
|
msg "已更新到 $TARGET_VERSION,服务已重启。" "Updated to $TARGET_VERSION; service restarted."
|
|
msg "管理员密码保持不变。" "Admin password unchanged."
|
|
msg "==========================================" "=============================================="
|
|
fi
|