限制:
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
:bi...
文章评论