Files
docker-backup/bin/backup.sh
Wang Defa fe57dd33f4 feat: 添加备份过程的详细日志
改进内容:
- 在文件夹备份时显示排除规则和文件大小
- 在合并备份时显示每个文件的路径和大小
- 添加 tar 合并命令的错误检查
- 改进日志输出,便于调试和追踪问题

这些日志将帮助用户:
1. 确认排除规则是否正确应用
2. 检查每个备份文件的大小
3. 快速定位备份失败的原因
2025-12-25 15:45:11 +08:00

413 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
###############################################################################
# Docker Backup Script
# 功能:备份指定文件夹和 MySQL 容器数据库
###############################################################################
set -e # 遇到错误立即退出
set -o pipefail # 管道命令任何一个失败都返回失败
# 脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 配置文件路径(系统级配置)
CONFIG_FILE="${CONFIG_FILE:-/etc/docker-backup/config.yml}"
# 临时目录
TEMP_DIR=""
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
###############################################################################
# 日志函数
###############################################################################
log() {
local level=$1
shift
local message="$@"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# 输出到 stderr避免干扰函数返回值
echo -e "[${timestamp}] [${level}] ${message}" >&2
# 如果配置了日志文件,同时写入文件
if [[ -n "${LOG_FILE}" ]] && [[ "${LOGGING_ENABLED}" == "true" ]]; then
echo "[${timestamp}] [${level}] ${message}" >> "${LOG_FILE}"
fi
}
log_info() {
log "INFO" "${GREEN}$@${NC}"
}
log_warn() {
log "WARN" "${YELLOW}$@${NC}"
}
log_error() {
log "ERROR" "${RED}$@${NC}"
}
###############################################################################
# 清理函数
###############################################################################
cleanup() {
if [[ -n "${TEMP_DIR}" ]] && [[ -d "${TEMP_DIR}" ]]; then
log_info "清理临时目录: ${TEMP_DIR}"
rm -rf "${TEMP_DIR}"
fi
}
# 注册清理函数
trap cleanup EXIT INT TERM
###############################################################################
# 检查依赖
###############################################################################
check_dependencies() {
log_info "检查依赖工具..."
local missing_deps=()
# 检查 yq
if ! command -v yq &> /dev/null; then
missing_deps+=("yq")
fi
# 检查 tar
if ! command -v tar &> /dev/null; then
missing_deps+=("tar")
fi
# 检查 gzip
if ! command -v gzip &> /dev/null; then
missing_deps+=("gzip")
fi
# 检查 docker
if ! command -v docker &> /dev/null; then
missing_deps+=("docker")
fi
if [[ ${#missing_deps[@]} -gt 0 ]]; then
log_error "缺少以下依赖工具: ${missing_deps[*]}"
log_error "请运行 install.sh 安装依赖"
exit 1
fi
log_info "依赖检查完成"
}
###############################################################################
# 加载配置
###############################################################################
load_config() {
log_info "加载配置文件: ${CONFIG_FILE}"
if [[ ! -f "${CONFIG_FILE}" ]]; then
log_error "配置文件不存在: ${CONFIG_FILE}"
exit 1
fi
# 读取备份基础配置
OUTPUT_DIR=$(yq eval '.backup.output_dir' "${CONFIG_FILE}")
BACKUP_PREFIX=$(yq eval '.backup.prefix' "${CONFIG_FILE}")
# 读取清理策略
RETENTION_ENABLED=$(yq eval '.backup.retention.enabled' "${CONFIG_FILE}")
KEEP_DAYS=$(yq eval '.backup.retention.keep_days' "${CONFIG_FILE}")
# 读取文件夹备份配置
FOLDERS_ENABLED=$(yq eval '.folders.enabled' "${CONFIG_FILE}")
# 读取 MySQL 配置
MYSQL_ENABLED=$(yq eval '.mysql.enabled' "${CONFIG_FILE}")
MYSQL_CONTAINER=$(yq eval '.mysql.container_name' "${CONFIG_FILE}")
MYSQL_USERNAME=$(yq eval '.mysql.username' "${CONFIG_FILE}")
MYSQL_PASSWORD=$(yq eval '.mysql.password' "${CONFIG_FILE}")
# 读取日志配置
LOGGING_ENABLED=$(yq eval '.logging.enabled' "${CONFIG_FILE}")
LOG_FILE=$(yq eval '.logging.log_file' "${CONFIG_FILE}")
# 创建输出目录
mkdir -p "${OUTPUT_DIR}"
# 创建日志目录
if [[ "${LOGGING_ENABLED}" == "true" ]] && [[ -n "${LOG_FILE}" ]]; then
mkdir -p "$(dirname "${LOG_FILE}")"
fi
log_info "配置加载完成"
}
###############################################################################
# 备份文件夹
###############################################################################
backup_folders() {
if [[ "${FOLDERS_ENABLED}" != "true" ]]; then
log_info "文件夹备份未启用,跳过"
return 0
fi
log_info "开始备份文件夹..."
# 创建临时目录
local temp_backup_dir="${TEMP_DIR}/folders"
mkdir -p "${temp_backup_dir}"
# 获取要备份的文件夹数量
local source_count=$(yq eval '.folders.sources | length' "${CONFIG_FILE}")
if [[ "${source_count}" == "0" ]] || [[ "${source_count}" == "null" ]]; then
log_warn "未配置要备份的文件夹"
return 0
fi
# 构建排除参数
local exclude_args=""
local exclude_count=$(yq eval '.folders.excludes | length' "${CONFIG_FILE}")
if [[ "${exclude_count}" != "0" ]] && [[ "${exclude_count}" != "null" ]]; then
for i in $(seq 0 $((exclude_count - 1))); do
local exclude_pattern=$(yq eval ".folders.excludes[$i]" "${CONFIG_FILE}")
exclude_args+=" --exclude='${exclude_pattern}'"
done
fi
# 备份每个文件夹
local folders_tar="${temp_backup_dir}/folders.tar.gz"
local tar_sources=""
for i in $(seq 0 $((source_count - 1))); do
local source_dir=$(yq eval ".folders.sources[$i]" "${CONFIG_FILE}")
if [[ ! -d "${source_dir}" ]]; then
log_warn "源目录不存在,跳过: ${source_dir}"
continue
fi
tar_sources+=" ${source_dir}"
log_info "添加备份源: ${source_dir}"
done
if [[ -z "${tar_sources}" ]]; then
log_warn "没有有效的备份源目录"
return 0
fi
# 执行打包
log_info "开始打包文件夹..."
log_info "排除规则: ${exclude_args}"
if eval "tar -czf '${folders_tar}' ${exclude_args} ${tar_sources}" 2>&1; then
local tar_size=$(du -h "${folders_tar}" | cut -f1)
log_info "文件夹备份完成: ${folders_tar} (大小: ${tar_size})"
echo "${folders_tar}"
else
log_error "文件夹备份失败"
return 1
fi
}
###############################################################################
# 备份 MySQL 数据库
###############################################################################
backup_mysql() {
if [[ "${MYSQL_ENABLED}" != "true" ]]; then
log_info "MySQL 备份未启用,跳过"
return 0
fi
log_info "开始备份 MySQL 数据库..."
# 检查容器是否存在且运行中
if ! docker ps --format '{{.Names}}' | grep -q "^${MYSQL_CONTAINER}$"; then
log_error "MySQL 容器不存在或未运行: ${MYSQL_CONTAINER}"
return 1
fi
# 创建临时目录
local temp_mysql_dir="${TEMP_DIR}/mysql"
mkdir -p "${temp_mysql_dir}"
# 获取要备份的数据库列表
local databases=$(yq eval '.mysql.databases' "${CONFIG_FILE}")
local mysql_dump="${temp_mysql_dir}/mysql.sql.gz"
if [[ "${databases}" == "all" ]] || [[ "${databases}" == "null" ]]; then
# 备份所有数据库
log_info "备份所有数据库..."
docker exec "${MYSQL_CONTAINER}" mysqldump \
--single-transaction \
--quick \
--skip-lock-tables \
-u"${MYSQL_USERNAME}" \
-p"${MYSQL_PASSWORD}" \
--all-databases \
| gzip > "${mysql_dump}"
else
# 备份指定数据库
local db_count=$(yq eval '.mysql.databases | length' "${CONFIG_FILE}")
local db_list=""
for i in $(seq 0 $((db_count - 1))); do
local db_name=$(yq eval ".mysql.databases[$i]" "${CONFIG_FILE}")
db_list+=" ${db_name}"
log_info "添加数据库: ${db_name}"
done
log_info "备份数据库: ${db_list}"
docker exec "${MYSQL_CONTAINER}" mysqldump \
--single-transaction \
--quick \
--skip-lock-tables \
-u"${MYSQL_USERNAME}" \
-p"${MYSQL_PASSWORD}" \
--databases ${db_list} \
| gzip > "${mysql_dump}"
fi
if [[ -f "${mysql_dump}" ]]; then
log_info "MySQL 备份完成: ${mysql_dump}"
echo "${mysql_dump}"
else
log_error "MySQL 备份失败"
return 1
fi
}
###############################################################################
# 合并备份文件
###############################################################################
merge_backups() {
local folders_tar=$1
local mysql_dump=$2
log_info "开始合并备份文件..."
# 生成时间戳
local timestamp=$(date '+%Y%m%d-%H%M%S')
local final_backup="${OUTPUT_DIR}/${BACKUP_PREFIX}-${timestamp}.tar.gz"
# 要打包的文件列表
local files_to_pack=""
if [[ -n "${folders_tar}" ]] && [[ -f "${folders_tar}" ]]; then
files_to_pack+=" ${folders_tar}"
fi
if [[ -n "${mysql_dump}" ]] && [[ -f "${mysql_dump}" ]]; then
files_to_pack+=" ${mysql_dump}"
fi
if [[ -z "${files_to_pack}" ]]; then
log_error "没有需要打包的文件"
return 1
fi
# 合并打包
# 构建相对于 TEMP_DIR 的路径列表
local files_list=""
if [[ -n "${folders_tar}" ]] && [[ -f "${folders_tar}" ]]; then
files_list+=" ${folders_tar#${TEMP_DIR}/}"
log_info "添加到合并: ${folders_tar#${TEMP_DIR}/} ($(du -h "${folders_tar}" | cut -f1))"
fi
if [[ -n "${mysql_dump}" ]] && [[ -f "${mysql_dump}" ]]; then
files_list+=" ${mysql_dump#${TEMP_DIR}/}"
log_info "添加到合并: ${mysql_dump#${TEMP_DIR}/} ($(du -h "${mysql_dump}" | cut -f1))"
fi
log_info "执行合并打包..."
if ! tar -czf "${final_backup}" -C "${TEMP_DIR}" ${files_list} 2>&1; then
log_error "tar 合并命令执行失败"
return 1
fi
if [[ -f "${final_backup}" ]]; then
local file_size=$(du -h "${final_backup}" | cut -f1)
log_info "备份完成: ${final_backup} (大小: ${file_size})"
echo "${final_backup}"
else
log_error "合并备份失败"
return 1
fi
}
###############################################################################
# 清理旧备份
###############################################################################
cleanup_old_backups() {
if [[ "${RETENTION_ENABLED}" != "true" ]]; then
log_info "自动清理未启用,跳过"
return 0
fi
log_info "开始清理旧备份..."
if [[ "${KEEP_DAYS}" != "null" ]] && [[ "${KEEP_DAYS}" =~ ^[0-9]+$ ]]; then
log_info "删除 ${KEEP_DAYS} 天前的备份文件..."
# 查找并删除旧文件
find "${OUTPUT_DIR}" -name "${BACKUP_PREFIX}-*.tar.gz" -type f -mtime +${KEEP_DAYS} -print | while read old_file; do
log_info "删除旧备份: ${old_file}"
rm -f "${old_file}"
done
fi
log_info "清理完成"
}
###############################################################################
# 主函数
###############################################################################
main() {
log_info "=========================================="
log_info "Docker Backup 开始执行"
log_info "=========================================="
# 检查依赖
check_dependencies
# 加载配置
load_config
# 创建临时目录
TEMP_DIR=$(mktemp -d -t docker-backup.XXXXXX)
log_info "临时目录: ${TEMP_DIR}"
# 备份文件夹
local folders_tar=$(backup_folders)
# 备份 MySQL
local mysql_dump=$(backup_mysql)
# 合并备份
local final_backup=$(merge_backups "${folders_tar}" "${mysql_dump}")
# 清理旧备份
cleanup_old_backups
log_info "=========================================="
log_info "Docker Backup 执行完成"
log_info "备份文件: ${final_backup}"
log_info "=========================================="
}
# 执行主函数
main "$@"