实现功能: - 文件夹和 MySQL 容器数据库备份 - tar.gz 压缩和自动清理旧备份 - systemd 定时任务集成 - 远程一键安装脚本(通过 Gitea 仓库) - 完整的 llmdoc 文档系统 安装方式: bash <(curl -sL https://gitea.bcde.io/wangdefa/docker-backup/raw/branch/main/install.sh) 配置文件位置:/etc/docker-backup/config.yml 命令:docker-backup, docker-backup-cleanup
401 lines
11 KiB
Bash
Executable File
401 lines
11 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')
|
|
|
|
echo -e "[${timestamp}] [${level}] ${message}"
|
|
|
|
# 如果配置了日志文件,同时写入文件
|
|
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 "开始打包文件夹..."
|
|
eval "tar -czf '${folders_tar}' ${exclude_args} ${tar_sources}" 2>&1 | while read line; do
|
|
log_info "$line"
|
|
done
|
|
|
|
if [[ -f "${folders_tar}" ]]; 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 "$@"
|