#!/usr/bin/env bash ################################################################################ # H5 应用更新客户端脚本 # # 功能:从版本服务器下载并更新 H5 应用 # 版本:1.0.0 # 作者:自动生成 ################################################################################ set -euo pipefail # 严格模式:遇到错误立即退出 # ============================================================================ # 颜色定义 # ============================================================================ readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' readonly BLUE='\033[0;34m' readonly NC='\033[0m' # No Color # ============================================================================ # 全局变量 # ============================================================================ readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly LOCK_FILE="/tmp/update-client-${USER}.lock" readonly SERVER="dros.overs.games" readonly DEFAULT_USER="deploy" readonly DEFAULT_DIR="./h5" readonly LOG_DIR="./logs" # 用户输入变量 USERNAME="" PASSWORD="" DOWNLOAD_DIR="" VERSION="" SILENT_MODE=false SKIP_CONFIRM=false INCLUDE_STATIC=false # 是否包含 static 文件夹 # 日志文件 LOG_FILE="" # ============================================================================ # 工具函数 # ============================================================================ # 打印信息 log_info() { echo -e "${GREEN}[INFO]${NC} $*" | tee -a "$LOG_FILE" } # 打印警告 log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" | tee -a "$LOG_FILE" } # 打印错误 log_error() { echo -e "${RED}[ERROR]${NC} $*" | tee -a "$LOG_FILE" >&2 } # 打印成功 log_success() { echo -e "${GREEN}[✓]${NC} $*" | tee -a "$LOG_FILE" } # 清理函数 cleanup() { if [ -f "$LOCK_FILE" ]; then rm -f "$LOCK_FILE" fi } # 设置清理陷阱 trap cleanup EXIT INT TERM # ============================================================================ # 帮助信息 # ============================================================================ show_help() { cat << EOF H5 应用更新客户端 用法: $0 [选项] 选项: -u, --user USERNAME 指定用户名(默认: $DEFAULT_USER) -p, --password PASS 指定密码 -d, --dir DIRECTORY 指定下载目录(默认: $DEFAULT_DIR) -v, --version VERSION 指定版本号(默认: latest) -i, --include-static 包含 static 文件夹更新(默认: 不包含) -y, --yes 跳过确认提示 -s, --silent 静默模式(最小输出) -h, --help 显示此帮助信息 示例: # 交互模式 $0 # 非交互模式(不包含 static) $0 -u deploy -p mypassword -d ./h5 -v latest -y # 包含 static 文件夹 $0 -u deploy -p mypassword -d ./h5 -v latest -y --include-static # 静默模式 $0 -u deploy -p mypassword -v v1.1.5-20251129-062104 -y -s EOF } # ============================================================================ # 检查依赖工具 # ============================================================================ check_dependencies() { log_info "检查依赖工具..." local missing_tools=() for tool in curl wget tar; do if ! command -v "$tool" &> /dev/null; then missing_tools+=("$tool") fi done if [ ${#missing_tools[@]} -gt 0 ]; then log_error "缺少必需工具: ${missing_tools[*]}" log_error "请安装缺少的工具后重试" exit 1 fi log_success "依赖工具检查通过" } # ============================================================================ # 检查并发执行 # ============================================================================ check_lock() { if [ -f "$LOCK_FILE" ]; then log_error "更新脚本已在运行中(锁文件: $LOCK_FILE)" log_error "如确认没有其他实例运行,请删除锁文件后重试" exit 1 fi touch "$LOCK_FILE" log_info "已创建锁文件" } # ============================================================================ # 初始化日志 # ============================================================================ init_log() { mkdir -p "$LOG_DIR" LOG_FILE="$LOG_DIR/update_$(date +%Y%m%d_%H%M%S).log" log_info "日志文件: $LOG_FILE" } # ============================================================================ # 验证用户名格式 # ============================================================================ validate_username() { local username=$1 if [[ ! $username =~ ^[a-zA-Z0-9_]+$ ]]; then log_error "用户名格式错误(仅允许字母、数字和下划线)" return 1 fi return 0 } # ============================================================================ # 验证密码 # ============================================================================ validate_password() { local password=$1 if [ -z "$password" ]; then log_error "密码不能为空" return 1 fi if [ ${#password} -lt 6 ]; then log_error "密码长度至少6位" return 1 fi return 0 } # ============================================================================ # 获取用户输入 # ============================================================================ get_user_input() { # 用户名 if [ -z "$USERNAME" ]; then read -p "请输入用户名 [默认: $DEFAULT_USER]: " USERNAME USERNAME=${USERNAME:-$DEFAULT_USER} fi if ! validate_username "$USERNAME"; then exit 1 fi log_info "用户名: $USERNAME" # 密码 if [ -z "$PASSWORD" ]; then while true; do read -s -p "请输入密码: " PASSWORD echo if validate_password "$PASSWORD"; then break fi done fi log_info "密码已设置(已脱敏)" # 下载目录 if [ -z "$DOWNLOAD_DIR" ]; then read -p "请输入下载目录 [默认: $DEFAULT_DIR]: " DOWNLOAD_DIR DOWNLOAD_DIR=${DOWNLOAD_DIR:-$DEFAULT_DIR} fi log_info "下载目录: $DOWNLOAD_DIR" # 询问是否更新 static 文件夹(仅在交互模式且未通过参数指定时询问) if [ "$INCLUDE_STATIC" = "false" ]; then read -p "是否需要更新 static 文件夹? [y/N]: " include_static if [[ $include_static =~ ^[Yy]$ ]]; then INCLUDE_STATIC=true log_info "static 文件夹: 将包含在更新中" else INCLUDE_STATIC=false log_info "static 文件夹: 将跳过更新" fi fi } # ============================================================================ # 验证路径权限 # ============================================================================ validate_path() { local dir=$1 # 检查路径是否存在 if [ ! -d "$dir" ]; then log_warn "目录不存在: $dir" if [ "$SKIP_CONFIRM" = false ]; then read -p "是否创建该目录? [y/N]: " confirm if [[ ! $confirm =~ ^[Yy]$ ]]; then log_error "用户取消操作" exit 1 fi fi mkdir -p "$dir" log_success "已创建目录: $dir" fi # 检查写入权限 if [ ! -w "$dir" ]; then log_error "无写入权限: $dir" exit 1 fi log_success "路径验证通过" } # ============================================================================ # 测试服务器连接 # ============================================================================ test_server_connection() { log_info "测试服务器连接: https://$SERVER" if ! curl -k -s --max-time 10 --user "$USERNAME:$PASSWORD" \ "https://$SERVER/releases/" > /dev/null 2>&1; then log_error "无法连接到服务器" log_error "请检查网络连接、用户名和密码" exit 1 fi log_success "服务器连接成功" } # ============================================================================ # 获取版本列表 # ============================================================================ get_version_list() { log_info "正在获取版本列表..." local versions versions=$(curl -k -s --user "$USERNAME:$PASSWORD" \ "https://$SERVER/releases/" | \ grep -oP 'v\d+\.\d+\.\d+-\d{8}-\d{6}' | \ sort -Vr | \ head -10) if [ -z "$versions" ]; then log_error "无法获取版本列表" exit 1 fi echo "$versions" } # ============================================================================ # 显示版本列表并选择 # ============================================================================ select_version() { if [ -n "$VERSION" ]; then log_info "使用指定版本: $VERSION" return fi log_info "获取可用版本..." local versions versions=$(get_version_list) echo echo "可用版本列表(最新10个):" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " 0. latest (最新版本)" local i=1 while IFS= read -r version; do echo " $i. $version" ((i++)) done <<< "$versions" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo # 用户选择 while true; do read -p "请选择版本 [0-$((i-1)),默认: 0]: " choice choice=${choice:-0} if [[ $choice =~ ^[0-9]+$ ]]; then if [ "$choice" -eq 0 ]; then VERSION="latest" break elif [ "$choice" -lt "$i" ]; then VERSION=$(echo "$versions" | sed -n "${choice}p") break fi fi log_error "无效选择,请重新输入" done log_info "已选择版本: $VERSION" } # ============================================================================ # 验证版本是否存在 # ============================================================================ verify_version_exists() { local version=$1 log_info "验证版本: $version" if ! curl -k -s --fail --head --user "$USERNAME:$PASSWORD" \ "https://$SERVER/releases/$version/" > /dev/null 2>&1; then log_error "版本不存在: $version" exit 1 fi log_success "版本验证通过" } # ============================================================================ # 备份当前版本 # ============================================================================ backup_current_version() { local dir=$1 if [ ! -d "$dir" ] || [ -z "$(ls -A "$dir" 2>/dev/null)" ]; then log_info "无需备份(目录为空或不存在)" return fi local backup_name="${dir}_backup_$(date +%Y%m%d_%H%M%S)" log_info "备份当前版本到: $backup_name" if ! mv "$dir" "$backup_name"; then log_error "备份失败" exit 1 fi log_success "备份完成: $backup_name" } # ============================================================================ # 下载并更新 # ============================================================================ download_and_update() { local version=$1 local target_dir=$2 log_info "开始下载版本: $version" log_info "目标目录: $target_dir" mkdir -p "$target_dir" # 使用 wget 下载(显示进度) local wget_opts=( "--user=$USERNAME" "--password=$PASSWORD" "--no-check-certificate" "--recursive" "--no-parent" "--no-host-directories" "--cut-dirs=2" "--level=inf" "-N" "-P" "$target_dir" ) # 如果不包含 static,添加排除参数 if [ "$INCLUDE_STATIC" = "false" ]; then wget_opts+=("-X" "static") wget_opts+=("--reject-regex" ".*/static/.*") log_info "static 文件夹: 跳过下载(使用排除参数)" else log_info "static 文件夹: 包含在更新中" fi if [ "$SILENT_MODE" = true ]; then wget_opts+=("-q") fi if ! wget "${wget_opts[@]}" "https://$SERVER/releases/$version/"; then log_error "下载失败" exit 1 fi log_success "下载完成" } # ============================================================================ # 验证文件完整性(如果存在校验文件) # ============================================================================ verify_integrity() { local dir=$1 # 检查是否存在 MD5 校验文件 if [ -f "$dir/checksum.md5" ]; then log_info "验证文件完整性(MD5)..." cd "$dir" || exit 1 if md5sum -c checksum.md5 > /dev/null 2>&1; then log_success "文件完整性验证通过" else log_error "文件完整性验证失败" exit 1 fi cd - > /dev/null || exit 1 elif [ -f "$dir/checksum.sha256" ]; then log_info "验证文件完整性(SHA256)..." cd "$dir" || exit 1 if sha256sum -c checksum.sha256 > /dev/null 2>&1; then log_success "文件完整性验证通过" else log_error "文件完整性验证失败" exit 1 fi cd - > /dev/null || exit 1 else log_warn "未找到校验文件,跳过完整性验证" fi } # ============================================================================ # 显示更新摘要 # ============================================================================ show_update_summary() { local static_status if [ "$INCLUDE_STATIC" = "true" ]; then static_status="包含" else static_status="跳过" fi echo echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " 更新摘要" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " 服务器: $SERVER" echo " 用户名: $USERNAME" echo " 目标目录: $DOWNLOAD_DIR" echo " 版本: $VERSION" echo " static 文件夹: $static_status" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo } # ============================================================================ # 确认更新 # ============================================================================ confirm_update() { if [ "$SKIP_CONFIRM" = true ]; then return fi read -p "确认执行更新? [y/N]: " confirm if [[ ! $confirm =~ ^[Yy]$ ]]; then log_info "用户取消更新" exit 0 fi } # ============================================================================ # 解析命令行参数 # ============================================================================ parse_args() { while [[ $# -gt 0 ]]; do case $1 in -u|--user) USERNAME="$2" shift 2 ;; -p|--password) PASSWORD="$2" shift 2 ;; -d|--dir) DOWNLOAD_DIR="$2" shift 2 ;; -v|--version) VERSION="$2" shift 2 ;; -i|--include-static) INCLUDE_STATIC=true shift ;; -y|--yes) SKIP_CONFIRM=true shift ;; -s|--silent) SILENT_MODE=true shift ;; -h|--help) show_help exit 0 ;; *) log_error "未知选项: $1" show_help exit 1 ;; esac done } # ============================================================================ # 主函数 # ============================================================================ main() { # 解析命令行参数 parse_args "$@" # 初始化日志 init_log # 检查依赖 check_dependencies # 检查并发 check_lock # 获取用户输入 get_user_input # 验证路径 validate_path "$DOWNLOAD_DIR" # 测试服务器连接 test_server_connection # 选择版本 select_version # 验证版本存在 verify_version_exists "$VERSION" # 显示摘要 show_update_summary # 确认更新 confirm_update log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" log_info "开始更新流程" log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" # 备份当前版本 backup_current_version "$DOWNLOAD_DIR" # 下载并更新 download_and_update "$VERSION" "$DOWNLOAD_DIR" # 验证完整性 verify_integrity "$DOWNLOAD_DIR" log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" log_success "更新完成!" log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" log_info "目标目录: $DOWNLOAD_DIR" log_info "版本: $VERSION" log_info "日志文件: $LOG_FILE" exit 0 } # 运行主函数 main "$@"