From 48086d0d1456484cac04a7e5fa7ba4ef9ccac49a Mon Sep 17 00:00:00 2001 From: k Date: Mon, 22 Dec 2025 15:35:50 +0000 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20lib/common.sh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common.sh | 367 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 lib/common.sh diff --git a/lib/common.sh b/lib/common.sh new file mode 100644 index 0000000..e33613d --- /dev/null +++ b/lib/common.sh @@ -0,0 +1,367 @@ +#!/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 +}