贴一个自用的备份数据库并发送到QQ邮箱的脚本

2026年06月13日 126点热度 0人点赞 0条评论

限制:

QQ邮箱最大附件为50M,这里附件分卷大小设置为36M,编码后会增加30%,接近50M上限,并分为多个邮件发送。

 

数据库账号为:backup,密码保存在:/etc/my.cnf.d/backup.conf ,文件内容为:

[client]
password="123456"

 

邮件mail配置为 /etc/mail.rc,在最后增加内容,请勿直接复制,替换成自己的内容:

#QQ邮箱
set smtp=smtps://smtp.qq.com
set from="xxxxxx@qq.com"
set smtp-auth-user="xxxxxx@qq.com"
set smtp-auth-password="QQ邮箱TOKEN"
set smtp-auth=login
set smtp-use=starttls

 

每8小时定时备份一次 crontab 脚本:

0 */8 * * * /data/backup/backup_database.sh >> /var/log/backup_database.log 2>&1

 

备份脚本:backup_database.h

#!/bin/bash
set -euo pipefail

# ========== 配置区 ==========
BACKUP_DATE=$(date +%Y%m%d_%H%M%S)
WORK_DIR="/tmp/db_backup_$$"                # 临时工作目录
FINAL_ARCHIVE="${BACKUP_DATE}.database.tar.gz"
SPLIT_SIZE="36m"
MAIL_TO="xxxx@qq.com"
MAIL_SUBJECT_PREFIX="网站数据备份-${BACKUP_DATE}"
MYSQL_HOST="127.0.0.1"
MYSQL_USER="backup"
MYSQL_CNF="/etc/my.cnf.d/pwd.conf"
DATABASES=(wordpress weather_ical proftpd phpmyadmin platform_public platform_logs smsfilter)

# 日志配置
LOG_DIR="/var/log/db_backup"
LOG_FILE="${LOG_DIR}/$(date +%Y%m%d).log"

# --- 可调参数 ---
GZIP_LEVEL="6"                  # 压缩级别(1-9,6为平衡)
ATTACH_SCRIPT=1                 # 将脚本自身作为附件(1=启用)
MAIL_SLEEP=3                    # 邮件发送间隔(秒)
MIN_FREE_SPACE_MB=512           # 工作目录最小剩余空间(MB)
STRICT_MODE=1                   # 1=任一数据库导出失败则终止;0=仅警告继续
# ------------------

# mysqldump 额外参数
MYSQLDUMP_OPTS="--single-transaction --max_allowed_packet=1G"
# ===========================

# ---------- 初始化 ----------
if [ ! -d "$LOG_DIR" ]; then
    mkdir -p "$LOG_DIR"
fi
touch "$LOG_FILE"
chmod 640 "$LOG_FILE"

# 清理 30 天前的日志
find "$LOG_DIR" -name "*.log" -type f -mtime +30 -delete 2>/dev/null || true

# 日志函数
log() {
    local level="$1"
    shift
    local message="$*"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}

# 错误处理
error_exit() {
    log "ERROR" "$1"
    log "INFO" "工作目录保留在: $WORK_DIR(请手动清理)"
    exit "${2:-1}"
}

# ---------- 开始执行 ----------
log "INFO" "========== 备份任务开始 =========="
log "INFO" "备份标识: $BACKUP_DATE"
log "INFO" "日志文件: $LOG_FILE"

# 检查依赖命令
for cmd in mysqldump tar split; do
    if ! command -v "$cmd" &>/dev/null; then
        error_exit "未找到命令 $cmd,请先安装。"
    fi
done

# ---------- 邮件命令检测 ----------
MAIL_CMD=""
if command -v mutt &>/dev/null; then
    MAIL_CMD="mutt"
    log "INFO" "使用 mutt 发送邮件"
elif command -v mailx &>/dev/null; then
    MAIL_CMD="mailx"
    log "INFO" "使用 mailx 发送邮件(支持 -a 附件)"
elif command -v mail &>/dev/null && mail -V 2>&1 | grep -qi "mailx"; then
    MAIL_CMD="mail"
    log "INFO" "使用 mail(mailx 兼容)"
else
    error_exit "未找到支持附件的邮件命令(mutt 或 mailx),请安装。"
fi

# 检查 MySQL 配置文件
if [ ! -f "$MYSQL_CNF" ]; then
    error_exit "MySQL配置文件 $MYSQL_CNF 不存在,请创建。"
