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>
This commit is contained in:
160
scripts/mr-backup.sh
Executable file
160
scripts/mr-backup.sh
Executable file
@@ -0,0 +1,160 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user