#!/bin/bash
# ==============================================
# MongoDB 安全数据替换脚本 (执行顺序:备份→校验→确认→清空→还原指定数据→失败回滚到备份)
# 依赖命令: mongoexport, mongorestore, mongosh/mongo
# ==============================================
# Configuration parameters (配置参数)
DEFAULT_HOST="localhost"
DEFAULT_PORT="27017"
DEFAULT_AUTH_SOURCE="admin"
BACKUP_DIR="./mongo_backups"
LOG_FILE="./mongo_restore_$(date +%Y%m%d_%H%M%S).log"
# Color definitions (颜色定义)
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color (无颜色)
# Global variable to store the backup file path (全局变量,用于存储备份文件路径)
BACKUP_FILE=""
# Log messages (日志消息函数)
log() {
local level=$1
local message=$2
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[${timestamp}] [${level}] ${message}" >> "$LOG_FILE"
case "$level" in
"ERROR") echo -e "${RED}[ERROR]${NC} ${message}" >&2 ;;
"WARN") echo -e "${YELLOW}[WARN]${NC} ${message}" ;;
"SUCCESS") echo -e "${GREEN}[SUCCESS]${NC} ${message}" ;;
*) echo "[INFO] ${message}" ;;
esac
}
# Check for required commands (检查所需命令)
check_required_commands() {
local missing=()
# Check for mongosh or mongo (检查 mongosh 或 mongo)
if ! command -v mongosh &> /dev/null && ! command -v mongo &> /dev/null; then
missing+=("mongosh/mongo")
fi
# Check for other required commands (检查其他所需命令)
for cmd in mongoexport mongorestore mongoimport; do
if ! command -v "$cmd" &> /dev/null; then
missing+=("$cmd")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
log "ERROR" "缺少必要的命令: ${missing[*]}"
log "ERROR" "请确保以下命令已安装并配置在您的系统 PATH 中:"
log "ERROR" "1. mongoexport"
log "ERROR" "2. mongorestore"
log "ERROR" "3. mongoimport"
log "ERROR" "4. mongosh 或 mongo"
exit 1
fi
}
# Find MongoDB client command (mongosh or mongo) (查找 MongoDB 客户端命令)
find_mongo_client() {
if command -v mongosh &> /dev/null; then
MONGO_CLIENT_CMD="mongosh"
elif command -v mongo &> /dev/null; then
MONGO_CLIENT_CMD="mongo"
else
log "ERROR" "严重错误: 未找到 MongoDB Shell 客户端"
exit 1
fi
log "INFO" "正在使用 MongoDB 客户端: ${MONGO_CLIENT_CMD}"
}
# Validate file existence and readability (验证文件存在性和可读性)
validate_file() {
local file=$1
local description=$2
if [ ! -e "$file" ]; then
log "ERROR" "${description} 文件不存在: $file"
return 1 # Indicate failure (指示失败)
fi
if [ ! -f "$file" ]; then
log "ERROR" "${description} 路径不是一个文件: $file"
return 1 # Indicate failure (指示失败)
fi
if [ ! -r "$file" ]; then
log "ERROR" "无法读取 ${description} 文件: $file"
return 1 # Indicate failure (指示失败)
fi
return 0 # Indicate success (指示成功)
}
# Display help information (显示帮助信息)
show_help() {
echo -e "${YELLOW}MongoDB 安全数据替换脚本 - 执行顺序: 备份 → 校验 → 确认 → 清空 → 还原指定数据 → 失败回滚到备份${NC}"
echo ""
echo "用法:"
echo " $0 -d 数据库名 -c 集合名 [-f 导入文件] [选项]"
echo ""
echo "必填参数:"
echo " -d, --database 数据库名称"
echo " -c, --collection 集合名称"
echo ""
echo "可选参数 (如果未提供 -f,则必须使用 --clear-only 或 --no-clear):"
echo " -f, --file 要导入的数据文件路径 (必须是 JSON/BSON 格式)"
echo ""
echo "选项:"
echo " -h, --host MongoDB 主机 (默认: ${DEFAULT_HOST})"
echo " -p, --port MongoDB 端口 (默认: ${DEFAULT_PORT})"
echo " -u, --username 认证用户名"
echo " -w, --password 认证密码"
echo " -a, --auth-source 认证数据库 (默认: ${DEFAULT_AUTH_SOURCE})"
echo " -y, --assume-yes 跳过确认并直接执行 (危险操作!)"
echo " --clear-only 仅清空集合并备份,不导入新数据 (此模式下无需 -f)"
echo " --no-clear 跳过清空集合步骤 (导入时保留现有数据)"
echo " --help 显示此帮助信息"
echo ""
echo -e "${RED}注意: 此脚本已修改。所有操作现在都将强制进行备份。${NC}"
echo -e "${RED}重要: 清空和导入操作只有在备份成功 并且 (如果提供了导入文件) 导入文件有效时才会继续。${NC}"
echo -e "${RED}如果导入失败,将尝试自动回滚到备份数据。${NC}"
echo "脚本需要 'mongoexport', 'mongorestore', 'mongoimport' 和 'mongosh' (或 'mongo') 命令在您的系统 PATH 中。"
exit 0
}
# Initialize environment (初始化环境)
init_env() {
# First, check for required commands (首先,检查所需命令)
check_required_commands
# Then, find the client (然后,查找客户端)
find_mongo_client
# Create backup directory (创建备份目录)
mkdir -p "$BACKUP_DIR" || {
log "ERROR" "创建备份目录失败: $BACKUP_DIR"
exit 1
}
# Initialize log file (初始化日志文件)
echo "=== MongoDB 安全替换脚本日志 ===" > "$LOG_FILE"
echo "执行时间: $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG_FILE"
echo "依赖检查:" >> "$LOG_FILE"
echo " mongosh/mongo: $(command -v ${MONGO_CLIENT_CMD})" >> "$LOG_FILE"
echo " mongoexport: $(command -v mongoexport)" >> "$LOG_FILE"
echo " mongorestore: $(command -v mongorestore)" >> "$LOG_FILE"
echo " mongoimport: $(command -v mongoimport)" >> "$LOG_FILE"
echo "==============================" >> "$LOG_FILE"
}
# Parse command-line arguments (解析命令行参数)
parse_arguments() {
while [[ $# -gt 0 ]]; do
case "$1" in
-d|--database)
DB_NAME="$2"
shift 2
;;
-c|--collection)
COLLECTION_NAME="$2"
shift 2
;;
-f|--file)
IMPORT_FILE="$2"
shift 2
;;
-h|--host)
HOST="$2"
shift 2
;;
-p|--port)
PORT="$2"
if ! [[ "$PORT" =~ ^[0-9]+$ ]]; then
log "ERROR" "端口号必须是数字"
exit 1
fi
shift 2
;;
-u|--username)
USERNAME="$2"
shift 2
;;
-w|--password)
PASSWORD="$2"
shift 2
;;
-a|--auth-source)
AUTH_SOURCE="$2"
shift 2
;;
-y|--assume-yes)
ASSUME_YES=true
shift
;;
--clear-only)
CLEAR_ONLY=true
shift
;;
--no-clear)
NO_CLEAR=true
shift
;;
--help)
show_help
;;
*)
log "ERROR" "未知参数: $1"
show_help
exit 1
;;
esac
done
# Validate required parameters (验证必填参数)
if [ -z "$DB_NAME" ] || [ -z "$COLLECTION_NAME" ]; then
log "ERROR" "缺少必填参数: 数据库名或集合名!"
show_help
exit 1
fi
# Validate --clear-only and --no-clear are mutually exclusive (验证 --clear-only 和 --no-clear 互斥)
if [ "$CLEAR_ONLY" = true ] && [ "$NO_CLEAR" = true ]; then
log "ERROR" "不能同时使用 --clear-only 和 --no-clear 选项。"
show_help
exit 1
fi
# Validate import file (-f) usage logic (验证导入文件 (-f) 的使用逻辑)
if [ -n "$IMPORT_FILE" ]; then
if [ "$CLEAR_ONLY" = true ]; then
log "ERROR" "不能同时指定导入文件 (-f) 和 --clear-only 选项。"
show_help
exit 1
fi
# Validate file extension (验证文件扩展名)
local ext="${IMPORT_FILE##*.}"
if [[ ! "$ext" =~ ^(json|bson)$ ]]; then
log "ERROR" "不支持的导入文件格式: $ext (仅支持 .json 或 .bson)"
exit 1
fi
else # No import file specified (未指定导入文件)
if [ "$CLEAR_ONLY" != true ] && [ "$NO_CLEAR" != true ]; then
log "ERROR" "请提供要导入的文件 (-f),或使用 --clear-only / --no-clear 选项。"
log "ERROR" "注意: 所有操作都将强制进行备份。"
show_help
exit 1
fi
fi
# If --no-clear is specified, an import file must also be specified (如果指定了 --no-clear,则必须同时指定导入文件)
if [ "$NO_CLEAR" = true ] && [ -z "$IMPORT_FILE" ]; then
log "ERROR" "使用 --no-clear 选项时,必须指定导入文件 (-f)。否则,操作无意义。"
show_help
exit 1
fi
}
# Build MongoDB connection string and authentication options (构建 MongoDB 连接字符串和认证选项)
build_connection() {
HOST="${HOST:-$DEFAULT_HOST}"
PORT="${PORT:-$DEFAULT_PORT}"
AUTH_SOURCE="${AUTH_SOURCE:-$DEFAULT_AUTH_SOURCE}"
# Build URI connection string for mongosh/mongo shell (为 mongosh/mongo shell 构建 URI 连接字符串)
MONGO_URI=""
if [ -n "$USERNAME" ] && [ -n "$PASSWORD" ]; then
# URL-encode special characters in password (对密码中的特殊字符进行 URL 编码)
# Check if jq is available for URL encoding, otherwise fall back (检查 jq 是否可用,否则回退)
if command -v jq &>/dev/null; then
ENCODED_PASSWORD=$(printf '%s' "$PASSWORD" | jq -sRr @uri)
else
log "WARN" "未找到 jq 命令。密码可能未进行 URL 编码。请确保您的密码不包含特殊字符,以便直接在 URI 中使用。"
ENCODED_PASSWORD="$PASSWORD"
fi
MONGO_URI="mongodb://${USERNAME}:${ENCODED_PASSWORD}@${HOST}:${PORT}/${DB_NAME}?authSource=${AUTH_SOURCE}"
else
MONGO_URI="mongodb://${HOST}:${PORT}/${DB_NAME}"
fi
# Build traditional command-line arguments for mongoexport/mongorestore (为 mongoexport/mongorestore 构建传统命令行参数)
AUTH_STR=""
if [ -n "$USERNAME" ] && [ -n "$PASSWORD" ]; then
AUTH_STR="--username ${USERNAME} --password ${PASSWORD} --authenticationDatabase ${AUTH_SOURCE}"
fi
FULL_OPTIONS="--host ${HOST} --port ${PORT} ${AUTH_STR}"
}
# Backup existing data (备份现有数据)
# Returns 0 on successful (non-empty) backup, 1 otherwise (成功备份(非空)返回 0,否则返回 1)
backup_data() {
local timestamp=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${COLLECTION_NAME}_${timestamp}.json"
log "INFO" "正在开始备份现有数据 -> ${BACKUP_FILE}"
if ! mongoexport ${FULL_OPTIONS} \
--db "${DB_NAME}" \
--collection="${COLLECTION_NAME}" \
--out="${BACKUP_FILE}" \
--jsonArray >> "$LOG_FILE" 2>&1; then
log "ERROR" "备份失败! 请检查 MongoDB 连接或权限。后续操作将被跳过。"
return 1
fi
# Validate backup file (验证备份文件)
if [ ! -s "$BACKUP_FILE" ]; then
log "WARN" "备份文件为空 (可能是空集合)。这通常没问题,但请注意。"
return 0 # Still consider it a successful backup of an empty state (仍然认为这是空状态的成功备份)
else
BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
log "SUCCESS" "备份成功 (大小: ${BACKUP_SIZE})"
return 0
fi
}
# Confirm operation (delete or clear) (确认操作(删除或清空))
confirm_operation() {
if [ "$ASSUME_YES" = true ]; then
log "WARN" "已启用自动确认,跳过用户确认"
return 0
fi
local BACKUP_STATUS="${GREEN}强制备份已完成${NC}" # Always backed up now (现在总是已备份)
local CLEAR_STATUS="永久删除"
if [ "$NO_CLEAR" = true ]; then
CLEAR_STATUS="${YELLOW}不删除${NC} (保留现有数据)"
elif [ "$CLEAR_ONLY" = true ]; then
CLEAR_STATUS="${YELLOW}仅清空${NC}" # Means deletion (意味着删除)
fi
local IMPORT_STATUS=""
if [ "$CLEAR_ONLY" = true ]; then
IMPORT_STATUS="${YELLOW}不导入${NC}"
elif [ -z "$IMPORT_FILE" ]; then # If no -f parameter, but not --clear-only (如果未提供 -f 参数,但不是 --clear-only)
IMPORT_STATUS="${YELLOW}不导入${NC}"
else
IMPORT_STATUS="从 ${GREEN}${IMPORT_FILE}${NC} 导入"
fi
echo -e "\n${YELLOW}======= 重要警告 =======${NC}"
echo -e "即将执行以下操作:"
echo -e "1. ${BACKUP_STATUS} ${DB_NAME}.${COLLECTION_NAME} 的现有数据"
echo -e "2. ${CLEAR_STATUS} ${DB_NAME}.${COLLECTION_NAME} 中的所有数据"
echo -e "3. ${IMPORT_STATUS} 新数据"
# Specific warnings (特定警告)
if [ "$NO_CLEAR" = true ] && [ -n "$IMPORT_FILE" ]; then
echo -e "${YELLOW}注意: 您已选择不清空集合。新数据将导入并与现有数据合并。${NC}"
fi
echo -e "备份文件将保存到: ${BACKUP_DIR}"
echo -e "${YELLOW}=======================${NC}\n"
read -p "您确定要继续吗? (yes/no): " -r
echo
if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then
log "INFO" "用户取消了操作"
exit 0
fi
}
# Clear target collection (清空目标集合)
clear_collection() {
log "INFO" "正在清空集合..."
if ! "${MONGO_CLIENT_CMD}" "${MONGO_URI}" --quiet --eval "
const result = db.${COLLECTION_NAME}.deleteMany({});
print('Documents deleted: ' + result.deletedCount);
" >> "$LOG_FILE" 2>&1; then
log "ERROR" "清空集合失败!"
return 1 # Indicate failure (指示失败)
fi
log "SUCCESS" "集合清空成功。"
return 0 # Indicate success (指示成功)
}
# Import new data (导入新数据)
import_data() {
local import_file_path=$1
local source_desc=$2 # e.g., "new data" or "backup data" (例如,“新数据”或“备份数据”)
log "INFO" "正在从 ${source_desc} (${import_file_path}) 开始导入数据..."
case "${import_file_path##*.}" in
json)
if ! mongoimport ${FULL_OPTIONS} \
--db "${DB_NAME}" \
--collection="${COLLECTION_NAME}" \
--file="${import_file_path}" \
--jsonArray >> "$LOG_FILE" 2>&1; then
log "ERROR" "${source_desc} JSON 导入失败!"
return 1
fi
;;
bson)
# IMPORTANT: mongorestore --dir expects a directory, not a single file.
# If import_file_path is a single .bson file, mongoimport --type bson is usually preferred.
# This section assumes import_file_path is a directory containing BSON dump.
if [ ! -d "${import_file_path}" ]; then
log "ERROR" "对于 BSON 导入,--file 选项应指定包含 BSON 文件的目录,而不是单个文件。"
log "ERROR" "如果 '${import_file_path}' 是单个 BSON 文件,请考虑使用 'mongoimport --type bson' 命令,或调整输入文件类型。"
return 1
fi
if ! mongorestore ${FULL_OPTIONS} \
--db "${DB_NAME}" \
--collection="${COLLECTION_NAME}" \
--dir="${import_file_path}" \
--drop >> "$LOG_FILE" 2>&1; then
log "ERROR" "${source_desc} BSON 导入失败!"
return 1
fi
;;
*)
log "ERROR" "不支持 ${source_desc} 的格式: ${import_file_path##*.}"
return 1
;;
esac
log "SUCCESS" "数据从 ${source_desc} 导入成功。"
return 0
}
# Restore data from backup (从备份还原数据)
restore_from_backup() {
log "ERROR" "正在尝试从备份还原: ${BACKUP_FILE}"
# Ensure collection is clear before restoring from backup to prevent duplicates if --no-clear was set
# (在从备份还原之前确保集合已清空,以防止在设置 --no-clear 时出现重复数据)
if ! clear_collection; then
log "ERROR" "在从备份还原之前清空集合失败。需要手动干预!"
return 1
fi
if import_data "$BACKUP_FILE" "备份数据"; then
log "SUCCESS" "成功从备份还原数据。"
return 0
else
log "ERROR" "从备份还原数据失败! 绝对需要手动干预。请检查日志以获取详细信息。"
return 1
fi
}
# Verify operation results (验证操作结果)
verify_operation() {
log "INFO" "正在验证操作结果..."
"${MONGO_CLIENT_CMD}" "${MONGO_URI}" --quiet --eval "
print('当前文档数量: ' + db.${COLLECTION_NAME}.countDocuments());
print('示例文档:');
printjson(db.${COLLECTION_NAME}.find().limit(1).toArray()[0] || '空集合');
" | tee -a "$LOG_FILE"
}
# Main flow (主流程)
main() {
# Initialize environment (初始化环境)
init_env
# Parse command-line arguments (解析命令行参数)
parse_arguments "$@"
# Build connection parameters (构建连接参数)
build_connection
# Print operation summary (打印操作摘要)
log "INFO" "操作摘要:"
log "INFO" "数据库: ${DB_NAME}"
log "INFO" "集合: ${COLLECTION_NAME}"
log "INFO" "备份模式: 强制执行备份" # Updated message (更新消息)
if [ "$NO_CLEAR" = true ]; then
log "INFO" "清空模式: 跳过清空 (保留现有数据)"
elif [ "$CLEAR_ONLY" = true ]; then
log "INFO" "清空模式: 仅清空集合 (不导入新数据)"
else
log "INFO" "清空模式: 先清空,然后导入"
fi
if [ -n "$IMPORT_FILE" ]; then
log "INFO" "导入文件: ${IMPORT_FILE}"
else
log "INFO" "导入文件: 未指定 (不执行导入)"
fi
[ -n "$USERNAME" ] && log "INFO" "认证用户: ${USERNAME}"
# --- 步骤 1: 强制备份现有数据 ---
# 如果备份失败,脚本将终止。
if ! backup_data; then
log "ERROR" "备份失败。中止后续操作。"
log "INFO" "日志文件: ${LOG_FILE}"
exit 1
fi
# --- 步骤 2: 校验还原文件是否存在,存在则执行后续操作 ---
local proceed_with_modification=false
if [ -n "$IMPORT_FILE" ]; then
if validate_file "$IMPORT_FILE" "导入"; then
log "INFO" "导入文件 '${IMPORT_FILE}' 校验成功。准备进行修改。"
proceed_with_modification=true
else
log "ERROR" "导入文件校验失败。中止清空/导入操作,但备份已存在。"
log "INFO" "日志文件: ${LOG_FILE}"
exit 1 # 导入文件无效,退出
fi
elif [ "$CLEAR_ONLY" = true ] || [ "$NO_CLEAR" = true ]; then
# 如果没有导入文件,但设置了 --clear-only 或 --no-clear,我们仍将其视为“修改”意图。
# --no-clear 且没有导入文件的情况已在 parse_arguments 中捕获。
log "INFO" "操作为仅清空或与现有数据合并。准备进行修改。"
proceed_with_modification=true
else
# 这种情况理论上应在 parse_arguments 中捕获,但作为回退。
log "WARN" "未指定导入文件,且未处于仅清空/不清空模式。除备份外,不执行数据导入或清空。"
fi
if [ "$proceed_with_modification" = true ]; then
# --- 步骤 3: 用户确认操作 ---
confirm_operation
local import_success=false
if [ "$NO_CLEAR" != true ]; then
# --- 步骤 4: 清空目标集合 (如果未跳过) ---
if ! clear_collection; then
log "ERROR" "清空集合失败。正在尝试从备份还原。"
if ! restore_from_backup; then
log "CRITICAL" "清空失败后从备份还原也失败。绝对需要手动干预!"
exit 1
fi
# 如果从备份还原成功,则认为此阶段的“修改”是成功的(即回滚成功)
import_success=true
fi
else
log "INFO" "由于 --no-clear 选项,跳过了清空集合步骤。"
fi
# --- 步骤 5: 导入新数据 (如果指定了文件且不是仅清空模式) ---
# 只有在清空成功(或跳过清空)且没有发生回滚的情况下才尝试导入新数据
if [ "$import_success" != true ] && [ -n "$IMPORT_FILE" ] && [ "$CLEAR_ONLY" != true ]; then
if ! import_data "$IMPORT_FILE" "新数据"; then
log "ERROR" "新数据导入失败。正在尝试从备份还原。"
# --- 步骤 6: 如果还原失败则用备份的数据还原 ---
if ! restore_from_backup; then
log "CRITICAL" "导入失败后从备份还原也失败。绝对需要手动干预!"
exit 1
fi
fi
else
log "INFO" "跳过了数据导入步骤。"
fi
# --- 步骤 7: 验证操作结果 ---
verify_operation
# 完成消息
echo -e "\n${GREEN}=== 操作完成 ===${NC}"
log "SUCCESS" "所有操作完成"
else
echo -e "\n${YELLOW}=== 操作完成 (未执行数据修改) ===${NC}"
log "INFO" "由于导入文件校验失败或未指定操作,未执行数据修改 (清空/导入)。"
fi
log "INFO" "备份文件: ${BACKUP_FILE}" # 始终报告备份文件
log "INFO" "日志文件: ${LOG_FILE}"
}
main "$@"