#!/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 | 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" ENV_DIR="/etc/vocat" ENV_FILE="${ENV_DIR}/env" UNIT_PATH="/etc/systemd/system/vocat.service" VOCAT_USER="vocat" # --- 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() { while true; do echo "选择语言 / Select language: 1) 中文 2) English" printf '> ' read -r choice 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 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" } # --- System user (idempotent) ------------------------------------------------ ensure_user() { if id "$VOCAT_USER" >/dev/null 2>&1; then return fi useradd --system --no-create-home --shell /usr/sbin/nologin "$VOCAT_USER" } # --- Data directory ---------------------------------------------------------- ensure_data_dir() { install -d -m 0755 /opt/vocat/data chown -R "$VOCAT_USER":"$VOCAT_USER" /opt/vocat || true } # --- 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=$(tr -dc 'A-Za-z0-9' "$ENV_FILE" chmod 0600 "$ENV_FILE" FIRST_INSTALL=1 } # --- systemd unit ------------------------------------------------------------ write_unit() { cat > "$UNIT_PATH" <