Files
ncue_backup/scripts/mr-backup.sh
dsyoon 3c9d9283bd Add daily backup scripts for PostgreSQL, MariaDB, and Gitea.
Enable scheduled backups of ncue.net databases and git.ncue.net repository mirrors via .env-driven shell scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 17:02:53 +09:00

161 lines
4.7 KiB
Bash
Executable File

#!/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"