Files
docker-backup/bin/backup.sh
Wang Defa 1ea8d681ee fix: 修复日志输出干扰函数返回值的问题
问题描述:
- backup_folders() 和 backup_mysql() 函数使用 echo 返回文件路径
- 但 log_info 也输出到 stdout,导致主函数捕获了所有日志而非路径
- 最终 merge_backups 收到空参数,显示"没有需要打包的文件"

修复内容:
- 将所有日志输出重定向到 stderr (>&2)
- 简化 tar 命令的输出处理逻辑
- 确保函数返回值只包含文件路径

影响文件:
- bin/backup.sh - log() 函数添加 >&2 重定向
- bin/cleanup.sh - 所有 log 函数添加 >&2 重定向
2025-12-25 15:40:36 +08:00

398 lines
11 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 "开始打包文件夹..."
if eval "tar -czf '${folders_tar}' ${exclude_args} ${tar_sources}" 2>&1; then
log_info "文件夹备份完成: ${folders_tar}"
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
# 合并打包
tar -czf "${final_backup}" -C "${TEMP_DIR}" \
$(basename "${folders_tar}" 2>/dev/null || true) \
$(basename "${mysql_dump}" 2>/dev/null || true)
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 "$@"