fi
chmod 600 "$MYSQL_CNF" 2>/dev/null || log "WARN" "无法修改 $MYSQL_CNF 权限,请手动设为600"
PERM=$(stat -c "%a" "$MYSQL_CNF" 2>/dev/null || stat -f "%Lp" "$MYSQL_CNF" 2>/dev/null)
if [ "$PERM" != "600" ]; then
    log "WARN" "配置文件 $MYSQL_CNF 权限为 $PERM,建议手动设为600"
fi

# 创建临时工作目录
cleanup() {
    if [ "${BACKUP_SUCCESS:-0}" -eq 1 ]; then
        log "INFO" "清理临时目录: $WORK_DIR"
        rm -rf "$WORK_DIR"
    else
        log "WARN" "备份未完全成功,保留工作目录: $WORK_DIR"
    fi
}
trap cleanup EXIT

mkdir -p "$WORK_DIR/sql_dumps"
log "INFO" "临时工作目录: $WORK_DIR"

# 磁盘空间检查
check_disk_space() {
    local dir="$1"
    local required_mb="$2"
    local available_mb=$(df -m "$dir" | awk 'NR==2 {print $4}')
    if [ -z "$available_mb" ]; then
        log "WARN" "无法获取磁盘空间信息,跳过检查"
        return
    fi
    if [ "$available_mb" -lt "$required_mb" ]; then
        error_exit "磁盘空间不足:可用 ${available_mb}MB < 要求 ${required_mb}MB"
    fi
    log "INFO" "磁盘空间检查通过:可用 ${available_mb}MB"
}
check_disk_space "$WORK_DIR" "$MIN_FREE_SPACE_MB"

# 准备脚本附件
SCRIPT_ATTACH=""
if [ "$ATTACH_SCRIPT" -eq 1 ] && [ -f "$0" ]; then
    SCRIPT_ATTACH="${WORK_DIR}/backup_script.sh"
    cp "$0" "$SCRIPT_ATTACH"
    chmod 644 "$SCRIPT_ATTACH"
    log "INFO" "已准备脚本附件(完整原始脚本): $SCRIPT_ATTACH"
fi

# ---------- 导出数据库 ----------
log "INFO" "开始导出数据库..."
export_failed=0
failed_dbs=()
for db in "${DATABASES[@]}"; do
    log "INFO" "  正在导出: $db"
    if mysqldump --defaults-extra-file="$MYSQL_CNF" \
                 -h "$MYSQL_HOST" -u "$MYSQL_USER" \
                 $MYSQLDUMP_OPTS \
                 "$db" > "$WORK_DIR/sql_dumps/${db}.sql" 2>>"$LOG_FILE"; then
        log "INFO" "    ✓ $db 导出成功"
    else
        log "ERROR" "    ✗ $db 导出失败"
        export_failed=1
        failed_dbs+=("$db")
    fi
done

if [ $export_failed -ne 0 ]; then
    if [ "$STRICT_MODE" -eq 1 ]; then
        error_exit "数据库导出存在失败(${failed_dbs[*]}),终止备份"
    else
        log "WARN" "部分数据库导出失败(${failed_dbs[*]}),继续后续操作(非严格模式)"
    fi
fi

# 打包 SQL 文件
log "INFO" "打包所有SQL文件(压缩级别 $GZIP_LEVEL)..."
export GZIP="-${GZIP_LEVEL}"
if tar czf "$WORK_DIR/${FINAL_ARCHIVE}" -C "$WORK_DIR/sql_dumps" . 2>>"$LOG_FILE"; then
    log "INFO" "✓ 打包成功: $WORK_DIR/${FINAL_ARCHIVE}"
else
    error_exit "✗ tar打包失败"
fi
unset GZIP

# 分卷压缩
log "INFO" "分卷压缩(每卷 $SPLIT_SIZE)..."
cd "$WORK_DIR"
if split -b "$SPLIT_SIZE" "$FINAL_ARCHIVE" "${BACKUP_DATE}.database.tar.gz.part-" 2>>"$LOG_FILE"; then
    log "INFO" "✓ 分卷压缩完成"
else
    error_exit "✗ 分卷压缩失败"
fi

# ---------- 获取分卷文件列表 ----------
PARTS=()
# 使用通配符匹配所有分卷文件
for file in "${BACKUP_DATE}.database.tar.gz.part-"*; do
    if [ -f "$file" ]; then
        PARTS+=("$file")
    fi
