Files
docker-backup/bin/backup.sh
Wang Defa 46a0ade8ba feat: 实现日志级别功能并修复日志输出问题
- 实现 logging.level 配置项支持(DEBUG/INFO/WARN/ERROR)
- 修复日志文件中 ANSI 颜色代码显示问题
- 修复 install.sh 中 show_info() 函数颜色显示
- 日志输出现在会根据配置的级别进行过滤
2025-12-25 16:34:30 +08:00

449 lines
13 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
# 日志级别映射
declare -A LOG_LEVEL_MAP=(
["DEBUG"]=0
["INFO"]=1
["WARN"]=2
["ERROR"]=3
)
# 当前日志级别(默认 INFO
CURRENT_LOG_LEVEL=1
###############################################################################
# 日志函数
###############################################################################
log() {
local level=$1
shift
local message="$@"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# 获取当前日志级别的数值
local level_value=${LOG_LEVEL_MAP[$level]:-1}
# 如果日志级别低于配置的级别,则不输出
if [[ $level_value -lt $CURRENT_LOG_LEVEL ]]; then
return 0
fi
# 输出到 stderr避免干扰函数返回值
echo -e "[${timestamp}] [${level}] ${message}" >&2
# 如果配置了日志文件,同时写入文件
if [[ -n "${LOG_FILE}" ]] && [[ "${LOGGING_ENABLED}" == "true" ]]; then
# 移除 ANSI 颜色代码后写入文件
local clean_message=$(echo -e "${message}" | sed 's/\x1b\[[0-9;]*m//g')
echo "[${timestamp}] [${level}] ${clean_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}")
LOG_LEVEL=$(yq eval '.logging.level' "${CONFIG_FILE}")
# 设置日志级别
if [[ -n "${LOG_LEVEL}" ]] && [[ "${LOG_LEVEL}" != "null" ]]; then
# 转换为大写
LOG_LEVEL=$(echo "${LOG_LEVEL}" | tr '[:lower:]' '[:upper:]')
# 设置当前日志级别
CURRENT_LOG_LEVEL=${LOG_LEVEL_MAP[$LOG_LEVEL]:-1}
log_info "日志级别设置为: ${LOG_LEVEL}"
else
log_info "使用默认日志级别: INFO"
fi
# 创建输出目录
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}"
# 将 tar 的 stdout 和 stderr 都重定向到 stderr避免干扰函数返回值
eval "tar -czf '${folders_tar}' ${exclude_args} ${tar_sources}" >&2 2>&1
if [[ -f "${folders_tar}" ]]; 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 "$@"