From 4b9235c22e0078efb853fffe504647f58d0c8af8 Mon Sep 17 00:00:00 2001 From: uprightbass360 Date: Sun, 2 Nov 2025 21:42:01 -0500 Subject: [PATCH] backup merge script --- backup-merge.sh | 958 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 958 insertions(+) create mode 100755 backup-merge.sh diff --git a/backup-merge.sh b/backup-merge.sh new file mode 100755 index 0000000..3771cc3 --- /dev/null +++ b/backup-merge.sh @@ -0,0 +1,958 @@ +#!/bin/bash +# Merge accounts and characters from a backup into an existing ACore database +# This script handles ID remapping to avoid conflicts +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +COLOR_RED='\033[0;31m' +COLOR_GREEN='\033[0;32m' +COLOR_YELLOW='\033[1;33m' +COLOR_BLUE='\033[0;34m' +COLOR_CYAN='\033[0;36m' +COLOR_RESET='\033[0m' + +log(){ printf '%b\n' "${COLOR_GREEN}$*${COLOR_RESET}"; } +info(){ printf '%b\n' "${COLOR_CYAN}$*${COLOR_RESET}"; } +warn(){ printf '%b\n' "${COLOR_YELLOW}$*${COLOR_RESET}"; } +err(){ printf '%b\n' "${COLOR_RED}$*${COLOR_RESET}"; } +fatal(){ err "$*"; exit 1; } + +MYSQL_PW="" +BACKUP_DIR="" +AUTH_DB="acore_auth" +CHARACTERS_DB="acore_characters" +DRY_RUN=false +IMPORT_ACCOUNTS=() +IMPORT_CHARACTERS=() +IMPORT_ALL_ACCOUNTS=false +IMPORT_ALL_CHARACTERS=false +SKIP_CONFLICTS=false +EXCLUDE_BOTS=false +TEMP_DIR="" +MERGE_LOG="" + +usage(){ + cat <<'EOF' +Usage: ./backup-merge.sh [options] + +Merges accounts and characters from a backup into the current database. +Automatically handles ID remapping to avoid conflicts. + +Options: + -b, --backup-dir DIR Backup directory containing SQL dumps (required) + -p, --password PASS MySQL root password (required) + --auth-db NAME Auth database name (default: acore_auth) + --characters-db NAME Characters database name (default: acore_characters) + --account USERNAME Import specific account by username (repeatable) + --character NAME Import specific character by name (repeatable) + --all-accounts Import all accounts from backup + --all-characters Import all characters from backup + --skip-conflicts Skip accounts/characters that already exist + --exclude-bots Exclude bot accounts/characters (RNDBOT*, playerbots) + --dry-run Show what would be imported without making changes + -h, --help Show this help and exit + +Examples: + # Dry-run to see what would be imported + ./backup-merge.sh --backup-dir ../ac-backup --password azerothcore123 --all-accounts --all-characters --dry-run + + # Import all accounts and characters, excluding bots + ./backup-merge.sh --backup-dir ../ac-backup --password azerothcore123 --all-accounts --all-characters --exclude-bots + + # Import specific accounts + ./backup-merge.sh --backup-dir ../ac-backup --password azerothcore123 --account ARTIMAGE --account HAMSAMMY + + # Import specific character (imports its account too) + ./backup-merge.sh --backup-dir ../ac-backup --password azerothcore123 --character Artimage + +EOF +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + -b|--backup-dir) + [[ $# -ge 2 ]] || fatal "--backup-dir requires a value" + BACKUP_DIR="$2" + shift 2 + ;; + -p|--password) + [[ $# -ge 2 ]] || fatal "--password requires a value" + MYSQL_PW="$2" + shift 2 + ;; + --auth-db) + [[ $# -ge 2 ]] || fatal "--auth-db requires a value" + AUTH_DB="$2" + shift 2 + ;; + --characters-db) + [[ $# -ge 2 ]] || fatal "--characters-db requires a value" + CHARACTERS_DB="$2" + shift 2 + ;; + --account) + [[ $# -ge 2 ]] || fatal "--account requires a value" + IMPORT_ACCOUNTS+=("$2") + shift 2 + ;; + --character) + [[ $# -ge 2 ]] || fatal "--character requires a value" + IMPORT_CHARACTERS+=("$2") + shift 2 + ;; + --all-accounts) + IMPORT_ALL_ACCOUNTS=true + shift + ;; + --all-characters) + IMPORT_ALL_CHARACTERS=true + shift + ;; + --skip-conflicts) + SKIP_CONFLICTS=true + shift + ;; + --exclude-bots) + EXCLUDE_BOTS=true + shift + ;; + --dry-run) + DRY_RUN=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + fatal "Unknown option: $1" + ;; + esac +done + +# Validation +[[ -n "$BACKUP_DIR" ]] || fatal "Backup directory is required (use --backup-dir)" +[[ -d "$BACKUP_DIR" ]] || fatal "Backup directory not found: $BACKUP_DIR" +[[ -n "$MYSQL_PW" ]] || fatal "MySQL password is required (use --password)" + +if [[ ${#IMPORT_ACCOUNTS[@]} -eq 0 ]] && [[ ${#IMPORT_CHARACTERS[@]} -eq 0 ]] && ! $IMPORT_ALL_ACCOUNTS && ! $IMPORT_ALL_CHARACTERS; then + fatal "Must specify what to import: --account, --character, --all-accounts, or --all-characters" +fi + +# Setup temp directory +TEMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TEMP_DIR"' EXIT + +MERGE_LOG="$TEMP_DIR/merge.log" +touch "$MERGE_LOG" + +# MySQL connection helper +mysql_exec(){ + local db="$1" + docker exec -i ac-mysql mysql -uroot -p"$MYSQL_PW" "$db" 2>/dev/null +} + +mysql_query(){ + local db="$1" + local query="$2" + docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -N -B "$db" -e "$query" 2>/dev/null +} + +# Extract SQL dumps +log "Extracting backup files..." + +AUTH_DUMP="" +CHARACTERS_DUMP="" + +# Find auth dump +for pattern in "acore_auth.sql.gz" "auth.sql.gz" "acore_auth.sql" "auth.sql"; do + if [[ -f "$BACKUP_DIR/$pattern" ]]; then + AUTH_DUMP="$BACKUP_DIR/$pattern" + break + fi +done + +# Find characters dump +for pattern in "acore_characters.sql.gz" "characters.sql.gz" "acore_characters.sql" "characters.sql"; do + if [[ -f "$BACKUP_DIR/$pattern" ]]; then + CHARACTERS_DUMP="$BACKUP_DIR/$pattern" + break + fi +done + +[[ -n "$AUTH_DUMP" ]] || fatal "Auth database dump not found in $BACKUP_DIR" +[[ -n "$CHARACTERS_DUMP" ]] || fatal "Characters database dump not found in $BACKUP_DIR" + +log "Found auth dump: ${AUTH_DUMP##*/}" +log "Found characters dump: ${CHARACTERS_DUMP##*/}" + +# Extract dumps to temp files +info "Decompressing backup files..." +if [[ "$AUTH_DUMP" == *.gz ]]; then + zcat "$AUTH_DUMP" > "$TEMP_DIR/auth.sql" +else + cp "$AUTH_DUMP" "$TEMP_DIR/auth.sql" +fi + +if [[ "$CHARACTERS_DUMP" == *.gz ]]; then + zcat "$CHARACTERS_DUMP" > "$TEMP_DIR/characters.sql" +else + cp "$CHARACTERS_DUMP" "$TEMP_DIR/characters.sql" +fi + +# Load backup data into temp database +log "Creating temporary staging database..." + +STAGE_AUTH_DB="merge_stage_auth_$$" +STAGE_CHARS_DB="merge_stage_chars_$$" + +# Drop any existing staging databases +docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -e "DROP DATABASE IF EXISTS $STAGE_AUTH_DB;" 2>/dev/null || true +docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -e "DROP DATABASE IF EXISTS $STAGE_CHARS_DB;" 2>/dev/null || true + +# Create staging databases +docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -e "CREATE DATABASE $STAGE_AUTH_DB;" 2>/dev/null +docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -e "CREATE DATABASE $STAGE_CHARS_DB;" 2>/dev/null + +# Cleanup staging databases on exit +cleanup_staging(){ + if [[ -n "${STAGE_AUTH_DB:-}" ]]; then + docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -e "DROP DATABASE IF EXISTS $STAGE_AUTH_DB;" 2>/dev/null || true + fi + if [[ -n "${STAGE_CHARS_DB:-}" ]]; then + docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -e "DROP DATABASE IF EXISTS $STAGE_CHARS_DB;" 2>/dev/null || true + fi +} +trap 'cleanup_staging; rm -rf "$TEMP_DIR"' EXIT + +info "Loading backup into staging database..." + +# Modify SQL to use staging database names +sed "s/\`acore_auth\`/\`$STAGE_AUTH_DB\`/g; s/USE \`acore_auth\`;/USE \`$STAGE_AUTH_DB\`;/g" "$TEMP_DIR/auth.sql" | \ + docker exec -i ac-mysql mysql -uroot -p"$MYSQL_PW" 2>/dev/null + +sed "s/\`acore_characters\`/\`$STAGE_CHARS_DB\`/g; s/USE \`acore_characters\`;/USE \`$STAGE_CHARS_DB\`;/g" "$TEMP_DIR/characters.sql" | \ + docker exec -i ac-mysql mysql -uroot -p"$MYSQL_PW" 2>/dev/null + +log "Backup loaded into staging databases" + +# Analysis phase +info "" +info "═══════════════════════════════════════════════════════════" +info " ANALYSIS PHASE" +info "═══════════════════════════════════════════════════════════" + +# Get current database state +CURRENT_MAX_ACCOUNT_ID=$(mysql_query "$AUTH_DB" "SELECT COALESCE(MAX(id), 0) FROM account;") +CURRENT_MAX_CHAR_GUID=$(mysql_query "$CHARACTERS_DB" "SELECT COALESCE(MAX(guid), 0) FROM characters;") +CURRENT_MAX_ITEM_GUID=$(mysql_query "$CHARACTERS_DB" "SELECT COALESCE(MAX(guid), 0) FROM item_instance;") + +info "Current database state:" +info " Max account ID: $CURRENT_MAX_ACCOUNT_ID" +info " Max character GUID: $CURRENT_MAX_CHAR_GUID" +info " Max item GUID: $CURRENT_MAX_ITEM_GUID" + +# Get backup database state +BACKUP_ACCOUNT_COUNT=$(mysql_query "$STAGE_AUTH_DB" "SELECT COUNT(*) FROM account;") +BACKUP_CHAR_COUNT=$(mysql_query "$STAGE_CHARS_DB" "SELECT COUNT(*) FROM characters;") + +info "" +info "Backup contains:" +info " Accounts: $BACKUP_ACCOUNT_COUNT" +info " Characters: $BACKUP_CHAR_COUNT" + +# Build list of accounts to import +info "" +info "Building import list..." + +if $IMPORT_ALL_ACCOUNTS; then + if $EXCLUDE_BOTS; then + # Exclude bot accounts (RNDBOT%, bot%, etc.) + mysql_query "$STAGE_AUTH_DB" " + SELECT username FROM account + WHERE username NOT LIKE 'RNDBOT%' + AND username NOT LIKE 'bot%' + AND username NOT LIKE 'BOT%' + " > "$TEMP_DIR/accounts_to_import.txt" + else + mysql_query "$STAGE_AUTH_DB" "SELECT username FROM account;" > "$TEMP_DIR/accounts_to_import.txt" + fi +else + > "$TEMP_DIR/accounts_to_import.txt" + for username in "${IMPORT_ACCOUNTS[@]}"; do + echo "$username" >> "$TEMP_DIR/accounts_to_import.txt" + done +fi + +# Build list of characters to import +if $IMPORT_ALL_CHARACTERS; then + if $EXCLUDE_BOTS; then + # Check if playerbots DB exists in backup + PLAYERBOTS_DB_EXISTS=$(docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -e "SHOW DATABASES LIKE 'merge_stage_playerbots_%';" 2>/dev/null | wc -l) + + if [[ $PLAYERBOTS_DB_EXISTS -gt 1 ]]; then + # Exclude characters linked to playerbots_random_bots table + STAGE_PLAYERBOTS_DB=$(docker exec ac-mysql mysql -uroot -p"$MYSQL_PW" -e "SHOW DATABASES LIKE 'merge_stage_playerbots_%';" 2>/dev/null | tail -1) + mysql_query "$STAGE_CHARS_DB" " + SELECT c.name + FROM characters c + INNER JOIN account a ON c.account = a.id + LEFT JOIN $STAGE_PLAYERBOTS_DB.playerbots_random_bots pb ON c.guid = pb.bot + WHERE pb.bot IS NULL + AND a.username NOT LIKE 'RNDBOT%' + AND a.username NOT LIKE 'bot%' + AND a.username NOT LIKE 'BOT%' + " > "$TEMP_DIR/characters_to_import.txt" 2>/dev/null || { + # Fallback if playerbots DB structure is different + mysql_query "$STAGE_CHARS_DB" " + SELECT c.name + FROM characters c + INNER JOIN $STAGE_AUTH_DB.account a ON c.account = a.id + WHERE a.username NOT LIKE 'RNDBOT%' + AND a.username NOT LIKE 'bot%' + AND a.username NOT LIKE 'BOT%' + " > "$TEMP_DIR/characters_to_import.txt" + } + else + # No playerbots DB, just exclude characters from bot accounts + mysql_query "$STAGE_CHARS_DB" " + SELECT c.name + FROM characters c + INNER JOIN $STAGE_AUTH_DB.account a ON c.account = a.id + WHERE a.username NOT LIKE 'RNDBOT%' + AND a.username NOT LIKE 'bot%' + AND a.username NOT LIKE 'BOT%' + " > "$TEMP_DIR/characters_to_import.txt" + fi + else + mysql_query "$STAGE_CHARS_DB" "SELECT name FROM characters;" > "$TEMP_DIR/characters_to_import.txt" + fi +else + > "$TEMP_DIR/characters_to_import.txt" + for charname in "${IMPORT_CHARACTERS[@]}"; do + echo "$charname" >> "$TEMP_DIR/characters_to_import.txt" + done +fi + +# If importing specific characters, also import their accounts +if [[ -s "$TEMP_DIR/characters_to_import.txt" ]] && ! $IMPORT_ALL_ACCOUNTS; then + while IFS= read -r charname; do + account_id=$(mysql_query "$STAGE_CHARS_DB" "SELECT account FROM characters WHERE name='$charname';" || echo "") + if [[ -n "$account_id" ]]; then + username=$(mysql_query "$STAGE_AUTH_DB" "SELECT username FROM account WHERE id=$account_id;" || echo "") + if [[ -n "$username" ]]; then + echo "$username" >> "$TEMP_DIR/accounts_to_import.txt" + fi + fi + done < "$TEMP_DIR/characters_to_import.txt" + + # Remove duplicates + sort -u "$TEMP_DIR/accounts_to_import.txt" -o "$TEMP_DIR/accounts_to_import.txt" +fi + +ACCOUNTS_TO_IMPORT=$(wc -l < "$TEMP_DIR/accounts_to_import.txt" | tr -d ' ') +CHARACTERS_TO_IMPORT=$(wc -l < "$TEMP_DIR/characters_to_import.txt" | tr -d ' ') + +if $EXCLUDE_BOTS; then + BOT_ACCOUNTS_EXCLUDED=$((BACKUP_ACCOUNT_COUNT - ACCOUNTS_TO_IMPORT)) + BOT_CHARS_EXCLUDED=$((BACKUP_CHAR_COUNT - CHARACTERS_TO_IMPORT)) + info "" + info "Bot filtering enabled:" + info " Bot accounts excluded: $BOT_ACCOUNTS_EXCLUDED" + info " Bot characters excluded: $BOT_CHARS_EXCLUDED" +fi + +info "" +info "Accounts to import: $ACCOUNTS_TO_IMPORT" +info "Characters to import: $CHARACTERS_TO_IMPORT" + +if [[ $ACCOUNTS_TO_IMPORT -eq 0 ]] && [[ $CHARACTERS_TO_IMPORT -eq 0 ]]; then + fatal "No accounts or characters selected for import" +fi + +# Conflict detection +info "" +info "Checking for conflicts..." + +ACCOUNT_CONFLICTS=0 +CHARACTER_CONFLICTS=0 + +# Check account username conflicts +> "$TEMP_DIR/account_conflicts.txt" +while IFS= read -r username; do + existing=$(mysql_query "$AUTH_DB" "SELECT COUNT(*) FROM account WHERE username='$username';" || echo "0") + if [[ "$existing" != "0" ]]; then + echo "$username" >> "$TEMP_DIR/account_conflicts.txt" + ((ACCOUNT_CONFLICTS++)) || true + fi +done < "$TEMP_DIR/accounts_to_import.txt" + +# Check character name conflicts +> "$TEMP_DIR/character_conflicts.txt" +while IFS= read -r charname; do + existing=$(mysql_query "$CHARACTERS_DB" "SELECT COUNT(*) FROM characters WHERE name='$charname';" || echo "0") + if [[ "$existing" != "0" ]]; then + echo "$charname" >> "$TEMP_DIR/character_conflicts.txt" + ((CHARACTER_CONFLICTS++)) || true + fi +done < "$TEMP_DIR/characters_to_import.txt" + +if [[ $ACCOUNT_CONFLICTS -gt 0 ]] || [[ $CHARACTER_CONFLICTS -gt 0 ]]; then + warn "Found conflicts:" + if [[ $ACCOUNT_CONFLICTS -gt 0 ]]; then + warn " $ACCOUNT_CONFLICTS account username(s) already exist:" + while IFS= read -r username; do + warn " - $username" + done < "$TEMP_DIR/account_conflicts.txt" + fi + if [[ $CHARACTER_CONFLICTS -gt 0 ]]; then + warn " $CHARACTER_CONFLICTS character name(s) already exist:" + while IFS= read -r charname; do + warn " - $charname" + done < "$TEMP_DIR/character_conflicts.txt" + fi + + if $SKIP_CONFLICTS; then + warn "Skipping conflicting entries (--skip-conflicts enabled)" + # Remove conflicts from import lists + if [[ -s "$TEMP_DIR/account_conflicts.txt" ]]; then + grep -vxF -f "$TEMP_DIR/account_conflicts.txt" "$TEMP_DIR/accounts_to_import.txt" > "$TEMP_DIR/accounts_to_import_filtered.txt" || true + mv "$TEMP_DIR/accounts_to_import_filtered.txt" "$TEMP_DIR/accounts_to_import.txt" + fi + if [[ -s "$TEMP_DIR/character_conflicts.txt" ]]; then + grep -vxF -f "$TEMP_DIR/character_conflicts.txt" "$TEMP_DIR/characters_to_import.txt" > "$TEMP_DIR/characters_to_import_filtered.txt" || true + mv "$TEMP_DIR/characters_to_import_filtered.txt" "$TEMP_DIR/characters_to_import.txt" + fi + + ACCOUNTS_TO_IMPORT=$(wc -l < "$TEMP_DIR/accounts_to_import.txt" | tr -d ' ') + CHARACTERS_TO_IMPORT=$(wc -l < "$TEMP_DIR/characters_to_import.txt" | tr -d ' ') + + if [[ $ACCOUNTS_TO_IMPORT -eq 0 ]] && [[ $CHARACTERS_TO_IMPORT -eq 0 ]]; then + warn "All entries had conflicts. Nothing to import." + exit 0 + fi + else + err "" + err "Conflicts detected. Options:" + err " 1. Use --skip-conflicts to skip existing entries" + err " 2. Manually rename conflicting accounts/characters in the backup" + err " 3. Delete conflicting entries from current database" + exit 1 + fi +else + log "No conflicts detected" +fi + +# Calculate ID offsets +ACCOUNT_OFFSET=$CURRENT_MAX_ACCOUNT_ID +CHAR_OFFSET=$CURRENT_MAX_CHAR_GUID +ITEM_OFFSET=$CURRENT_MAX_ITEM_GUID + +info "" +info "ID remapping offsets:" +info " Account ID offset: +$ACCOUNT_OFFSET" +info " Character GUID offset: +$CHAR_OFFSET" +info " Item GUID offset: +$ITEM_OFFSET" + +# Generate mapping tables +info "" +info "Generating ID mapping tables..." + +# Account ID mapping +mysql_exec "$STAGE_AUTH_DB" </dev/null 2>&1 || warn "Services already stopped" + +log "Starting import..." + +# Import accounts +if [[ $ACCOUNTS_TO_IMPORT -gt 0 ]]; then + log "Importing $ACCOUNTS_TO_IMPORT account(s)..." + + # Import main account table + cat </dev/null || echo "0") + + if [[ "$row_count" == "0" ]]; then + continue + fi + + # Get all columns except guid + columns=$(mysql_query "$STAGE_CHARS_DB" " + SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY ORDINAL_POSITION SEPARATOR ', ') + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = '$STAGE_CHARS_DB' AND TABLE_NAME = '$table' + ") + + # Build select list with guid remapping + select_list=$(echo "$columns" | sed 's/\bguid\b/cm.new_guid/g') + + # Import with guid remapping + cat </dev/null || warn " Warning: Could not import $table" +INSERT IGNORE INTO $table ($columns) +SELECT $select_list +FROM $STAGE_CHARS_DB.$table t +INNER JOIN $STAGE_CHARS_DB.character_guid_map cm ON t.guid = cm.old_guid; +EOSQL + + done + + log "✓ Character progression data imported" +fi + +# Restart services +log "" +log "Restarting services..." +docker restart ac-authserver ac-worldserver >/dev/null 2>&1 + +log "Waiting for services to initialize..." +sleep 5 + +for i in {1..30}; do + if docker exec ac-worldserver pgrep worldserver >/dev/null 2>&1 && docker exec ac-authserver pgrep authserver >/dev/null 2>&1; then + log "✓ Services running" + break + fi + if [ $i -eq 30 ]; then + warn "Services took longer than expected to start" + fi + sleep 2 +done + +# Final report +log "" +log "═══════════════════════════════════════════════════════════" +log " IMPORT COMPLETE" +log "═══════════════════════════════════════════════════════════" + +if [[ $ACCOUNTS_TO_IMPORT -gt 0 ]]; then + log "" + log "Imported accounts:" + while IFS= read -r username; do + new_id=$(mysql_query "$AUTH_DB" "SELECT id FROM account WHERE username='$username';" || echo "?") + log " ✓ $username (account id: $new_id)" + done < "$TEMP_DIR/accounts_to_import.txt" +fi + +if [[ $CHARACTERS_TO_IMPORT -gt 0 ]]; then + log "" + log "Imported characters:" + while IFS= read -r charname; do + new_guid=$(mysql_query "$CHARACTERS_DB" "SELECT guid FROM characters WHERE name='$charname';" || echo "?") + log " ✓ $charname (guid: $new_guid)" + done < "$TEMP_DIR/characters_to_import.txt" +fi + +log "" +log "Merge complete! All accounts and characters have been imported." +log "Players can now log in with their restored accounts."