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:
153
scripts/pg-backup.sh
Executable file
153
scripts/pg-backup.sh
Executable file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostgreSQL 논리 백업 (pg_dump -Fc). .env의 PG_DB_* 및 PG_BACKUP_* 사용.
|
||||
# PG_BACKUP_SCOPE=all 이면 서버의 연결 가능·비템플릿 DB 전체를 각각 .dump 로 저장.
|
||||
# 운영 cron 예: 0 2 * * * cd /path/ncue_backup && bash scripts/daily-backup.sh >> /var/log/ncue-backup.log 2>&1
|
||||
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"
|
||||
|
||||
: "${PG_DB_HOST:?PG_DB_HOST is required in .env}"
|
||||
: "${PG_DB_USER:?PG_DB_USER is required in .env}"
|
||||
: "${PG_DB_PASSWORD:?PG_DB_PASSWORD is required in .env}"
|
||||
|
||||
PG_DB_PORT="${PG_DB_PORT:-5432}"
|
||||
PG_DB_NAME="${PG_DB_NAME:-postgres}"
|
||||
BACKUP_DIR="${BACKUP_DIR:-$REPO_ROOT/backup}"
|
||||
BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-30}"
|
||||
PG_BACKUP_SCOPE="${PG_BACKUP_SCOPE:-all}"
|
||||
PG_BACKUP_GLOBALS="${PG_BACKUP_GLOBALS:-1}"
|
||||
PG_BACKUP_SUPERUSER="${PG_BACKUP_SUPERUSER:-postgres}"
|
||||
|
||||
export PGHOST="$PG_DB_HOST"
|
||||
export PGPORT="$PG_DB_PORT"
|
||||
|
||||
if ! command -v pg_dump >/dev/null 2>&1; then
|
||||
echo "pg-backup: pg_dump not found. Install postgresql-client (apt) or postgresql (brew)." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v psql >/dev/null 2>&1; then
|
||||
echo "pg-backup: psql not found. Install postgresql-client." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set_backup_credentials() {
|
||||
if [[ -n "${PG_BACKUP_SUPERUSER_PASSWORD:-}" ]]; then
|
||||
export PGUSER="$PG_BACKUP_SUPERUSER"
|
||||
export PGPASSWORD="$PG_BACKUP_SUPERUSER_PASSWORD"
|
||||
else
|
||||
export PGUSER="$PG_DB_USER"
|
||||
export PGPASSWORD="$PG_DB_PASSWORD"
|
||||
fi
|
||||
}
|
||||
|
||||
list_all_databases() {
|
||||
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -v ON_ERROR_STOP=1 -tAc \
|
||||
"SELECT datname FROM pg_database WHERE datallowconn AND NOT datistemplate ORDER BY datname;"
|
||||
}
|
||||
|
||||
dump_database() {
|
||||
local db_name="$1"
|
||||
local dump_file="$2"
|
||||
pg_dump \
|
||||
-h "$PGHOST" \
|
||||
-p "$PGPORT" \
|
||||
-U "$PGUSER" \
|
||||
-d "$db_name" \
|
||||
-Fc \
|
||||
--no-password \
|
||||
-f "$dump_file"
|
||||
}
|
||||
|
||||
backup_globals() {
|
||||
if [[ "$PG_BACKUP_GLOBALS" != "1" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ -z "${PG_BACKUP_SUPERUSER_PASSWORD:-}" ]]; then
|
||||
echo "[$(log_ts)] globals skipped: set PG_BACKUP_SUPERUSER_PASSWORD for role/global backup." >&2
|
||||
return 0
|
||||
fi
|
||||
local prev_user="$PGUSER" prev_pass="$PGPASSWORD"
|
||||
export PGUSER="$PG_BACKUP_SUPERUSER"
|
||||
export PGPASSWORD="$PG_BACKUP_SUPERUSER_PASSWORD"
|
||||
pg_dumpall \
|
||||
-h "$PGHOST" \
|
||||
-p "$PGPORT" \
|
||||
-U "$PGUSER" \
|
||||
--globals-only \
|
||||
--no-password \
|
||||
-f "$RUN_DIR/00_globals.sql"
|
||||
export PGUSER="$prev_user"
|
||||
export PGPASSWORD="$prev_pass"
|
||||
echo "[$(log_ts)] globals saved: $RUN_DIR/00_globals.sql"
|
||||
}
|
||||
|
||||
set_backup_credentials
|
||||
resolve_run_dir "postgresql"
|
||||
mkdir -p "$BACKUP_DIR/latest"
|
||||
|
||||
echo "[$(log_ts)] pg-backup start → $RUN_DIR (scope=${PG_BACKUP_SCOPE}, retention ${BACKUP_RETENTION_DAYS} days)"
|
||||
|
||||
if [[ -z "${PG_BACKUP_SUPERUSER_PASSWORD:-}" && "$PG_BACKUP_SCOPE" == "all" ]]; then
|
||||
echo "[$(log_ts)] note: PG_BACKUP_SUPERUSER_PASSWORD not set; backing up only databases visible to ${PG_DB_USER}." >&2
|
||||
fi
|
||||
|
||||
backup_globals
|
||||
|
||||
declare -a TARGET_DBS=()
|
||||
if [[ "$PG_BACKUP_SCOPE" == "single" ]]; then
|
||||
TARGET_DBS=("$PG_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 "pg-backup: no databases found to backup." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
MANIFEST="$RUN_DIR/00_manifest.txt"
|
||||
{
|
||||
echo "# pg-backup manifest $(log_ts)"
|
||||
echo "scope=${PG_BACKUP_SCOPE}"
|
||||
echo "host=${PGHOST}:${PGPORT}"
|
||||
echo "user=${PGUSER}"
|
||||
} > "$MANIFEST"
|
||||
|
||||
dump_ok=0
|
||||
dump_fail=0
|
||||
for db_name in "${TARGET_DBS[@]}"; do
|
||||
dump_file="$RUN_DIR/${db_name}.dump"
|
||||
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}.dump ${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 "pg-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 "postgresql"
|
||||
|
||||
if [[ -z "${BACKUP_RUN_DIR:-}" ]]; then
|
||||
prune_old_backups "$BACKUP_DIR" "$BACKUP_RETENTION_DAYS"
|
||||
fi
|
||||
|
||||
echo "[$(log_ts)] pg-backup done: ${dump_ok} database(s), latest → $BACKUP_DIR/latest/postgresql"
|
||||
Reference in New Issue
Block a user