done

if [ ${#PARTS[@]} -eq 0 ]; then
    error_exit "没有生成任何分卷文件"
fi

# 排序确保顺序
IFS=$'\n' PARTS=($(printf '%s\n' "${PARTS[@]}" | sort))
unset IFS

TOTAL_PARTS=${#PARTS[@]}
log "INFO" "共生成 $TOTAL_PARTS 个分卷"

# 生成校验和文件
CHECKSUM_FILE="${WORK_DIR}/checksums.sha256"
for part in "${PARTS[@]}"; do
    sha256sum "$part" >> "$CHECKSUM_FILE"
done
log "INFO" "校验和文件已生成: $CHECKSUM_FILE"

# ---------- 发送邮件 ----------
log "INFO" "开始发送邮件(共 $TOTAL_PARTS 封)..."

MAIL_VERBOSE_OPTS=()
if [[ "$MAIL_CMD" != "mutt" ]]; then
    MAIL_VERBOSE_OPTS+=("-v")
    log "INFO" "已为 $MAIL_CMD 启用 -v 调试输出"
fi

for i in "${!PARTS[@]}"; do
    PART_FILE="${PARTS[$i]}"
    PART_BASENAME=$(basename "$PART_FILE")
    PART_NUM=$((i+1))
    MAIL_SUBJECT="${MAIL_SUBJECT_PREFIX} - 分卷 ${PART_NUM}/${TOTAL_PARTS}"

    log "INFO" "发送分卷 ${PART_NUM}/${TOTAL_PARTS}: $PART_BASENAME"

    # 准备日志附件
    LOG_ATTACH="${WORK_DIR}/execution_log.txt"
    cp "$LOG_FILE" "$LOG_ATTACH" 2>/dev/null || echo "日志文件不存在" > "$LOG_ATTACH"

    # 构建附件参数
    ATTACH_ARGS=(-a "$PART_FILE" -a "$LOG_ATTACH")
    if [ $PART_NUM -eq 1 ]; then
        ATTACH_ARGS+=(-a "$CHECKSUM_FILE")
        if [ -n "$SCRIPT_ATTACH" ] && [ -f "$SCRIPT_ATTACH" ]; then
            ATTACH_ARGS+=(-a "$SCRIPT_ATTACH")
        fi
    fi

    MAIL_BODY=$(cat <<EOF
网站数据库备份分卷 ${PART_NUM}/${TOTAL_PARTS}。

备份标识:${BACKUP_DATE}
分卷大小:每卷 ${SPLIT_SIZE}
当前附件:${PART_BASENAME}
总卷数:${TOTAL_PARTS}

合并方法(将所有分卷下载到同一目录后执行):
  cat ${BACKUP_DATE}.database.tar.gz.part-* > ${BACKUP_DATE}.database.tar.gz
  tar xzvf ${BACKUP_DATE}.database.tar.gz

校验分卷完整性(建议):
  sha256sum -c checksums.sha256

本邮件为第 ${PART_NUM} 个分卷,请确保收到所有 ${TOTAL_PARTS} 个分卷后再进行合并。

每封邮件均附带完整的执行日志(execution_log.txt),第一封额外附有校验和文件(checksums.sha256)${SCRIPT_ATTACH:+和备份脚本(backup_script.sh)}。
其余分卷仅包含数据文件和日志。
EOF
)

    if echo "$MAIL_BODY" | $MAIL_CMD -s "$MAIL_SUBJECT" "${MAIL_VERBOSE_OPTS[@]}" "${ATTACH_ARGS[@]}" -- "$MAIL_TO" >>"$LOG_FILE" 2>&1; then
        log "INFO" "  ✓ 分卷 ${PART_NUM}/${TOTAL_PARTS} 发送成功"
    else
        error_exit "  ✗ 分卷 ${PART_NUM}/${TOTAL_PARTS} 发送失败(详情见日志)"
    fi

    [ $MAIL_SLEEP -gt 0 ] && sleep "$MAIL_SLEEP"
done

BACKUP_SUCCESS=1
log "INFO" "所有 ${TOTAL_PARTS} 个分卷已通过 ${TOTAL_PARTS} 封邮件发送完成"
log "INFO" "========== 备份任务结束 =========="

exit 0

路灯

这个人很懒,什么都没留下

文章评论