改进内容: - 在文件夹备份时显示排除规则和文件大小 - 在合并备份时显示每个文件的路径和大小 - 添加 tar 合并命令的错误检查 - 改进日志输出,便于调试和追踪问题 这些日志将帮助用户: 1. 确认排除规则是否正确应用 2. 检查每个备份文件的大小 3. 快速定位备份失败的原因
413 lines
12 KiB
Bash
Executable File
413 lines
12 KiB
Bash
Executable File
#!/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 "$@"
|