#!/bin/bash # ============================================================================= # postgres_backup.sh # Backup wszystkich baz PostgreSQL na serwerze # # Przeznaczenie: cron (nie interaktywny) # Wymagania: pg_dump, psql, dostęp do pliku poświadczeń (format pgpass) # # Przywracanie pojedynczej bazy: # psql -h localhost -U postgres -d postgres -f nazwa_bazy.sql # gunzip -c nazwa_bazy.sql.gz | psql -h localhost -U postgres -d postgres # ============================================================================= # ============================================================================= # KONFIGURACJA # ============================================================================= BASE_DIR="/home/admin/postgres_backup" # Format daty katalogu backupu (składnia date +FORMAT) DATE_FORMAT="%d-%m-%Y" # 1 = dodaj godzinę i minuty do nazwy katalogu (np. 19-02-2026_14-35) ADD_TIME=0 # Liczba dni przechowywania backupów (0 = bez automatycznego usuwania) RETENTION=7 # 1 = kompresuj do .sql.gz | 0 = zostawiaj jako .sql ENABLE_COMPRESSION=1 # Plik poświadczeń w formacie pgpass: host:port:database:user:password # Musi mieć uprawnienia 600 lub 400 CREDENTIALS_FILE="/usr/local/directadmin/conf/postgresql.conf" # Bazy wykluczone z backupu (oddzielone spacją) EXCLUDE_DBS="template0 template1" # ============================================================================= # WEWNĘTRZNA KONFIGURACJA – nie zmieniaj # ============================================================================= LOCK_FILE="/var/run/postgres_backup.lock" LOG_FILE="/var/log/postgres_backup.log" # ============================================================================= # INICJALIZACJA # ============================================================================= set -uo pipefail # Kolory tylko na terminalu if [[ -t 1 ]]; then RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' else RED=''; GREEN=''; YELLOW=''; NC='' fi ts() { date '+%Y-%m-%d %H:%M:%S'; } log() { echo "[$(ts)] $*" | tee -a "$LOG_FILE"; } ok() { echo -e "${GREEN}[OK]${NC} [$(ts)] $*" | tee -a "$LOG_FILE"; } warn() { echo -e "${YELLOW}[WARN]${NC} [$(ts)] $*" | tee -a "$LOG_FILE"; } die() { echo -e "${RED}[BŁĄD]${NC} [$(ts)] $*" | tee -a "$LOG_FILE" >&2; cleanup; exit 1; } cleanup() { rm -f "$LOCK_FILE"; } trap cleanup EXIT INT TERM # --------------------------------------------------------------------------- # Blokada przed równoległym uruchomieniem # --------------------------------------------------------------------------- if [[ -f "$LOCK_FILE" ]]; then OLD_PID=$(cat "$LOCK_FILE" 2>/dev/null || echo "") if [[ -n "$OLD_PID" ]] && kill -0 "$OLD_PID" 2>/dev/null; then die "Backup jest już uruchomiony (PID: ${OLD_PID}). Przerywam." fi warn "Stały plik blokady (PID ${OLD_PID} nie istnieje) – usuwam i kontynuuję." rm -f "$LOCK_FILE" fi echo $$ > "$LOCK_FILE" # --------------------------------------------------------------------------- # Weryfikacja pliku poświadczeń # --------------------------------------------------------------------------- [[ -f "$CREDENTIALS_FILE" ]] \ || die "Brak pliku poświadczeń: ${CREDENTIALS_FILE}" _perms=$(stat -c '%a' "$CREDENTIALS_FILE") [[ "$_perms" == "600" || "$_perms" == "400" ]] \ || die "Plik ${CREDENTIALS_FILE} ma zbyt otwarte uprawnienia (${_perms}). Wymagane: 600 lub 400." # Parsowanie: host:port:database:user:password _line=$(head -1 "$CREDENTIALS_FILE") PGHOST=$(echo "$_line" | cut -d: -f1) PGPORT=$(echo "$_line" | cut -d: -f2) PGUSER=$(echo "$_line" | cut -d: -f4) [[ -n "$PGHOST" && -n "$PGPORT" && -n "$PGUSER" ]] \ || die "Nieprawidłowy format pliku poświadczeń. Oczekiwany: host:port:database:user:password" # Hasło przekazywane przez PGPASSFILE – nie pojawia się w środowisku ani linii poleceń export PGPASSFILE="$CREDENTIALS_FILE" # --------------------------------------------------------------------------- # Katalog backupu # --------------------------------------------------------------------------- if [[ "$ADD_TIME" == "1" ]]; then DATE_STR=$(date +"${DATE_FORMAT}_%H-%M") else DATE_STR=$(date +"${DATE_FORMAT}") fi BACKUP_DIR="${BASE_DIR}/${DATE_STR}" mkdir -p "$BACKUP_DIR" || die "Nie można utworzyć katalogu backupu: ${BACKUP_DIR}" # ============================================================================= # BACKUP # ============================================================================= log "=================================================================" log "Rozpoczęcie backupu PostgreSQL" log "Host: ${PGHOST}:${PGPORT} Użytkownik: ${PGUSER}" log "Katalog: ${BACKUP_DIR}" log "Kompresja: ${ENABLE_COMPRESSION} Retencja: ${RETENTION} dni" log "=================================================================" # --------------------------------------------------------------------------- # Globals: użytkownicy, role, hasła, przynależności (pg_dumpall --globals-only) # Wymagane do pełnego disaster recovery – przywróć jako pierwsze # --------------------------------------------------------------------------- ERRORS=0 BACKED_UP=0 GLOBALS_SUCCESS=true if [[ "$ENABLE_COMPRESSION" == "1" ]]; then GLOBALS_FILE="${BACKUP_DIR}/_globals.sql.gz" pg_dumpall \ -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" \ --no-password --globals-only --clean --if-exists \ 2>>"$LOG_FILE" \ | gzip -9 > "$GLOBALS_FILE" \ || GLOBALS_SUCCESS=false else GLOBALS_FILE="${BACKUP_DIR}/_globals.sql" pg_dumpall \ -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" \ --no-password --globals-only --clean --if-exists \ > "$GLOBALS_FILE" 2>>"$LOG_FILE" \ || GLOBALS_SUCCESS=false fi if [[ "$GLOBALS_SUCCESS" == "true" ]]; then SIZE=$(du -sh "$GLOBALS_FILE" 2>/dev/null | cut -f1) ok "_globals → ${GLOBALS_FILE##*/} (${SIZE})" else warn "_globals: backup ról i użytkowników nie powiódł się – plik usunięty." rm -f "$GLOBALS_FILE" ERRORS=$((ERRORS + 1)) fi # Lista baz danych DATABASES=$( psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" \ --no-password -t -A \ -c "SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname;" \ 2>>"$LOG_FILE" ) || die "Nie można pobrać listy baz. Sprawdź połączenie i poświadczenia." # Zbuduj wzorzec wykluczeń EXCLUDE_PATTERN=$(echo "$EXCLUDE_DBS" | tr ' ' '|') for DB in $DATABASES; do # Pomiń wykluczone if echo "$DB" | grep -qE "^(${EXCLUDE_PATTERN})$"; then continue fi SUCCESS=true if [[ "$ENABLE_COMPRESSION" == "1" ]]; then OUT_FILE="${BACKUP_DIR}/${DB}.sql.gz" pg_dump \ -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" \ --no-password --create --clean --if-exists \ -F p "$DB" 2>>"$LOG_FILE" \ | gzip -9 > "$OUT_FILE" \ || SUCCESS=false else OUT_FILE="${BACKUP_DIR}/${DB}.sql" pg_dump \ -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" \ --no-password --create --clean --if-exists \ -F p "$DB" > "$OUT_FILE" 2>>"$LOG_FILE" \ || SUCCESS=false fi if [[ "$SUCCESS" == "true" ]]; then SIZE=$(du -sh "$OUT_FILE" 2>/dev/null | cut -f1) ok "${DB} → ${OUT_FILE##*/} (${SIZE})" BACKED_UP=$((BACKED_UP + 1)) else warn "${DB}: backup nie powiódł się – plik usunięty." rm -f "$OUT_FILE" ERRORS=$((ERRORS + 1)) fi done log "Wynik: ${BACKED_UP} OK, ${ERRORS} błędów." # ============================================================================= # RETENCJA # ============================================================================= if [[ "$RETENTION" -gt 0 ]]; then log "Usuwanie backupów starszych niż ${RETENTION} dni..." find "$BASE_DIR" -maxdepth 1 -mindepth 1 -type d -mtime +"$RETENTION" \ -exec rm -rf {} + 2>/dev/null || true fi # ============================================================================= # ZAKOŃCZENIE # ============================================================================= log "Backup zakończony." [[ "$ERRORS" -eq 0 ]] || exit 1 exit 0