#!/usr/bin/env bash set -Eeuo pipefail ETC_DIR=${PLUGIN_ETC_DIR:-/etc/sub2api-add} VAR_DIR=${PLUGIN_VAR_DIR:-/var/lib/sub2api-add} PREFIX=${PLUGIN_INSTALL_PREFIX:-/opt/sub2api-add} SOURCE_DIR=${PLUGIN_SOURCE_DIR:-/opt/sub2api-add} RUN_USER=${PLUGIN_SYSTEM_USER:-sub2api-plugin} SELECTION=all PURGE=false YES=false validate_managed_root() { local value=$1 label=$2 component current="" [[ -n "$value" && "$value" = /* ]] || die "$label 必须是绝对路径" [[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || die "$label 包含非法换行" [[ "$value" != "/" ]] || die "$label 不允许指向根目录" local parts=() IFS='/' read -r -a parts <<< "${value#/}" for component in "${parts[@]}"; do [[ -z "$component" ]] && continue [[ "$component" != "." && "$component" != ".." ]] || die "$label 不能包含 . 或 .. 路径组件" current="$current/$component" [[ ! -L "$current" ]] || die "$label 的路径组件不能是符号链接:$current" [[ ! -e "$current" || -d "$current" ]] || die "$label 的路径组件不是目录:$current" done if [[ "$value" != */sub2api-add && "${PLUGIN_ALLOW_CUSTOM_PATHS:-false}" != "true" ]]; then die "$label 必须位于受管的 sub2api-add 目录;如确需自定义路径请显式设置 PLUGIN_ALLOW_CUSTOM_PATHS=true" fi } ensure_real_parent() { local path=$1 label=$2 parent component current="" parts=() parent=$(dirname -- "$path") IFS='/' read -r -a parts <<< "${parent#/}" for component in "${parts[@]}"; do [[ -z "$component" ]] && continue current="$current/$component" [[ -d "$current" && ! -L "$current" ]] || die "$label 的父目录必须是已存在的真实目录:$current" done } ensure_real_dir() { local path=$1 label=$2 component current="" parts=() [[ -n "$path" && "$path" = /* ]] || die "$label 路径无效" IFS='/' read -r -a parts <<< "${path#/}" for component in "${parts[@]}"; do [[ -z "$component" ]] && continue current="$current/$component" [[ -d "$current" && ! -L "$current" ]] || die "$label 必须是已存在的真实目录:$current" done } ensure_regular_target() { local path=$1 label=$2 [[ ! -L "$path" ]] || die "$label 不能是符号链接:$path" } die() { printf '错误:%s\n' "$*" >&2; exit 1; } [[ $EUID -eq 0 ]] || die "请使用 root 或 sudo 运行" command -v systemctl >/dev/null 2>&1 || die "缺少 systemd/systemctl" while (($#)); do case "$1" in --plugin) (($# >= 2)) || die "--plugin 需要参数"; SELECTION=$2; shift 2 ;; --purge) PURGE=true; shift ;; --yes) YES=true; shift ;; -h|--help) printf '%s\n' '用法:deploy/uninstall.sh [--plugin all|plugin-admin|subscription-admin] [--purge --yes]'; exit 0 ;; *) die "未知参数:$1" ;; esac done case "$SELECTION" in all) PLUGINS=(plugin-admin subscription-admin) ;; plugin-admin|subscription-admin) PLUGINS=("$SELECTION") ;; *) die "插件必须是 all、plugin-admin 或 subscription-admin" ;; esac validate_managed_root "$ETC_DIR" PLUGIN_ETC_DIR validate_managed_root "$VAR_DIR" PLUGIN_VAR_DIR validate_managed_root "$PREFIX" PLUGIN_INSTALL_PREFIX validate_managed_root "$SOURCE_DIR" PLUGIN_SOURCE_DIR ensure_real_parent "$ETC_DIR" PLUGIN_ETC_DIR ensure_real_parent "$VAR_DIR" PLUGIN_VAR_DIR ensure_real_parent "$PREFIX" PLUGIN_INSTALL_PREFIX ensure_real_parent "$SOURCE_DIR" PLUGIN_SOURCE_DIR ensure_real_dir /etc/systemd/system SYSTEMD_UNIT_DIR if $PURGE && [[ "${PLUGIN_PURGE_SOURCE:-true}" == "true" && "$SOURCE_DIR" != "$PREFIX" && "${PLUGIN_ALLOW_CUSTOM_PATHS:-false}" != "true" ]]; then die "为避免误删,--purge 默认要求 PLUGIN_SOURCE_DIR 与 PLUGIN_INSTALL_PREFIX 相同" fi if $PURGE && ! $YES; then [[ -t 0 ]] || die "非交互环境执行 --purge 时必须同时提供 --yes" read -r -p "将删除插件配置和数据,继续?[y/N] " answer [[ "$answer" == [yY] ]] || { printf '%s\n' '已取消'; exit 0; } fi for plugin in "${PLUGINS[@]}"; do unit="sub2api-$plugin.service" unit_path="/etc/systemd/system/$unit" if [[ -e "$unit_path" || -L "$unit_path" ]]; then ensure_regular_target "$unit_path" SYSTEMD_UNIT fi systemctl disable --now "$unit" 2>/dev/null || true rm -f -- "$unit_path" # Keep the checked-out source clean so a later install can fast-forward it. # Only generated binaries are removed unless --purge is explicitly used. if [[ -e "$PREFIX/$plugin/bin/$plugin" || -L "$PREFIX/$plugin/bin/$plugin" ]]; then ensure_real_parent "$PREFIX/$plugin/bin/$plugin" PLUGIN_INSTALL_PREFIX rm -f -- "$PREFIX/$plugin/bin/$plugin" fi if $PURGE; then plugin_root="$PREFIX/$plugin" if [[ -d "$plugin_root" && ! -L "$plugin_root" ]]; then ensure_real_parent "$plugin_root" PLUGIN_INSTALL_PREFIX rm -rf -- "${plugin_root:?}" fi rm -f -- "$ETC_DIR/$plugin.env" plugin_data="$VAR_DIR/$plugin" if [[ -d "$plugin_data" && ! -L "$plugin_data" ]]; then ensure_real_parent "$plugin_data" PLUGIN_VAR_DIR rm -rf -- "${plugin_data:?}" fi fi done systemctl daemon-reload if $PURGE && [[ -d "$ETC_DIR" && ! -L "$ETC_DIR" ]]; then rmdir "$ETC_DIR" 2>/dev/null || true; fi if $PURGE && [[ -d "$VAR_DIR" && ! -L "$VAR_DIR" ]]; then rmdir "$VAR_DIR" 2>/dev/null || true; fi if $PURGE && [[ "${PLUGIN_PURGE_SOURCE:-true}" == "true" && "$SOURCE_DIR" == "$PREFIX" && -d "$SOURCE_DIR" && ! -L "$SOURCE_DIR" ]]; then ensure_real_parent "$SOURCE_DIR" PLUGIN_SOURCE_DIR; rm -rf -- "$SOURCE_DIR"; fi if $PURGE; then userdel "$RUN_USER" 2>/dev/null || true printf '%s\n' '插件、配置、数据和源码已删除;Core 数据库未被触碰。' else printf '%s\n' "插件服务已卸载;配置和数据仍保留在 $ETC_DIR 与 $VAR_DIR。" fi