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>
161 lines
4.7 KiB
Bash
Executable File
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"
|