#!/usr/bin/env bash # MariaDB/MySQL 논리 백업 (mysqldump). .env의 MR_DB_* 및 MR_BACKUP_* 사용. # MR_BACKUP_SCOPE=all 이면 접근 가능한 사용자 DB 전체를 각각 .sql.gz 로 저장. set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" # shellcheck source=scripts/lib/load-env.sh source "$REPO_ROOT/scripts/lib/load-env.sh" # shellcheck source=scripts/lib/common.sh source "$REPO_ROOT/scripts/lib/common.sh" load_project_env "$REPO_ROOT/.env" : "${MR_DB_HOST:?MR_DB_HOST is required in .env}" : "${MR_DB_USER:?MR_DB_USER is required in .env}" : "${MR_DB_PASSWORD:?MR_DB_PASSWORD is required in .env}" MR_DB_PORT="${MR_DB_PORT:-3306}" MR_DB_NAME="${MR_DB_NAME:-mysql}" BACKUP_DIR="${BACKUP_DIR:-$REPO_ROOT/backup}" BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-30}" MR_BACKUP_SCOPE="${MR_BACKUP_SCOPE:-all}" MYSQL_BIN="${MYSQL_BIN:-mysql}" MYSQLDUMP_BIN="${MYSQLDUMP_BIN:-mysqldump}" MR_DUMP_PY="$REPO_ROOT/scripts/lib/mr_dump.py" use_python_fallback=0 if ! command -v "$MYSQLDUMP_BIN" >/dev/null 2>&1; then if [[ -f "$MR_DUMP_PY" ]] && python3 -c "import pymysql" >/dev/null 2>&1; then echo "[$(log_ts)] mr-backup: mysqldump not found; using Python fallback (pymysql)." >&2 use_python_fallback=1 else echo "mr-backup: mysqldump not found. Install mariadb-client (apt) or mariadb (brew), or install pymysql." >&2 exit 1 fi fi if [[ "$use_python_fallback" -eq 0 ]] && ! command -v "$MYSQL_BIN" >/dev/null 2>&1; then echo "mr-backup: mysql client not found. Install mariadb-client." >&2 exit 1 fi mysql_common_args=( -h "$MR_DB_HOST" -P "$MR_DB_PORT" -u "$MR_DB_USER" --password="$MR_DB_PASSWORD" --batch --skip-column-names ) list_all_databases() { if [[ "$use_python_fallback" -eq 1 ]]; then python3 - "$MR_DB_HOST" "$MR_DB_PORT" "$MR_DB_USER" "$MR_DB_PASSWORD" <<'PY' import pymysql, sys host, port, user, password = sys.argv[1:5] conn = pymysql.connect(host=host, port=int(port), user=user, password=password, connect_timeout=30) try: with conn.cursor() as cur: cur.execute( "SELECT schema_name FROM information_schema.schemata " "WHERE schema_name NOT IN ('information_schema','performance_schema','sys','mysql') " "ORDER BY schema_name" ) for row in cur.fetchall(): print(row[0]) finally: conn.close() PY return fi "$MYSQL_BIN" "${mysql_common_args[@]}" -e \ "SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema','performance_schema','sys','mysql') ORDER BY schema_name;" } dump_database() { local db_name="$1" local dump_file="$2" if [[ "$use_python_fallback" -eq 1 ]]; then python3 "$MR_DUMP_PY" \ --host "$MR_DB_HOST" \ --port "$MR_DB_PORT" \ --user "$MR_DB_USER" \ --password "$MR_DB_PASSWORD" \ --database "$db_name" \ --output "$dump_file" return fi "$MYSQLDUMP_BIN" \ -h "$MR_DB_HOST" \ -P "$MR_DB_PORT" \ -u "$MR_DB_USER" \ --password="$MR_DB_PASSWORD" \ --single-transaction \ --routines \ --triggers \ --events \ --databases "$db_name" \ | gzip -c > "$dump_file" } resolve_run_dir "mariadb" mkdir -p "$BACKUP_DIR/latest" echo "[$(log_ts)] mr-backup start → $RUN_DIR (scope=${MR_BACKUP_SCOPE}, retention ${BACKUP_RETENTION_DAYS} days)" declare -a TARGET_DBS=() if [[ "$MR_BACKUP_SCOPE" == "single" ]]; then TARGET_DBS=("$MR_DB_NAME") else while IFS= read -r db; do [[ -n "$db" ]] && TARGET_DBS+=("$db") done < <(list_all_databases) if [[ ${#TARGET_DBS[@]} -eq 0 ]]; then echo "mr-backup: no databases found to backup." >&2 exit 1 fi fi MANIFEST="$RUN_DIR/00_manifest.txt" { echo "# mr-backup manifest $(log_ts)" echo "scope=${MR_BACKUP_SCOPE}" echo "host=${MR_DB_HOST}:${MR_DB_PORT}" echo "user=${MR_DB_USER}" } > "$MANIFEST" dump_ok=0 dump_fail=0 for db_name in "${TARGET_DBS[@]}"; do dump_file="$RUN_DIR/${db_name}.sql.gz" echo "[$(log_ts)] dumping database: $db_name" if dump_database "$db_name" "$dump_file"; then bytes="$(wc -c < "$dump_file" | tr -d ' ')" echo "[$(log_ts)] dump saved: $dump_file (${bytes} bytes)" echo "${db_name}.sql.gz ${bytes}" >> "$MANIFEST" dump_ok=$((dump_ok + 1)) else echo "[$(log_ts)] dump failed: $db_name" >&2 dump_fail=$((dump_fail + 1)) fi done if [[ "$dump_ok" -eq 0 ]]; then echo "mr-backup: all database dumps failed." >&2 exit 1 fi if [[ "$dump_fail" -gt 0 ]]; then echo "[$(log_ts)] warning: ${dump_fail} database dump(s) failed, ${dump_ok} succeeded." >&2 fi update_latest_link "mariadb" if [[ -z "${BACKUP_RUN_DIR:-}" ]]; then prune_old_backups "$BACKUP_DIR" "$BACKUP_RETENTION_DAYS" fi echo "[$(log_ts)] mr-backup done: ${dump_ok} database(s), latest → $BACKUP_DIR/latest/mariadb"