#!/bin/bash # ============================================================================ # MySQL 8.0.24 备份恢复工具 - 公共函数库 # ============================================================================ # 说明: 此文件包含所有脚本共用的函数 # ============================================================================ # 颜色定义 (用于终端输出) RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # ============================================================================ # 日志函数 # ============================================================================ # 获取当前时间戳 get_timestamp() { date +"%Y-%m-%d %H:%M:%S" } # 打印信息日志 log_info() { local message="$1" echo -e "${GREEN}[INFO]${NC} [$(get_timestamp)] $message" if [[ -n "${LOG_FILE:-}" ]]; then echo "[INFO] [$(get_timestamp)] $message" >> "$LOG_FILE" fi } # 打印警告日志 log_warn() { local message="$1" echo -e "${YELLOW}[WARN]${NC} [$(get_timestamp)] $message" >&2 if [[ -n "${LOG_FILE:-}" ]]; then echo "[WARN] [$(get_timestamp)] $message" >> "$LOG_FILE" fi } # 打印错误日志 log_error() { local message="$1" echo -e "${RED}[ERROR]${NC} [$(get_timestamp)] $message" >&2 if [[ -n "${LOG_FILE:-}" ]]; then echo "[ERROR] [$(get_timestamp)] $message" >> "$LOG_FILE" fi } # 打印调试日志 (仅在 VERBOSE=true 时输出) log_debug() { local message="$1" if [[ "${VERBOSE:-false}" == "true" ]]; then echo -e "${BLUE}[DEBUG]${NC} [$(get_timestamp)] $message" if [[ -n "${LOG_FILE:-}" ]]; then echo "[DEBUG] [$(get_timestamp)] $message" >> "$LOG_FILE" fi fi } # ============================================================================ # 错误处理函数 # ============================================================================ # 错误退出函数 die() { local message="$1" local exit_code="${2:-1}" log_error "$message" log_error "脚本异常退出,退出码: $exit_code" exit "$exit_code" } # 设置错误处理 trap setup_error_trap() { # 捕获错误并输出详细信息 trap 'error_handler $? $LINENO $BASH_LINENO "$BASH_COMMAND" $(printf "::%s" ${FUNCNAME[@]:-})' ERR } # 错误处理器 error_handler() { local exit_code=$1 local line_no=$2 local bash_lineno=$3 local last_command=$4 local func_trace=$5 log_error "============================================================" log_error "脚本执行出错!" log_error "============================================================" log_error "退出码: $exit_code" log_error "错误行号: $line_no" log_error "失败命令: $last_command" log_error "函数调用栈: $func_trace" log_error "============================================================" } # ============================================================================ # 验证函数 # ============================================================================ # 检查是否以 root 用户运行 check_root() { if [[ $EUID -ne 0 ]]; then die "此脚本需要以 root 权限运行,请使用 sudo" fi } # 检查必要的命令是否存在 check_commands() { local commands=("$@") local missing=() for cmd in "${commands[@]}"; do if ! command -v "$cmd" &> /dev/null; then missing+=("$cmd") fi done if [[ ${#missing[@]} -gt 0 ]]; then die "缺少必要的命令: ${missing[*]}" fi } # 检查 MySQL 连接 check_mysql_connection() { log_info "检查 MySQL 连接..." local mysql_cmd="$MYSQL_PATH" local connect_args="-h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER}" if [[ -n "$MYSQL_PASSWORD" ]]; then connect_args="$connect_args -p${MYSQL_PASSWORD}" fi if ! $mysql_cmd $connect_args -e "SELECT 1;" &> /dev/null; then die "无法连接到 MySQL 服务器 (${MYSQL_HOST}:${MYSQL_PORT})" fi log_info "MySQL 连接成功" } # 检查目录是否存在,不存在则创建 ensure_dir() { local dir="$1" if [[ ! -d "$dir" ]]; then log_info "创建目录: $dir" mkdir -p "$dir" || die "无法创建目录: $dir" fi } # 检查磁盘空间 check_disk_space() { local target_dir="$1" local required_mb="${2:-1024}" # 默认需要 1GB # 获取目标目录所在分区的可用空间 (MB) local available_mb=$(df -m "$target_dir" | awk 'NR==2 {print $4}') if [[ $available_mb -lt $required_mb ]]; then die "磁盘空间不足! 需要: ${required_mb}MB, 可用: ${available_mb}MB" fi log_debug "磁盘空间检查通过: 需要 ${required_mb}MB, 可用 ${available_mb}MB" } # ============================================================================ # MySQL 操作函数 # ============================================================================ # 执行 MySQL 命令 execute_mysql() { local sql="$1" local mysql_cmd="$MYSQL_PATH" local connect_args="-h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER}" if [[ -n "$MYSQL_PASSWORD" ]]; then connect_args="$connect_args -p${MYSQL_PASSWORD}" fi $mysql_cmd $connect_args -N -e "$sql" 2>&1 } # 获取当前 binlog 位置 get_binlog_position() { local result=$(execute_mysql "SHOW MASTER STATUS\G") local binlog_file=$(echo "$result" | grep "File:" | awk '{print $2}') local binlog_pos=$(echo "$result" | grep "Position:" | awk '{print $2}') if [[ -z "$binlog_file" || -z "$binlog_pos" ]]; then die "无法获取 binlog 位置,请确保 MySQL 已启用 binlog" fi echo "${binlog_file}:${binlog_pos}" } # 获取所有数据库列表 get_databases() { local exclude_regex="" # 构建排除正则表达式 for db in $EXCLUDE_DATABASES; do if [[ -n "$exclude_regex" ]]; then exclude_regex="${exclude_regex}|" fi exclude_regex="${exclude_regex}^${db}$" done # 获取数据库列表 local databases=$(execute_mysql "SHOW DATABASES;") # 过滤排除的数据库 if [[ -n "$exclude_regex" ]]; then databases=$(echo "$databases" | grep -vE "$exclude_regex") fi echo "$databases" } # ============================================================================ # 文件操作函数 # ============================================================================ # 压缩文件 compress_file() { local input_file="$1" local output_file="$2" log_info "压缩文件: $input_file -> $output_file" case "$COMPRESS_TOOL" in gzip) gzip -c "$input_file" > "$output_file" || die "压缩失败" ;; pigz) pigz -p "$COMPRESS_THREADS" -c "$input_file" > "$output_file" || die "压缩失败" ;; lz4) lz4 -c "$input_file" > "$output_file" || die "压缩失败" ;; *) die "不支持的压缩工具: $COMPRESS_TOOL" ;; esac log_info "压缩完成" } # 解压文件 decompress_file() { local input_file="$1" local output_file="$2" log_info "解压文件: $input_file -> $output_file" case "$input_file" in *.gz) gunzip -c "$input_file" > "$output_file" || die "解压失败" ;; *.lz4) lz4 -d -c "$input_file" > "$output_file" || die "解压失败" ;; *) # 假设是未压缩文件,直接复制 cp "$input_file" "$output_file" || die "复制失败" ;; esac log_info "解压完成" } # 清理过期备份 cleanup_old_backups() { local backup_dir="$1" local retention_days="$2" log_info "清理 $retention_days 天前的备份: $backup_dir" local count=$(find "$backup_dir" -maxdepth 1 -type d -mtime +$retention_days 2>/dev/null | wc -l) if [[ $count -gt 0 ]]; then find "$backup_dir" -maxdepth 1 -type d -mtime +$retention_days -exec rm -rf {} \; log_info "已清理 $count 个过期备份目录" else log_info "没有需要清理的过期备份" fi } # ============================================================================ # 通知函数 # ============================================================================ # 发送通知 send_notification() { local subject="$1" local message="$2" if [[ "${ENABLE_EMAIL_NOTIFICATION:-false}" == "true" && -n "${NOTIFICATION_EMAIL:-}" ]]; then log_info "发送邮件通知到: $NOTIFICATION_EMAIL" echo "$message" | mail -s "$subject" "$NOTIFICATION_EMAIL" || log_warn "邮件发送失败" fi } # ============================================================================ # 锁文件函数 (防止并发执行) # ============================================================================ # 获取锁 acquire_lock() { local lock_file="$1" local lock_name="${2:-backup}" if [[ -f "$lock_file" ]]; then local pid=$(cat "$lock_file") if kill -0 "$pid" 2>/dev/null; then die "${lock_name} 进程已在运行 (PID: $pid)" else log_warn "发现过期的锁文件,正在清理..." rm -f "$lock_file" fi fi echo $$ > "$lock_file" || die "无法创建锁文件" log_debug "已获取锁: $lock_file (PID: $$)" } # 释放锁 release_lock() { local lock_file="$1" if [[ -f "$lock_file" ]]; then rm -f "$lock_file" log_debug "已释放锁: $lock_file" fi } # ============================================================================ # 时间计算函数 # ============================================================================ # 计算执行时间 calculate_duration() { local start_time=$1 local end_time=$2 local duration=$((end_time - start_time)) local hours=$((duration / 3600)) local minutes=$(((duration % 3600) / 60)) local seconds=$((duration % 60)) printf "%02d:%02d:%02d" $hours $minutes $seconds } # 获取文件大小 (人类可读格式) get_file_size() { local file="$1" if [[ -f "$file" ]]; then ls -lh "$file" | awk '{print $5}' elif [[ -d "$file" ]]; then du -sh "$file" | awk '{print $1}' else echo "N/A" fi }