install.sh 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. #!/usr/bin/env bash
  2. #
  3. # vocat install / update script for binary + systemd deployments.
  4. #
  5. # Usage:
  6. # sudo bash install.sh [version] # install a specific version
  7. # sudo bash install.sh # install latest release
  8. # sudo bash install.sh --force # reinstall even at the same version
  9. # curl -fsSL <raw url> | sudo bash # one-liner (latest)
  10. #
  11. # Behavior:
  12. # - Prompts for script language (中文 / English) as soon as it runs.
  13. # - If the installed version equals the target version, does nothing (unless --force).
  14. # - On first install, generates a random 32-char admin password, writes it to
  15. # /etc/vocat/env (0600, loaded by the systemd unit), and prints it ONCE.
  16. # - On update, preserves the existing env file and credentials.
  17. # - (Re)writes the systemd unit and restarts the service.
  18. #
  19. # Published script: must contain no secrets, IPs, or passwords.
  20. set -euo pipefail
  21. # --- Publisher configuration -------------------------------------------------
  22. # Default GitHub repository in owner/name form. Publishers: set this to your
  23. # own repo, or override per-run with VOCAT_REPO.
  24. REPO="${VOCAT_REPO:-MengMengCode/VoCat}"
  25. INSTALL_DIR="/opt/vocat/bin"
  26. BINARY_PATH="${INSTALL_DIR}/vocat"
  27. ENV_DIR="/etc/vocat"
  28. ENV_FILE="${ENV_DIR}/env"
  29. UNIT_PATH="/etc/systemd/system/vocat.service"
  30. VOCAT_USER="vocat"
  31. # --- Language ----------------------------------------------------------------
  32. LANG_CHOICE=""
  33. msg() {
  34. # $1 = zh text, $2 = en text
  35. if [ "$LANG_CHOICE" = "en" ]; then
  36. printf '%s\n' "$2"
  37. else
  38. printf '%s\n' "$1"
  39. fi
  40. }
  41. prompt_language() {
  42. while true; do
  43. echo "选择语言 / Select language: 1) 中文 2) English"
  44. printf '> '
  45. read -r choice
  46. case "$choice" in
  47. 1|"") LANG_CHOICE="zh"; return ;;
  48. 2) LANG_CHOICE="en"; return ;;
  49. esac
  50. done
  51. }
  52. die() {
  53. msg "$1" "$2" >&2
  54. exit 1
  55. }
  56. # --- Root --------------------------------------------------------------------
  57. [ "$(id -u)" -eq 0 ] || die "请以 root 身份运行此脚本。" "Run this script as root."
  58. prompt_language
  59. # --- Parse args --------------------------------------------------------------
  60. FORCE=0
  61. TARGET_VERSION=""
  62. for arg in "$@"; do
  63. case "$arg" in
  64. --force) FORCE=1 ;;
  65. -h|--help)
  66. msg "用法: sudo bash install.sh [--force] [版本]" "Usage: sudo bash install.sh [--force] [version]"
  67. exit 0
  68. ;;
  69. *) TARGET_VERSION="${arg#v}" ;;
  70. esac
  71. done
  72. # --- Resolve target version --------------------------------------------------
  73. resolve_target_version() {
  74. if [ -n "$TARGET_VERSION" ]; then
  75. TARGET_VERSION="${TARGET_VERSION#v}"
  76. return
  77. fi
  78. local api_url="https://api.github.com/repos/${REPO}/releases/latest"
  79. local auth_hdr=()
  80. if [ -n "${GITHUB_TOKEN:-}" ]; then
  81. auth_hdr=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
  82. fi
  83. local resp
  84. resp=$(curl -fsSL "${auth_hdr[@]}" "$api_url") || die "无法获取最新版本信息。检查网络或 REPO 设置。" "Failed to fetch latest release. Check network or REPO."
  85. # Parse "tag_name": "vX.Y.Z" without jq.
  86. local tag
  87. tag=$(printf '%s\n' "$resp" | grep -m1 '"tag_name"' | sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
  88. [ -n "$tag" ] || die "无法解析最新版本的 tag_name。" "Could not parse tag_name from the release response."
  89. TARGET_VERSION="${tag#v}"
  90. }
  91. # --- Skip if already installed at the same version ---------------------------
  92. skip_if_equal() {
  93. [ -x "$BINARY_PATH" ] || return 0
  94. [ "$FORCE" -eq 1 ] && return 0
  95. local installed
  96. installed=$("$BINARY_PATH" version 2>/dev/null | awk '{print $2}' | sed -E 's/[[:space:]]*\(.*$//') || return 0
  97. [ -z "$installed" ] && return 0
  98. if [ "$installed" = "$TARGET_VERSION" ]; then
  99. msg "已安装版本 $installed,与目标版本相同,跳过更新。" "Installed version $installed equals target; skipping."
  100. exit 0
  101. fi
  102. msg "当前 $installed -> $TARGET_VERSION,开始更新。" "Updating $installed -> $TARGET_VERSION."
  103. }
  104. # --- Detect architecture -----------------------------------------------------
  105. detect_arch() {
  106. case "$(uname -m)" in
  107. x86_64) ARCH="amd64" ;;
  108. i386|i486|i586|i686) ARCH="386" ;;
  109. aarch64|arm64) ARCH="arm64" ;;
  110. armv7l|armv7*) ARCH="armv7" ;;
  111. *) die "不支持的架构: $(uname -m)" "Unsupported architecture: $(uname -m)" ;;
  112. esac
  113. }
  114. # --- Download + verify -------------------------------------------------------
  115. VOCAT_TMP=""
  116. download_and_verify() {
  117. VOCAT_TMP=$(mktemp -d)
  118. trap 'rm -rf "$VOCAT_TMP"' EXIT
  119. local base="https://github.com/${REPO}/releases/download/v${TARGET_VERSION}"
  120. local asset="vocat-linux-${ARCH}"
  121. msg "下载 $asset ..." "Downloading $asset ..."
  122. curl -fsSL -o "${VOCAT_TMP}/vocat" "${base}/${asset}" || die "下载二进制失败。" "Failed to download the binary."
  123. curl -fsSL -o "${VOCAT_TMP}/SHA256SUMS" "${base}/SHA256SUMS" || die "下载 SHA256SUMS 失败。" "Failed to download SHA256SUMS."
  124. local expected actual
  125. # Match a line whose filename field equals the asset (with optional binary-mode * prefix).
  126. expected=$(awk -v a="$asset" '$2 == a || $2 == ("*" a) {print $1; exit}' "${VOCAT_TMP}/SHA256SUMS")
  127. [ -n "$expected" ] || die "SHA256SUMS 中找不到 $asset 的校验行。" "$asset not found in SHA256SUMS."
  128. actual=$(sha256sum "${VOCAT_TMP}/vocat" | awk '{print $1}')
  129. [ "$actual" = "$expected" ] || die "SHA-256 校验失败。" "SHA-256 verification failed."
  130. }
  131. # --- Install binary ----------------------------------------------------------
  132. install_binary() {
  133. install -d -m 0755 "$INSTALL_DIR"
  134. install -m 0755 "${VOCAT_TMP}/vocat" "$BINARY_PATH"
  135. }
  136. # --- System user (idempotent) ------------------------------------------------
  137. ensure_user() {
  138. if id "$VOCAT_USER" >/dev/null 2>&1; then
  139. return
  140. fi
  141. useradd --system --no-create-home --shell /usr/sbin/nologin "$VOCAT_USER"
  142. }
  143. # --- Data directory ----------------------------------------------------------
  144. ensure_data_dir() {
  145. install -d -m 0755 /opt/vocat/data
  146. chown -R "$VOCAT_USER":"$VOCAT_USER" /opt/vocat || true
  147. }
  148. # --- Env file (first install only) -------------------------------------------
  149. # Generates a random 32-char secret, stores it in the 0600 env file, and flags
  150. # FIRST_INSTALL so we can print the secret once at the end.
  151. FIRST_INSTALL=0
  152. setup_env() {
  153. if [ -f "$ENV_FILE" ]; then
  154. return
  155. fi
  156. install -d -m 0755 "$ENV_DIR"
  157. local secret
  158. secret=$(tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32)
  159. [ -n "$secret" ] || die "生成随机密钥失败。" "Failed to generate a random secret."
  160. printf 'VOCAT_ADMIN_PASSWORD=%s\n' "$secret" > "$ENV_FILE"
  161. chmod 0600 "$ENV_FILE"
  162. FIRST_INSTALL=1
  163. }
  164. # --- systemd unit ------------------------------------------------------------
  165. write_unit() {
  166. cat > "$UNIT_PATH" <<EOF
  167. [Unit]
  168. Description=vocat
  169. After=network.target
  170. [Service]
  171. User=${VOCAT_USER}
  172. EnvironmentFile=${ENV_FILE}
  173. Environment=VOCAT_DATABASE_PATH=/opt/vocat/data/vocat.db
  174. ExecStart=${BINARY_PATH}
  175. Restart=on-failure
  176. [Install]
  177. WantedBy=multi-user.target
  178. EOF
  179. chmod 0644 "$UNIT_PATH"
  180. }
  181. enable_and_start() {
  182. systemctl daemon-reload
  183. systemctl enable --now vocat
  184. }
  185. # --- Main --------------------------------------------------------------------
  186. resolve_target_version
  187. detect_arch
  188. skip_if_equal
  189. download_and_verify
  190. install_binary
  191. ensure_user
  192. ensure_data_dir
  193. setup_env
  194. write_unit
  195. enable_and_start
  196. if [ "$FIRST_INSTALL" -eq 1 ]; then
  197. secret=$(grep -E '^VOCAT_ADMIN_PASSWORD=' "$ENV_FILE" | cut -d= -f2-)
  198. echo
  199. msg "================ 安装完成 ================" "================ Install complete ================"
  200. msg "首次安装已生成管理员初始密码 (仅显示一次):" "First-install admin password (shown once):"
  201. echo
  202. echo " $secret"
  203. echo
  204. msg "用户名为 admin。请立即记录此密码。" "Username is admin. Record this password now."
  205. msg "登录后或运行以下命令修改密码:" "Change it via the web UI or run:"
  206. echo " sudo vocat menu"
  207. msg "==========================================" "=============================================="
  208. else
  209. echo
  210. msg "================ 更新完成 ================" "================ Update complete ================"
  211. msg "已更新到 $TARGET_VERSION,服务已重启。" "Updated to $TARGET_VERSION; service restarted."
  212. msg "管理员密码保持不变。" "Admin password unchanged."
  213. msg "==========================================" "=============================================="
  214. fi