Files
frpc-console/deploy.sh
T

1083 lines
39 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# ============================================================
# frpc-console 一键部署脚本 (Docker 优先)
# 用法:
# run-deploy.sh 引导后自动执行
# ============================================================
set -e
# ---------- 颜色检测 ----------
if [ -t 1 ]; then
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[0;36m'; MAGENTA='\033[0;35m'; NC='\033[0m'
else
RED=''; GREEN=''; YELLOW=''; BLUE=''; CYAN=''; MAGENTA=''; NC=''
fi
# ---------- 配置 ----------
REPO_URL="https://git.whitetop.xyz/lxh2875931338/frpc-console.git"
WORK_DIR="/tmp/frpc-console-build"
DEFAULT_PORT=9300
DEFAULT_DEPLOY_DIR="/opt/frpc-console"
IMAGE_NAME="frpc-console"
KEEP_IMAGES=3
# ---------- 镜像加速配置 ----------
DEFAULT_MIRRORS=(
"https://docker.1panel.live"
"https://docker.1panel.dev"
"https://docker.1ms.run"
)
DAEMON_JSON="/etc/docker/daemon.json"
MIRROR_CHECK_TIMEOUT=5
# ---------- 镜像加速状态变量 ----------
MIRROR_STATUS="" # no_file | no_key | empty_array | has_valid_ordered | has_valid_reorder_needed | all_invalid
DEFAULT_STATUS="" # default_unavailable | default_partial | default_all_available
WRITE_DAEMON=false
RESTART_DOCKER=false
REORDER=false
INSERT_DEFAULT=false
HAS_REGISTRY_MIRRORS_KEY=false
AVAILABLE_MIRRORS=() # 可用的默认地址
USER_MIRRORS_AVAILABLE=() # 用户配置中可用的地址
USER_MIRRORS_UNAVAILABLE=() # 用户配置中不可用的地址
USER_MIRRORS_ALL=() # 用户配置所有地址(保持原始顺序)
MIRROR_DETECT_OUTPUT="" # 检测阶段输出给用户的描述
# ---------- 其他状态变量 ----------
OS=""; OS_VERSION=""; ARCH=""
HAS_GIT=false; HAS_CURL=false; HAS_WGET=false; HAS_DOCKER=false
HAS_JQ=false
NEED_INSTALL_GIT=false; NEED_INSTALL_CURL=false; NEED_INSTALL_WGET=false
PORT=${DEFAULT_PORT}
DEPLOY_DIR=${DEFAULT_DEPLOY_DIR}
DATA_DIR="${DEPLOY_DIR}/data"
CONTAINER_EXISTS=false; CONTAINER_RUNNING=false
SKIP_CONFIRM=false; CHECK_ONLY=false; DRY_RUN=false
CHANNEL=""; BRANCH=""; TARGET_VERSION=""; IMAGE_TAG=""
CURRENT_VERSION=""
# ---------- 打印函数 ----------
print_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
print_success() { echo -e "${GREEN}[✓]${NC} $1"; }
print_warn() { echo -e "${YELLOW}[⚠]${NC} $1"; }
print_error() { echo -e "${RED}[✗]${NC} $1"; }
print_step() { echo -e "\n${CYAN}${NC} $1"; }
print_title() { echo -e "\n${MAGENTA}════════════════════════════════════════════════════════${NC}"; }
print_subtitle() { echo -e "${MAGENTA} $1${NC}"; }
# ---------- 解析命令行参数 ----------
parse_args() {
for arg in "$@"; do
case $arg in
--yes|-y) SKIP_CONFIRM=true ;;
--check) CHECK_ONLY=true ;;
--dry-run) DRY_RUN=true ;;
--help|-h)
echo "用法: ./deploy.sh [选项]"
echo "选项:"
echo " --yes, -y 跳过所有确认提示"
echo " --check 只检测环境,不执行部署"
echo " --dry-run 显示将执行的操作,不实际执行"
echo " --help, -h 显示帮助信息"
exit 0
;;
*) print_error "未知选项: $arg"; exit 1 ;;
esac
done
}
# ---------- 权限检测 ----------
check_root() {
if [ "$EUID" -ne 0 ]; then
print_error "请使用 root 权限运行此脚本"
echo " sudo bash deploy.sh"
exit 1
fi
}
# ---------- 系统检测 ----------
detect_os() {
if [ -f /etc/os-release ]; then
. /etc/os-release
OS=$ID; OS_VERSION=$VERSION_ID
else
print_error "无法识别操作系统"
exit 1
fi
}
detect_arch() {
ARCH=$(uname -m)
case $ARCH in
x86_64|amd64|aarch64|arm64|armv7l|armhf)
print_success "CPU 架构: $ARCH"
;;
*)
print_error "不支持的 CPU 架构: $ARCH"
exit 1
;;
esac
}
# ---------- 工具检测 ----------
check_tools() {
command -v git &> /dev/null && HAS_GIT=true || NEED_INSTALL_GIT=true
command -v curl &> /dev/null && HAS_CURL=true || NEED_INSTALL_CURL=true
command -v wget &> /dev/null && HAS_WGET=true || NEED_INSTALL_WGET=true
command -v docker &> /dev/null && HAS_DOCKER=true
command -v jq &> /dev/null && HAS_JQ=true || HAS_JQ=false
}
check_container() {
if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^frpc-console$"; then
CONTAINER_EXISTS=true
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^frpc-console$"; then
CONTAINER_RUNNING=true
fi
fi
}
# ---------- 镜像加速:连通性检测 ----------
check_mirror_available() {
local url="$1"
local http_code
http_code=$(curl -s --max-time "$MIRROR_CHECK_TIMEOUT" -o /dev/null -w "%{http_code}" \
"${url}/v2/" 2>/dev/null)
if [[ "$http_code" =~ ^2[0-9][0-9]$ ]] || [ "$http_code" = "401" ] || [ "$http_code" = "403" ]; then
return 0
fi
return 1
}
# ---------- 镜像加速:安装 jq ----------
install_jq() {
print_info "尝试安装 jq..."
case $OS in
opensuse*|suse*|opensuse-tumbleweed|opensuse-slowroll|opensuse-leap)
zypper install -y jq 2>/dev/null || true
;;
ubuntu|debian|linuxmint)
apt update -qq 2>/dev/null && apt install -y jq 2>/dev/null || true
;;
centos|rhel|fedora|rocky|almalinux)
yum install -y jq 2>/dev/null || true
;;
alpine)
apk add jq 2>/dev/null || true
;;
arch|manjaro|endeavouros)
pacman -S --needed --noconfirm jq 2>/dev/null || true
;;
*)
return 1
;;
esac
command -v jq &> /dev/null && HAS_JQ=true || HAS_JQ=false
if [ "$HAS_JQ" = true ]; then
print_success "jq 安装成功"
return 0
else
print_warn "jq 安装失败,将使用 sed 模式(功能受限)"
return 1
fi
}
# ---------- 镜像加速:检测阶段 ----------
check_docker_mirrors() {
print_step "检测 Docker 镜像源配置..."
# 1. 检测三个默认地址的连通性
print_info "检测默认镜像源连通性 (超时 ${MIRROR_CHECK_TIMEOUT}s)..."
local default_available_count=0
for mirror in "${DEFAULT_MIRRORS[@]}"; do
if check_mirror_available "$mirror"; then
AVAILABLE_MIRRORS+=("$mirror")
default_available_count=$((default_available_count + 1))
print_info " ✅ $mirror 可用"
else
print_info " ❌ $mirror 不可用"
fi
done
if [ ${#AVAILABLE_MIRRORS[@]} -eq 0 ]; then
DEFAULT_STATUS="default_unavailable"
print_warn "所有默认镜像源均不可用,将跳过镜像加速配置"
elif [ ${#AVAILABLE_MIRRORS[@]} -eq ${#DEFAULT_MIRRORS[@]} ]; then
DEFAULT_STATUS="default_all_available"
print_success "所有默认镜像源均可用"
else
DEFAULT_STATUS="default_partial"
print_info "部分默认镜像源可用 (${#AVAILABLE_MIRRORS[@]}/${#DEFAULT_MIRRORS[@]})"
fi
# 2. 检查 daemon.json
if [ ! -f "$DAEMON_JSON" ]; then
MIRROR_STATUS="no_file"
HAS_REGISTRY_MIRRORS_KEY=false
print_info "daemon.json 不存在"
# 生成检测报告
generate_mirror_detect_output
return 0
fi
# 3. 检测 jq 是否可解析
if ! jq -e '.' "$DAEMON_JSON" >/dev/null 2>&1 2>/dev/null; then
print_warn "daemon.json 格式不标准,将备份并重建"
local backup_path="${DAEMON_JSON}.bak.$(date +%Y%m%d-%H%M%S)"
cp "$DAEMON_JSON" "$backup_path"
print_info "已备份至: $backup_path"
echo '{}' > "$DAEMON_JSON"
print_warn "已重建 daemon.json,请确认是否需要将其他自定义配置迁回"
MIRROR_STATUS="no_file"
HAS_REGISTRY_MIRRORS_KEY=false
generate_mirror_detect_output
return 0
fi
# 4. 检查是否存在 registry-mirrors 字段
local has_key
has_key=$(jq -e 'has("registry-mirrors")' "$DAEMON_JSON" 2>/dev/null)
if [ "$has_key" != "true" ]; then
HAS_REGISTRY_MIRRORS_KEY=false
MIRROR_STATUS="no_key"
print_info "daemon.json 中无 registry-mirrors 字段"
generate_mirror_detect_output
return 0
fi
HAS_REGISTRY_MIRRORS_KEY=true
# 5. 读取 registry-mirrors 数组
local mirror_list
mirror_list=$(jq -r '."registry-mirrors" // [] | .[]' "$DAEMON_JSON" 2>/dev/null)
if [ -z "$mirror_list" ]; then
MIRROR_STATUS="empty_array"
print_info "registry-mirrors 为空数组"
generate_mirror_detect_output
return 0
fi
# 6. 读取用户配置所有地址(保持顺序)
USER_MIRRORS_ALL=()
while IFS= read -r mirror; do
if [ -n "$mirror" ]; then
USER_MIRRORS_ALL+=("$mirror")
fi
done <<< "$mirror_list"
# 7. 检测用户配置中每个地址的连通性
print_info "检测用户配置的镜像源连通性..."
USER_MIRRORS_AVAILABLE=()
USER_MIRRORS_UNAVAILABLE=()
for mirror in "${USER_MIRRORS_ALL[@]}"; do
if check_mirror_available "$mirror"; then
USER_MIRRORS_AVAILABLE+=("$mirror")
print_info " ✅ $mirror (用户配置) 可用"
else
USER_MIRRORS_UNAVAILABLE+=("$mirror")
print_info " ❌ $mirror (用户配置) 不可用"
fi
done
# 8. 判断状态
if [ ${#USER_MIRRORS_AVAILABLE[@]} -gt 0 ]; then
# 检查是否需要重排:是否存在不可用在可用之前
local need_reorder=false
local seen_unavailable=false
for mirror in "${USER_MIRRORS_ALL[@]}"; do
local is_avail=false
for a in "${USER_MIRRORS_AVAILABLE[@]}"; do
if [ "$a" = "$mirror" ]; then
is_avail=true
break
fi
done
if [ "$is_avail" = true ] && [ "$seen_unavailable" = true ]; then
need_reorder=true
break
fi
if [ "$is_avail" = false ]; then
seen_unavailable=true
fi
done
if [ "$need_reorder" = true ]; then
MIRROR_STATUS="has_valid_reorder_needed"
print_info "可用镜像源排在不可用镜像源之后,需要重排"
else
MIRROR_STATUS="has_valid_ordered"
print_info "用户镜像源配置正常,无需调整"
fi
else
# 用户配置全部不可用
MIRROR_STATUS="all_invalid"
print_warn "用户配置全部不可用"
fi
generate_mirror_detect_output
}
# ---------- 镜像加速:生成检测输出 ----------
generate_mirror_detect_output() {
MIRROR_DETECT_OUTPUT=""
# 如果默认地址全部不可用,直接输出警告
if [ "$DEFAULT_STATUS" = "default_unavailable" ]; then
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${YELLOW}⚠ 所有默认镜像源均不可用,将跳过镜像加速配置${NC}\n"
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}如果 Docker 构建失败,请检查网络或手动配置镜像源。\n"
return
fi
# 构建可用地址列表显示
local avail_list=""
for m in "${AVAILABLE_MIRRORS[@]}"; do
avail_list="${avail_list}${m}\n"
done
case "$MIRROR_STATUS" in
no_file|no_key|empty_array)
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${YELLOW}未检测到 registry-mirrors 配置。${NC}\n\n"
if [ ${#AVAILABLE_MIRRORS[@]} -gt 0 ]; then
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}将写入以下可用镜像源:\n\n${avail_list}\n"
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}写入位置:${DAEMON_JSON}\n"
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${YELLOW}Docker 将在配置完成后自动重启以应用配置。${NC}\n"
WRITE_DAEMON=true
RESTART_DOCKER=true
REORDER=false
INSERT_DEFAULT=true
else
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${RED}无可用的默认镜像源,将跳过配置。${NC}\n"
WRITE_DAEMON=false
RESTART_DOCKER=false
REORDER=false
INSERT_DEFAULT=false
fi
;;
has_valid_ordered)
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${GREEN}检测完成。用户已配置可用的镜像源:${NC}\n\n"
for m in "${USER_MIRRORS_AVAILABLE[@]}"; do
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${m}\n"
done
if [ ${#USER_MIRRORS_UNAVAILABLE[@]} -gt 0 ]; then
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}\n${YELLOW}不可用地址:${NC}\n"
for m in "${USER_MIRRORS_UNAVAILABLE[@]}"; do
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${m}\n"
done
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}\n可用地址已排在不可用地址之前,无需修改。\n"
else
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}\n所有地址均可用,无需修改。\n"
fi
WRITE_DAEMON=false
RESTART_DOCKER=false
REORDER=false
INSERT_DEFAULT=false
;;
has_valid_reorder_needed)
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${YELLOW}检测完成。用户配置中存在不可用或顺序不当的镜像源。${NC}\n\n"
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}当前配置顺序:\n"
local idx=1
for m in "${USER_MIRRORS_ALL[@]}"; do
local status=""
local found=false
for a in "${USER_MIRRORS_AVAILABLE[@]}"; do
if [ "$a" = "$m" ]; then
found=true
break
fi
done
if [ "$found" = true ]; then
status="${GREEN}✅ 可用${NC}"
else
status="${RED}❌ 不可用${NC}"
fi
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT} ${idx}. ${m} ${status}\n"
idx=$((idx + 1))
done
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}\n${YELLOW}将重新排序:可用地址前置,不可用地址后移。${NC}\n"
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}\n新顺序:\n"
local new_idx=1
for m in "${USER_MIRRORS_AVAILABLE[@]}"; do
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT} ${new_idx}. ${m} ${GREEN}${NC}\n"
new_idx=$((new_idx + 1))
done
for m in "${USER_MIRRORS_UNAVAILABLE[@]}"; do
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT} ${new_idx}. ${m} ${RED}${NC}\n"
new_idx=$((new_idx + 1))
done
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}\n${YELLOW}Docker 将在配置完成后自动重启以应用配置。${NC}\n"
WRITE_DAEMON=true
RESTART_DOCKER=true
REORDER=true
INSERT_DEFAULT=false
;;
all_invalid)
if [ ${#AVAILABLE_MIRRORS[@]} -gt 0 ]; then
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${YELLOW}检测完成。用户配置的镜像源全部不可用:${NC}\n\n"
for m in "${USER_MIRRORS_UNAVAILABLE[@]}"; do
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${m}\n"
done
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}\n${GREEN}检测到可用镜像源:${NC}\n\n"
for m in "${AVAILABLE_MIRRORS[@]}"; do
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${m}\n"
done
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}\n${YELLOW}将把可用地址插入到用户配置最前面。${NC}\n"
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${YELLOW}Docker 将在配置完成后自动重启以应用配置。${NC}\n"
WRITE_DAEMON=true
RESTART_DOCKER=true
REORDER=false
INSERT_DEFAULT=true
else
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${RED}检测完成。用户配置的镜像源全部不可用:${NC}\n\n"
for m in "${USER_MIRRORS_UNAVAILABLE[@]}"; do
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${m}\n"
done
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}\n${RED}默认镜像源也全部不可用。将跳过镜像加速配置。${NC}\n"
MIRROR_DETECT_OUTPUT="${MIRROR_DETECT_OUTPUT}${YELLOW}如果 Docker 构建失败,请检查网络连接,或手动配置 /etc/docker/daemon.json 中的 registry-mirrors。${NC}\n"
WRITE_DAEMON=false
RESTART_DOCKER=false
REORDER=false
INSERT_DEFAULT=false
fi
;;
esac
# 打印检测输出
echo ""
print_title
print_subtitle "Docker 镜像源检测结果"
echo ""
echo -e "$MIRROR_DETECT_OUTPUT"
print_title
echo ""
}
# ---------- 镜像加速:配置执行阶段 ----------
configure_docker_mirrors() {
if [ "$WRITE_DAEMON" != true ]; then
print_info "无需修改 Docker 镜像源配置"
return 0
fi
print_step "配置 Docker 镜像源..."
# 确保 jq 可用
if [ "$HAS_JQ" = false ]; then
print_warn "jq 未安装,尝试安装..."
if ! install_jq; then
print_error "jq 安装失败,无法配置镜像源"
print_error "请手动配置 /etc/docker/daemon.json 中的 registry-mirrors"
return 1
fi
fi
# 构建新镜像列表
local new_mirrors=()
if [ "$REORDER" = true ]; then
# 重排:可用前置,不可用后移(仅重排用户配置)
new_mirrors=("${USER_MIRRORS_AVAILABLE[@]}" "${USER_MIRRORS_UNAVAILABLE[@]}")
print_info "重排镜像源顺序"
elif [ "$INSERT_DEFAULT" = true ]; then
if [ "$MIRROR_STATUS" = "no_file" ] || [ "$MIRROR_STATUS" = "no_key" ] || [ "$MIRROR_STATUS" = "empty_array" ]; then
# 无配置或空配置:直接写入可用默认地址
new_mirrors=("${AVAILABLE_MIRRORS[@]}")
print_info "写入可用默认镜像源"
else
# all_invalid:可用默认地址插入到最前面
new_mirrors=("${AVAILABLE_MIRRORS[@]}" "${USER_MIRRORS_UNAVAILABLE[@]}")
print_info "插入可用默认镜像源到最前面"
fi
else
print_info "无需修改镜像源配置"
return 0
fi
# 备份 daemon.json
if [ -f "$DAEMON_JSON" ]; then
local backup_path="${DAEMON_JSON}.bak.$(date +%Y%m%d-%H%M%S)"
cp "$DAEMON_JSON" "$backup_path"
print_info "已备份: $backup_path"
fi
# 写入新配置(仅修改 registry-mirrors 字段,保留其他所有字段)
local json_mirrors
json_mirrors=$(printf '%s\n' "${new_mirrors[@]}" | jq -R . | jq -s .)
if ! jq --argjson mirrors "$json_mirrors" '."registry-mirrors" = $mirrors' \
"$DAEMON_JSON" > "${DAEMON_JSON}.tmp" 2>/dev/null; then
print_error "写入 daemon.json 失败"
return 1
fi
mv "${DAEMON_JSON}.tmp" "$DAEMON_JSON"
print_success "daemon.json 已更新"
print_info "新镜像源列表: ${new_mirrors[*]}"
# 重启 Docker
if [ "$RESTART_DOCKER" = true ]; then
print_step "重启 Docker 服务..."
if systemctl restart docker 2>/dev/null; then
print_success "Docker 重启完成"
sleep 2
else
print_error "Docker 重启失败,请手动重启后重新运行脚本"
print_info "手动重启命令: systemctl restart docker"
exit 1
fi
fi
print_success "镜像源配置完成"
return 0
}
# ---------- 版本管理 ----------
read_version_from_remote() {
local branch="$1"
local version_url="https://git.whitetop.xyz/lxh2875931338/frpc-console/raw/${branch}/version.ini"
local content
content=$(curl -sSL "$version_url" 2>/dev/null)
if [ -z "$content" ]; then
print_error "无法从远程读取 version.ini: $version_url"
return 1
fi
SEMVER=$(echo "$content" | grep -v "^[;#]" | head -1 | awk '{print $1}')
echo "$SEMVER"
}
generate_target_version() {
local channel="$1"
local branch="${2:-$BRANCH}"
SEMVER=$(read_version_from_remote "$branch")
if [ -z "$SEMVER" ]; then
print_error "读取语义版本失败"
exit 1
fi
local minor="${SEMVER#*.}"
if [ "$minor" = "0" ] || [ "$minor" = "5" ]; then
CHANNEL_TYPE="lts"
else
CHANNEL_TYPE="preview"
fi
if [ "$channel" = "lts" ] || [ "$CHANNEL_TYPE" = "lts" ]; then
TARGET_VERSION="${SEMVER}-lts-$(date +%Y%m%d)"
IMAGE_TAG="${SEMVER}-lts-$(date +%Y%m%d)"
else
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
TARGET_VERSION="${SEMVER}-preview-${TIMESTAMP}"
IMAGE_TAG="preview-${TIMESTAMP}"
fi
}
get_current_version() {
local version_file="${DEPLOY_DIR}/data/version.ini"
if [ -f "$version_file" ]; then
grep -v "^[;#]" "$version_file" | head -1 | tr -d '\n'
else
if [ "$CONTAINER_EXISTS" = true ]; then
docker exec frpc-console cat /app/data/version.ini 2>/dev/null | grep -v "^[;#]" | head -1 | tr -d '\n' || echo ""
else
echo ""
fi
fi
}
# ---------- 通道选择 ----------
select_channel() {
if [ -n "$DEPLOY_CHANNEL" ] && [ -n "$DEPLOY_BRANCH" ]; then
CHANNEL="$DEPLOY_CHANNEL"
BRANCH="$DEPLOY_BRANCH"
print_info "使用环境变量: ${CHANNEL} 通道 (${BRANCH} 分支)"
return
fi
if [ -n "$CHANNEL" ]; then
if [ "$CHANNEL" != "lts" ] && [ "$CHANNEL" != "preview" ]; then
print_error "无效通道: $CHANNEL"
exit 1
fi
BRANCH="$([ "$CHANNEL" = "preview" ] && echo "test" || echo "main")"
return
fi
echo ""
echo -e "${CYAN}请选择部署通道:${NC}"
echo " 1. LTS (稳定版) [默认] → main 分支"
echo " 2. Preview (技术预览版) → test 分支"
echo ""
read -p "请选择 [1]: " CHANNEL_INPUT </dev/tty
CHANNEL_INPUT=${CHANNEL_INPUT:-1}
case $CHANNEL_INPUT in
1|"") CHANNEL="lts"; BRANCH="main" ;;
2) CHANNEL="preview"; BRANCH="test" ;;
*) print_error "无效选择"; exit 1 ;;
esac
print_info "已选择: ${CHANNEL} 通道 (${BRANCH} 分支)"
}
# ---------- 环境汇总 ----------
print_environment_summary() {
print_title
print_subtitle "环境检测结果"
echo ""
echo -e " ${CYAN}操作系统:${NC} $OS $OS_VERSION"
echo -e " ${CYAN}CPU 架构:${NC} $ARCH"
echo ""
echo " ${CYAN}必要工具:${NC}"
if [ "$HAS_GIT" = true ]; then
echo -e " git ✅ 已安装 ($(git --version | awk '{print $3}'))"
else
echo " git ❌ 未安装 (将自动安装)"
fi
if [ "$HAS_CURL" = true ]; then
echo " curl ✅ 已安装"
else
echo " curl ❌ 未安装 (将自动安装)"
fi
if [ "$HAS_WGET" = true ]; then
echo " wget ✅ 已安装"
else
echo " wget ❌ 未安装 (将自动安装)"
fi
echo ""
echo " ${CYAN}Docker 环境:${NC}"
if [ "$HAS_DOCKER" = true ]; then
echo -e " docker ✅ 已安装 ($(docker --version | awk '{print $3}' | tr -d ','))"
else
echo " docker ❌ 未安装"
print_error "Docker 未安装,请先安装 Docker"
exit 1
fi
if [ "$CONTAINER_EXISTS" = true ]; then
echo ""
echo " ${CYAN}容器状态:${NC}"
if [ "$CONTAINER_RUNNING" = true ]; then
echo -e " frpc-console ✅ 运行中"
else
echo -e " frpc-console ⏸️ 已停止"
fi
CURRENT_VERSION=$(get_current_version)
[ -n "$CURRENT_VERSION" ] && echo -e " ${CYAN}当前版本:${NC} $CURRENT_VERSION"
fi
echo ""
}
# ---------- 部署计划 ----------
generate_plan() {
PLAN=""
# 按时间先后顺序排列
if [ "$WRITE_DAEMON" = true ]; then
PLAN="${PLAN} • 配置 Docker 镜像源"
if [ "$RESTART_DOCKER" = true ]; then
PLAN="${PLAN}(写入可用地址,并重启 Docker\n"
else
PLAN="${PLAN}\n"
fi
fi
if [ "$NEED_INSTALL_GIT" = true ] || [ "$NEED_INSTALL_CURL" = true ] || [ "$NEED_INSTALL_WGET" = true ]; then
local pkgs=""
[ "$NEED_INSTALL_GIT" = true ] && pkgs="${pkgs} git"
[ "$NEED_INSTALL_CURL" = true ] && pkgs="${pkgs} curl"
[ "$NEED_INSTALL_WGET" = true ] && pkgs="${pkgs} wget"
PLAN="${PLAN} • 安装必要工具:${pkgs}\n"
fi
PLAN="${PLAN} • 拉取 frpc-console 源码 (${BRANCH} 分支)\n"
if [ "$CONTAINER_EXISTS" = true ]; then
PLAN="${PLAN} • 停止并删除旧容器\n"
fi
PLAN="${PLAN} • 构建 Docker 镜像: ${IMAGE_NAME}:${IMAGE_TAG}\n"
PLAN="${PLAN} • 启动容器 (端口 ${PORT})\n"
PLAN="${PLAN} • 写入版本文件并验证"
}
print_deployment_plan() {
print_title
print_subtitle "部署计划"
echo ""
echo -e " ${CYAN}将执行以下操作:${NC}"
echo -e "$(echo -e "$PLAN")"
echo ""
echo -e " ${CYAN}配置信息:${NC}"
echo -e " ────────────────────────────────────"
echo -e " 监听端口 : ${PORT}"
echo -e " 部署目录 : ${DEPLOY_DIR}"
echo -e " 目标版本 : ${TARGET_VERSION}"
echo -e " 镜像标签 : ${IMAGE_TAG}"
echo -e " 源码分支 : ${BRANCH}"
echo -e " ────────────────────────────────────"
if [ "$CONTAINER_EXISTS" = true ]; then
echo ""
echo -e " ${YELLOW}⚠ 检测到已存在的 frpc-console 容器${NC}"
echo -e " 当前版本: ${CURRENT_VERSION}"
echo -e " 目标版本: ${TARGET_VERSION}"
fi
echo ""
}
confirm_deploy() {
if [ "$SKIP_CONFIRM" = true ]; then
echo -e "${GREEN}▶ 已启用 --yes,自动确认${NC}"
return 0
fi
echo -e -n "${CYAN}确认执行? 输入 Y 继续,输入 n 自定义配置 [Y/n]: ${NC}"
read -r CONFIRM </dev/tty
case $CONFIRM in
n|N) return 1 ;;
*) return 0 ;;
esac
}
custom_config() {
print_title
print_subtitle "自定义配置"
echo ""
read -p "请输入监听端口 [${DEFAULT_PORT}]: " INPUT_PORT </dev/tty
PORT=${INPUT_PORT:-$DEFAULT_PORT}
read -p "请输入部署目录 [${DEFAULT_DEPLOY_DIR}]: " INPUT_DEPLOY_DIR </dev/tty
DEPLOY_DIR=${INPUT_DEPLOY_DIR:-$DEFAULT_DEPLOY_DIR}
DATA_DIR="${DEPLOY_DIR}/data"
echo ""
echo -e " ${CYAN}更新后的配置:${NC}"
echo -e " ────────────────────────────────────"
echo -e " 监听端口 : ${PORT}"
echo -e " 部署目录 : ${DEPLOY_DIR}"
echo -e " 数据目录 : ${DATA_DIR}"
echo -e " ────────────────────────────────────"
echo ""
}
# ---------- 执行部署 ----------
do_deploy() {
print_title
print_subtitle "开始部署"
echo ""
# ----- 事务前钩子:备份数据库(仅降级时) -----
BACKUP_DIR=""
if [ -n "$CURRENT_VERSION" ] && [ -n "$TARGET_VERSION" ]; then
CUR_SEMVER=$(echo "$CURRENT_VERSION" | sed -E 's/^([0-9]+\.[0-9]+).*$/\1/')
TGT_SEMVER=$(echo "$TARGET_VERSION" | sed -E 's/^([0-9]+\.[0-9]+).*$/\1/')
if [ "$TGT_SEMVER" \< "$CUR_SEMVER" ] 2>/dev/null; then
print_warn "检测到降级操作: $CURRENT_VERSION$TARGET_VERSION"
echo ""
echo -e "${YELLOW}⚠ 降级可能导致数据不兼容${NC}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/opt/frpc-console-backups/${TIMESTAMP}_${CHANNEL}"
echo -e "${CYAN}备份目录: ${BACKUP_DIR}${NC}"
read -p "确认执行降级? [y/N]: " CONFIRM_DOWNGRADE </dev/tty
if [[ ! "$CONFIRM_DOWNGRADE" =~ ^[Yy]$ ]]; then
print_info "已取消降级操作"
exit 0
fi
print_step "执行降级前备份..."
mkdir -p "$BACKUP_DIR"
if [ -d "$DEPLOY_DIR" ]; then
cp -r "$DEPLOY_DIR" "$BACKUP_DIR/frpc-console"
print_success "备份完成: $BACKUP_DIR"
else
print_warn "部署目录不存在,跳过备份"
fi
fi
else
print_info "首次部署,跳过版本比对"
fi
# ----- 安装必要工具 -----
if [ "$NEED_INSTALL_GIT" = true ] || [ "$NEED_INSTALL_CURL" = true ] || [ "$NEED_INSTALL_WGET" = true ]; then
print_step "安装必要工具..."
local pkgs=""
[ "$NEED_INSTALL_GIT" = true ] && pkgs="${pkgs} git"
[ "$NEED_INSTALL_CURL" = true ] && pkgs="${pkgs} curl"
[ "$NEED_INSTALL_WGET" = true ] && pkgs="${pkgs} wget"
case $OS in
opensuse*|suse*|opensuse-tumbleweed|opensuse-slowroll|opensuse-leap)
if [ "$NEED_INSTALL_GIT" = true ]; then
zypper install -y git-core
pkgs=$(echo "$pkgs" | sed -E 's/(^| )git( |$)/ /g' | sed 's/ */ /g' | sed 's/^ //;s/ $//')
fi
[ -n "$pkgs" ] && zypper install -y $pkgs
;;
ubuntu|debian|linuxmint) apt update -qq && apt install -y $pkgs ;;
centos|rhel|fedora|rocky|almalinux) yum install -y $pkgs ;;
alpine) apk add $pkgs ;;
arch|manjaro|endeavouros) pacman -S --needed --noconfirm $pkgs ;;
*) print_error "无法识别包管理器"; exit 1 ;;
esac
print_success "必要工具安装完成"
fi
# ----- 拉取代码 -----
print_step "拉取代码..."
rm -rf "$WORK_DIR"
git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$WORK_DIR"
cd "$WORK_DIR"
mkdir -p bin static
print_success "代码拉取完成 (分支: ${BRANCH})"
# ============================================================
# 镜像加速配置:拉取代码之后、docker build 之前执行
# ============================================================
if ! configure_docker_mirrors; then
print_error "镜像源配置失败,请检查后重试"
exit 1
fi
# ----- 停止旧容器 -----
if [ "$CONTAINER_EXISTS" = true ]; then
print_step "停止旧容器..."
if [ "$CONTAINER_RUNNING" = true ]; then
docker stop frpc-console 2>/dev/null || true
fi
docker rm frpc-console 2>/dev/null || true
print_success "旧容器已清理"
fi
# ----- 准备部署目录 -----
print_step "准备部署目录..."
mkdir -p "$DEPLOY_DIR"
mkdir -p "$DATA_DIR"
# ----- 检测并迁移旧版本文件(从根目录迁移到 data/)-----
print_step "检测旧版本文件..."
OLD_DB="${DEPLOY_DIR}/frpc-console.db"
OLD_TOML="${DEPLOY_DIR}/frpc.toml"
OLD_LOG="${DEPLOY_DIR}/frpc.log"
OLD_PID="${DEPLOY_DIR}/frpc.pid"
OLD_VERSION="${DEPLOY_DIR}/version.ini"
NEW_DB="${DATA_DIR}/frpc-console.db"
if [ -f "$OLD_DB" ] && [ ! -f "$NEW_DB" ]; then
print_warn "检测到旧版本数据库文件在根目录,将迁移到 data/ 子目录"
BACKUP_OLD_DIR="${DEPLOY_DIR}/.old_backup_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_OLD_DIR"
for file in frpc-console.db frpc.toml frpc.log frpc.pid version.ini; do
if [ -f "${DEPLOY_DIR}/${file}" ]; then
cp "${DEPLOY_DIR}/${file}" "${BACKUP_OLD_DIR}/" 2>/dev/null || true
print_info " 已备份: ${file}"
fi
done
for file in frpc-console.db frpc.toml frpc.log frpc.pid version.ini; do
if [ -f "${DEPLOY_DIR}/${file}" ]; then
cp "${DEPLOY_DIR}/${file}" "${DATA_DIR}/" 2>/dev/null || true
print_info " 已迁移: ${file} → data/"
fi
done
print_success "旧文件已迁移到 data/ 目录"
print_info "旧文件备份位置: ${BACKUP_OLD_DIR}"
print_info "确认服务正常后,可手动删除备份目录"
else
print_info "未检测到需要迁移的旧文件"
fi
# ----- 构建 Docker 镜像 -----
print_step "构建 Docker 镜像..."
docker build --no-cache \
-t "${IMAGE_NAME}:${IMAGE_TAG}" \
.
if [ $? -ne 0 ]; then
print_error "Docker 构建失败"
exit 1
fi
print_success "Docker 镜像构建完成: ${IMAGE_NAME}:${IMAGE_TAG}"
# 验证镜像内容
print_step "验证镜像内容..."
if docker run --rm --entrypoint ls "${IMAGE_NAME}:${IMAGE_TAG}" -l /app/frpc-console 2>/dev/null; then
print_success "二进制文件存在于镜像中"
else
print_error "镜像中缺少 /app/frpc-console"
exit 1
fi
# 标记通道标签
if [ "$CHANNEL" = "lts" ]; then
docker tag "${IMAGE_NAME}:${IMAGE_TAG}" "${IMAGE_NAME}:lts"
print_info "已标记 ${IMAGE_NAME}:lts"
else
docker tag "${IMAGE_NAME}:${IMAGE_TAG}" "${IMAGE_NAME}:preview"
print_info "已标记 ${IMAGE_NAME}:preview"
fi
# ----- 启动新容器 -----
print_step "启动 frpc-console 容器..."
docker run -d \
--name frpc-console \
--restart=always \
--network host \
-v ${DEPLOY_DIR}/data:/app/data \
-e PORT=${PORT} \
-e TZ=Asia/Shanghai \
${IMAGE_NAME}:${IMAGE_TAG}
if docker ps | grep -q frpc-console; then
print_success "容器启动成功!"
else
print_error "容器启动失败,请检查日志: docker logs frpc-console"
exit 1
fi
# ----- 写入版本文件 -----
print_step "写入版本文件..."
echo "$TARGET_VERSION" > "${DEPLOY_DIR}/data/version.ini"
print_success "版本写入: ${TARGET_VERSION}"
# ----- 检查 frpc 子进程(基于 PID 文件)-----
print_step "检查 frpc 状态..."
sleep 3
PID_FILE="${DEPLOY_DIR}/data/frpc.pid"
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE" 2>/dev/null | tr -d ' \n')
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
print_success "frpc 进程运行正常 (PID: $PID)"
else
print_warn "frpc 进程未运行(PID 文件存在但进程已退出,可能配置为空)"
fi
else
print_warn "frpc 进程未运行(PID 文件不存在,请导入 TOML 后重试)"
fi
# ----- 事务后钩子:检测数据库 -----
print_step "验证数据库状态..."
if docker exec frpc-console sqlite3 /app/data/frpc-console.db "SELECT COUNT(*) FROM users;" 2>/dev/null | grep -q "^[0-9]"; then
print_success "数据库可用"
else
print_warn "数据库为空或不可用,请通过 WebUI 注册管理员账户"
fi
# ----- 验证版本 -----
print_step "验证版本..."
sleep 1
CONTAINER_VERSION=$(docker exec frpc-console cat /app/data/version.ini 2>/dev/null | grep -v "^[;#]" | head -1 | tr -d '\n' || echo "unknown")
echo -e " 容器版本: ${CONTAINER_VERSION}"
echo -e " 目标版本: ${TARGET_VERSION}"
if [ "$CONTAINER_VERSION" = "$TARGET_VERSION" ]; then
print_success "版本写入成功"
else
print_warn "版本不匹配,请检查"
fi
# ----- 清理旧镜像 -----
print_step "清理旧镜像..."
CURRENT_IMAGE_ID=$(docker images -q "${IMAGE_NAME}:${IMAGE_TAG}" 2>/dev/null)
if [ -n "$CURRENT_IMAGE_ID" ]; then
ALL_IMAGES=$(docker images "${IMAGE_NAME}:*" --format "{{.ID}} {{.CreatedAt}}" 2>/dev/null | sort -k2 -r | awk '{print $1}')
COUNTER=0
TO_DELETE=""
for IMG_ID in $ALL_IMAGES; do
if [ "$IMG_ID" = "$CURRENT_IMAGE_ID" ]; then
continue
fi
COUNTER=$((COUNTER + 1))
if [ $COUNTER -le $KEEP_IMAGES ]; then
continue
fi
TO_DELETE="$TO_DELETE $IMG_ID"
done
if [ -n "$TO_DELETE" ]; then
docker rmi -f $TO_DELETE 2>/dev/null || true
print_info "已清理旧镜像"
else
print_info "无需清理,镜像数量未超过保留策略"
fi
fi
# ----- 备份目录提示 -----
if [ -n "$BACKUP_DIR" ]; then
echo ""
print_warn "降级前数据已备份到: $BACKUP_DIR"
echo " 如需回退: cp -r $BACKUP_DIR/frpc-console/* $DEPLOY_DIR/"
fi
# ----- 清理临时文件 -----
print_step "清理临时文件..."
rm -rf "$WORK_DIR"
print_success "临时文件已清理"
# ----- 输出 -----
print_title
print_success "frpc-console 部署完成!"
print_title
echo ""
echo -e " ${CYAN}📍 访问地址:${NC} http://$(hostname -I | awk '{print $1}'):${PORT}"
echo -e " ${CYAN}📂 数据目录:${NC} ${DEPLOY_DIR}"
echo -e " ${CYAN}🐳 容器名称:${NC} frpc-console"
echo -e " ${CYAN}🏷️ 版本:${NC} ${TARGET_VERSION}"
echo -e " ${CYAN}🌿 分支:${NC} ${BRANCH}"
echo ""
echo -e " ${CYAN}常用命令:${NC}"
echo " docker logs frpc-console # 查看日志"
echo " docker restart frpc-console # 重启服务"
echo " cat ${DEPLOY_DIR}/data/version.ini # 查看当前版本"
echo ""
echo -e " ${YELLOW}首次访问需要注册管理员账户${NC}"
echo ""
print_title
}
# ---------- 主流程 ----------
main() {
clear 2>/dev/null || true
parse_args "$@"
check_root
print_step "检测环境..."
detect_os
detect_arch
check_tools
check_container
# ---------- 镜像源检测(拉取代码前,只检测不修改) ----------
check_docker_mirrors
select_channel
generate_target_version "$CHANNEL" "$BRANCH"
CURRENT_VERSION=""
if [ "$CONTAINER_EXISTS" = true ]; then
CURRENT_VERSION=$(get_current_version)
fi
print_environment_summary
if [ "$CHECK_ONLY" = true ]; then
print_info "环境检测完成(--check 模式)"
exit 0
fi
if [ "$HAS_DOCKER" = false ]; then
print_error "Docker 未安装"
exit 1
fi
generate_plan
print_deployment_plan
if ! confirm_deploy; then
custom_config
generate_plan
print_deployment_plan
if ! confirm_deploy; then
print_info "已取消部署"
exit 0
fi
fi
if [ "$DRY_RUN" = true ]; then
print_info "演练模式(--dry-run"
exit 0
fi
do_deploy
}
main "$@"