#!/bin/bash # 固定资产系统备份脚本 set -euo pipefail PROJECT_DIR="${PROJECT_DIR:-/opt/equipment-management}" PROJECT_NAME="${PROJECT_NAME:-equipment-management}" BACKUP_ROOT="${BACKUP_ROOT:-/opt/backups/equipment-management}" KEEP_BACKUPS="${KEEP_BACKUPS:-3}" DB_CONTAINER="${DB_CONTAINER:-equipment-mysql}" BACKEND_CONTAINER="${BACKEND_CONTAINER:-equipment-backend}" DB_NAME="${DB_NAME:-equipment_management}" DB_USER="${DB_USER:-root}" DB_PASSWORD="${DB_PASSWORD:-${MYSQL_ROOT_PASSWORD:-}}" UPLOADS_PATH="${UPLOADS_PATH:-/app/uploads}" TIMESTAMP="$(date +%Y%m%d_%H%M%S)" BACKUP_DIR="${BACKUP_ROOT}/${TIMESTAMP}" cleanup_on_error() { echo "备份失败,清理不完整目录: ${BACKUP_DIR}" rm -rf "${BACKUP_DIR}" } prune_old_backups() { if ! [[ "${KEEP_BACKUPS}" =~ ^[0-9]+$ ]]; then echo "KEEP_BACKUPS 不是有效数字: ${KEEP_BACKUPS}" exit 1 fi mapfile -t existing_backups < <( find "${BACKUP_ROOT}" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; \ | awk '/^[0-9]{8}_[0-9]{6}$/' \ | sort \ | while IFS= read -r dir_name; do printf '%s/%s\n' "${BACKUP_ROOT}" "${dir_name}" done ) local total="${#existing_backups[@]}" if [ "${total}" -le "${KEEP_BACKUPS}" ]; then echo ">>> 当前标准备份数量 ${total},无需清理" return fi local delete_count=$((total - KEEP_BACKUPS)) echo ">>> 仅保留最近 ${KEEP_BACKUPS} 份备份,清理 ${delete_count} 份旧备份" for backup_dir in "${existing_backups[@]:0:${delete_count}}"; do echo " 删除: ${backup_dir}" rm -rf "${backup_dir}" done } trap cleanup_on_error ERR if ! docker inspect "${DB_CONTAINER}" >/dev/null 2>&1; then echo "数据库容器不存在: ${DB_CONTAINER}" exit 1 fi if ! docker inspect "${BACKEND_CONTAINER}" >/dev/null 2>&1; then echo "后端容器不存在: ${BACKEND_CONTAINER}" exit 1 fi if [ -z "${DB_PASSWORD}" ]; then echo ">>> 未从 DB_PASSWORD / MYSQL_ROOT_PASSWORD 读取到数据库密码,尝试读取 ${DB_CONTAINER} 容器环境" if ! container_env="$(docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "${DB_CONTAINER}")"; then echo "无法读取数据库容器环境: ${DB_CONTAINER}" >&2 exit 1 fi while IFS= read -r env_line; do case "${env_line}" in MYSQL_ROOT_PASSWORD=*) DB_PASSWORD="${env_line#MYSQL_ROOT_PASSWORD=}" break ;; esac done <<< "${container_env}" unset container_env env_line fi if [ -z "${DB_PASSWORD}" ]; then echo "数据库密码缺失:请设置 DB_PASSWORD 或 MYSQL_ROOT_PASSWORD;容器 ${DB_CONTAINER} 中也未找到 MYSQL_ROOT_PASSWORD。" >&2 exit 1 fi mkdir -p "${BACKUP_DIR}/db" "${BACKUP_DIR}/uploads" "${BACKUP_DIR}/config" echo "备份目录: ${BACKUP_DIR}" echo "保留策略: 最近 ${KEEP_BACKUPS} 份" echo ">>> 备份数据库 ${DB_NAME}" docker exec -e "MYSQL_PWD=${DB_PASSWORD}" "${DB_CONTAINER}" \ mysqldump -u"${DB_USER}" --single-transaction --routines --triggers "${DB_NAME}" \ | gzip > "${BACKUP_DIR}/db/${DB_NAME}.sql.gz" echo ">>> 备份上传目录 ${UPLOADS_PATH}" docker exec "${BACKEND_CONTAINER}" \ sh -lc "if [ -d '${UPLOADS_PATH}' ]; then tar -C '${UPLOADS_PATH}' -czf - .; else printf '' | gzip; fi" \ > "${BACKUP_DIR}/uploads/uploads.tar.gz" echo ">>> 备份部署配置" cp "${PROJECT_DIR}/docker-compose.yml" "${BACKUP_DIR}/config/docker-compose.yml" cp "${PROJECT_DIR}/deploy.sh" "${BACKUP_DIR}/config/deploy.sh" cp "${PROJECT_DIR}/Dockerfile" "${BACKUP_DIR}/config/Dockerfile" if [ -d "${PROJECT_DIR}/frontend/public/branding" ]; then cp -R "${PROJECT_DIR}/frontend/public/branding" "${BACKUP_DIR}/config/branding" fi cat > "${BACKUP_DIR}/backup-info.txt" <>> 生成校验信息" ( cd "${BACKUP_DIR}" if [ -d config/branding ]; then find config/branding -type f -print0 | xargs -0 sha256sum > branding-sha256.tmp else : > branding-sha256.tmp fi cat <(sha256sum db/${DB_NAME}.sql.gz uploads/uploads.tar.gz config/docker-compose.yml config/deploy.sh config/Dockerfile) branding-sha256.tmp > SHA256SUMS rm -f branding-sha256.tmp ) prune_old_backups trap - ERR echo "备份完成: ${BACKUP_DIR}"