feat: add comprehensive PostgreSQL backup and restore scripts
All checks were successful
Basil CI/CD Pipeline / Shared Package Tests (push) Successful in 1m10s
Basil CI/CD Pipeline / Code Linting (push) Successful in 1m18s
Basil CI/CD Pipeline / Web Tests (push) Successful in 1m29s
Basil CI/CD Pipeline / Security Scanning (push) Successful in 1m14s
Basil CI/CD Pipeline / API Tests (push) Successful in 1m45s
Basil CI/CD Pipeline / Trigger Deployment (push) Successful in 12s
Basil CI/CD Pipeline / Build All Packages (push) Successful in 1m31s
Basil CI/CD Pipeline / E2E Tests (push) Has been skipped
Basil CI/CD Pipeline / Build & Push Docker Images (push) Successful in 14m27s

Added production-grade backup and restore scripts for PostgreSQL servers
that can backup all databases automatically with retention management.

New scripts:
- scripts/backup-all-postgres-databases.sh - Backs up all databases on a
  PostgreSQL server with automatic retention, compression, verification,
  and notification support
- scripts/restore-postgres-database.sh - Restores individual databases
  with safety backups and verification
- scripts/README-POSTGRES-BACKUP.md - Complete documentation with examples,
  best practices, and troubleshooting

Features:
- Automatic detection and backup of all user databases
- Excludes system databases (postgres, template0, template1)
- Backs up global objects (roles, tablespaces)
- Optional gzip compression (80-90% space savings)
- Automatic retention management (configurable days)
- Integrity verification (gzip -t for compressed files)
- Safety backups before restore operations
- Detailed logging with color-coded output
- Backup summary reporting
- Email/Slack notification support (optional)
- Interactive restore with confirmation prompts
- Force mode for automation
- Verbose debugging mode
- Comprehensive error handling

Backup directory structure:
  /var/backups/postgresql/YYYYMMDD/
    - globals_YYYYMMDD_HHMMSS.sql.gz
    - database1_YYYYMMDD_HHMMSS.sql.gz
    - database2_YYYYMMDD_HHMMSS.sql.gz

Usage examples:
  # Backup all databases with compression
  ./backup-all-postgres-databases.sh -c

  # Custom configuration
  ./backup-all-postgres-databases.sh -h db.server.com -U backup_user -d /mnt/backups -r 60 -c

  # Restore database
  ./restore-postgres-database.sh /var/backups/postgresql/20260120/mydb_20260120_020001.sql.gz

  # Force restore (skip confirmation)
  ./restore-postgres-database.sh backup.sql.gz -d mydb -f

Automation:
  # Add to crontab for daily backups at 2 AM
  0 2 * * * /path/to/backup-all-postgres-databases.sh -c >> /var/log/postgres-backup.log 2>&1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Paul R Kartchner
2026-01-25 21:39:32 -07:00
parent 883b7820ed
commit fb18caa3c2
3 changed files with 1256 additions and 0 deletions

View File

@@ -0,0 +1,458 @@
# PostgreSQL Backup Scripts
Comprehensive backup and restore scripts for PostgreSQL databases.
## Scripts Overview
### 1. `backup-all-postgres-databases.sh`
Backs up all databases on a PostgreSQL server (excluding system databases).
**Features:**
- ✅ Backs up all user databases automatically
- ✅ Includes global objects (roles, tablespaces)
- ✅ Optional gzip compression
- ✅ Automatic retention management
- ✅ Integrity verification
- ✅ Detailed logging with color output
- ✅ Backup summary reporting
- ✅ Email/Slack notification support (optional)
### 2. `restore-postgres-database.sh`
Restores a single database from backup.
**Features:**
- ✅ Safety backup before restore
- ✅ Interactive confirmation
- ✅ Automatic database name detection
- ✅ Compressed file support
- ✅ Integrity verification
- ✅ Post-restore verification
---
## Quick Start
### Backup All Databases
```bash
# Basic usage
./backup-all-postgres-databases.sh
# With compression (recommended)
./backup-all-postgres-databases.sh -c
# Custom configuration
./backup-all-postgres-databases.sh \
-h db.example.com \
-U postgres \
-d /mnt/backups \
-r 60 \
-c
```
### Restore a Database
```bash
# Interactive restore (with confirmation)
./restore-postgres-database.sh /var/backups/postgresql/20260120/mydb_20260120_020001.sql.gz
# Force restore (skip confirmation)
./restore-postgres-database.sh backup.sql.gz -d mydb -f
```
---
## Detailed Usage
### Backup Script Options
```bash
./backup-all-postgres-databases.sh [options]
Options:
-h HOST Database host (default: localhost)
-p PORT Database port (default: 5432)
-U USER Database user (default: postgres)
-d BACKUP_DIR Backup directory (default: /var/backups/postgresql)
-r DAYS Retention days (default: 30)
-c Enable compression (gzip)
-v Verbose output
-H Show help
```
### Restore Script Options
```bash
./restore-postgres-database.sh <backup_file> [options]
Options:
-h HOST Database host (default: localhost)
-p PORT Database port (default: 5432)
-U USER Database user (default: postgres)
-d DBNAME Target database name (default: from filename)
-f Force restore (skip confirmation)
-v Verbose output
-H Show help
```
---
## Automated Backups with Cron
### Daily Backups (Recommended)
```bash
# Edit crontab
sudo crontab -e
# Add daily backup at 2 AM with compression
0 2 * * * /path/to/backup-all-postgres-databases.sh -c >> /var/log/postgres-backup.log 2>&1
```
### Alternative Schedules
```bash
# Every 6 hours
0 */6 * * * /path/to/backup-all-postgres-databases.sh -c
# Twice daily (2 AM and 2 PM)
0 2,14 * * * /path/to/backup-all-postgres-databases.sh -c
# Weekly on Sundays at 3 AM
0 3 * * 0 /path/to/backup-all-postgres-databases.sh -c -r 90
```
---
## Backup Directory Structure
```
/var/backups/postgresql/
├── 20260120/ # Date-based subdirectory
│ ├── globals_20260120_020001.sql.gz # Global objects backup
│ ├── basil_20260120_020001.sql.gz # Database backup
│ ├── myapp_20260120_020001.sql.gz # Database backup
│ └── wiki_20260120_020001.sql.gz # Database backup
├── 20260121/
│ ├── globals_20260121_020001.sql.gz
│ └── ...
└── 20260122/
└── ...
```
---
## Configuration Examples
### Local PostgreSQL Server
```bash
./backup-all-postgres-databases.sh \
-h localhost \
-U postgres \
-c
```
### Remote PostgreSQL Server
```bash
./backup-all-postgres-databases.sh \
-h db.example.com \
-p 5432 \
-U backup_user \
-d /mnt/network/backups \
-r 60 \
-c \
-v
```
### High-Frequency Backups
```bash
# Short retention for frequent backups
./backup-all-postgres-databases.sh \
-r 7 \
-c
```
---
## Authentication Setup
### Option 1: .pgpass File (Recommended)
Create `~/.pgpass` with connection credentials:
```bash
echo "localhost:5432:*:postgres:your-password" >> ~/.pgpass
chmod 600 ~/.pgpass
```
Format: `hostname:port:database:username:password`
### Option 2: Environment Variables
```bash
export PGPASSWORD="your-password"
./backup-all-postgres-databases.sh
```
### Option 3: Peer Authentication (Local Only)
Run as the postgres system user:
```bash
sudo -u postgres ./backup-all-postgres-databases.sh
```
---
## Monitoring and Notifications
### Email Notifications
Edit the scripts and uncomment the email notification section:
```bash
# In backup-all-postgres-databases.sh, uncomment:
if command -v mail &> /dev/null; then
echo "$summary" | mail -s "PostgreSQL Backup $status - $(hostname)" admin@example.com
fi
```
### Slack Notifications
Set webhook URL and uncomment:
```bash
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
# In script, uncomment:
if [ -n "$SLACK_WEBHOOK_URL" ]; then
curl -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "{\"text\":\"PostgreSQL Backup $status\n$summary\"}"
fi
```
### Log Rotation
Create `/etc/logrotate.d/postgres-backup`:
```
/var/log/postgres-backup.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
}
```
---
## Backup Verification
### Manual Verification
```bash
# List backups
ls -lh /var/backups/postgresql/$(date +%Y%m%d)/
# Verify compressed file integrity
gzip -t /var/backups/postgresql/20260120/basil_20260120_020001.sql.gz
# Preview backup contents
gunzip -c backup.sql.gz | head -50
```
### Test Restore (Recommended Monthly)
```bash
# Restore to a test database
./restore-postgres-database.sh backup.sql.gz -d test_restore -f
# Verify
psql -d test_restore -c "\dt"
# Cleanup
dropdb test_restore
```
---
## Disaster Recovery
### Full Server Restore
1. **Install PostgreSQL** on new server
2. **Restore global objects first**:
```bash
gunzip -c globals_YYYYMMDD_HHMMSS.sql.gz | psql -U postgres -d postgres
```
3. **Restore each database**:
```bash
./restore-postgres-database.sh basil_20260120_020001.sql.gz
./restore-postgres-database.sh myapp_20260120_020001.sql.gz
```
### Point-in-Time Recovery
For PITR capabilities, enable WAL archiving in `postgresql.conf`:
```
wal_level = replica
archive_mode = on
archive_command = 'cp %p /var/lib/postgresql/wal_archive/%f'
max_wal_senders = 3
```
Then use `pg_basebackup` and WAL replay for PITR.
---
## Troubleshooting
### Permission Denied
```bash
# Fix backup directory permissions
sudo chown -R postgres:postgres /var/backups/postgresql
sudo chmod 755 /var/backups/postgresql
# Fix script permissions
chmod +x backup-all-postgres-databases.sh
```
### Connection Failed
```bash
# Test connection manually
psql -h localhost -U postgres -c "SELECT version();"
# Check pg_hba.conf
sudo cat /etc/postgresql/*/main/pg_hba.conf
# Ensure proper authentication line exists:
# local all postgres peer
# host all all 127.0.0.1/32 scram-sha-256
```
### Out of Disk Space
```bash
# Check disk usage
df -h /var/backups
# Clean old backups manually
find /var/backups/postgresql -type d -name "????????" -mtime +30 -exec rm -rf {} \;
# Reduce retention period
./backup-all-postgres-databases.sh -r 7
```
### Backup File Corrupted
```bash
# Verify integrity
gzip -t backup.sql.gz
# If corrupted, use previous backup
ls -lt /var/backups/postgresql/*/basil_*.sql.gz | head
```
---
## Performance Optimization
### Large Databases
For very large databases, consider:
```bash
# Parallel dump (PostgreSQL 9.3+)
pg_dump -Fd -j 4 -f backup_directory mydb
# Custom format (smaller, faster restore)
pg_dump -Fc mydb > backup.custom
# Restore from custom format
pg_restore -d mydb backup.custom
```
### Network Backups
```bash
# Direct SSH backup (no local storage)
pg_dump mydb | gzip | ssh backup-server "cat > /backups/mydb.sql.gz"
```
---
## Best Practices
1. **Always test restores** - Backups are worthless if you can't restore
2. **Monitor backup completion** - Set up alerts for failed backups
3. **Use compression** - Saves 80-90% of disk space
4. **Multiple backup locations** - Local + remote/cloud storage
5. **Verify backup integrity** - Run gzip -t on compressed backups
6. **Document procedures** - Keep runbooks for disaster recovery
7. **Encrypt sensitive backups** - Use gpg for encryption if needed
8. **Regular retention review** - Adjust based on compliance requirements
---
## Security Considerations
### Encryption at Rest
```bash
# Encrypt backup with GPG
pg_dump mydb | gzip | gpg --encrypt --recipient admin@example.com > backup.sql.gz.gpg
# Decrypt for restore
gpg --decrypt backup.sql.gz.gpg | gunzip | psql mydb
```
### Secure Transfer
```bash
# Use SCP with key authentication
scp -i ~/.ssh/backup_key backup.sql.gz backup-server:/secure/backups/
# Or rsync over SSH
rsync -av -e "ssh -i ~/.ssh/backup_key" \
/var/backups/postgresql/ \
backup-server:/secure/backups/
```
### Access Control
```bash
# Restrict backup directory permissions
chmod 700 /var/backups/postgresql
chown postgres:postgres /var/backups/postgresql
# Restrict script permissions
chmod 750 backup-all-postgres-databases.sh
chown root:postgres backup-all-postgres-databases.sh
```
---
## Additional Resources
- [PostgreSQL Backup Documentation](https://www.postgresql.org/docs/current/backup.html)
- [pg_dump Manual](https://www.postgresql.org/docs/current/app-pgdump.html)
- [pg_restore Manual](https://www.postgresql.org/docs/current/app-pgrestore.html)
- [Continuous Archiving and PITR](https://www.postgresql.org/docs/current/continuous-archiving.html)
---
## Support
For issues or questions:
- Check script help: `./backup-all-postgres-databases.sh -H`
- Review logs: `tail -f /var/log/postgres-backup.log`
- Test connection: `psql -h localhost -U postgres`

View File

@@ -0,0 +1,402 @@
#!/bin/bash
#
# PostgreSQL All Databases Backup Script
# Backs up all databases on a PostgreSQL server using pg_dump
#
# Usage:
# ./backup-all-postgres-databases.sh [options]
#
# Options:
# -h HOST Database host (default: localhost)
# -p PORT Database port (default: 5432)
# -U USER Database user (default: postgres)
# -d BACKUP_DIR Backup directory (default: /var/backups/postgresql)
# -r DAYS Retention days (default: 30)
# -c Enable compression (gzip)
# -v Verbose output
#
# Cron example (daily at 2 AM):
# 0 2 * * * /path/to/backup-all-postgres-databases.sh -c >> /var/log/postgres-backup.log 2>&1
set -e
set -o pipefail
# Default configuration
DB_HOST="localhost"
DB_PORT="5432"
DB_USER="postgres"
BACKUP_DIR="/var/backups/postgresql"
RETENTION_DAYS=30
COMPRESS=false
VERBOSE=false
# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Parse command line arguments
while getopts "h:p:U:d:r:cvH" opt; do
case $opt in
h) DB_HOST="$OPTARG" ;;
p) DB_PORT="$OPTARG" ;;
U) DB_USER="$OPTARG" ;;
d) BACKUP_DIR="$OPTARG" ;;
r) RETENTION_DAYS="$OPTARG" ;;
c) COMPRESS=true ;;
v) VERBOSE=true ;;
H)
echo "PostgreSQL All Databases Backup Script"
echo ""
echo "Usage: $0 [options]"
echo ""
echo "Options:"
echo " -h HOST Database host (default: localhost)"
echo " -p PORT Database port (default: 5432)"
echo " -U USER Database user (default: postgres)"
echo " -d BACKUP_DIR Backup directory (default: /var/backups/postgresql)"
echo " -r DAYS Retention days (default: 30)"
echo " -c Enable compression (gzip)"
echo " -v Verbose output"
echo " -H Show this help"
echo ""
exit 0
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
esac
done
# Logging functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" >&2
}
log_debug() {
if [ "$VERBOSE" = true ]; then
echo -e "${BLUE}[DEBUG]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
fi
}
# Check dependencies
check_dependencies() {
log_debug "Checking dependencies..."
if ! command -v psql &> /dev/null; then
log_error "psql not found. Please install PostgreSQL client tools."
exit 1
fi
if ! command -v pg_dump &> /dev/null; then
log_error "pg_dump not found. Please install PostgreSQL client tools."
exit 1
fi
if [ "$COMPRESS" = true ] && ! command -v gzip &> /dev/null; then
log_error "gzip not found. Please install gzip or disable compression."
exit 1
fi
log_debug "All dependencies satisfied"
}
# Test database connection
test_connection() {
log_debug "Testing database connection..."
if ! psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -c "SELECT version();" &> /dev/null; then
log_error "Cannot connect to PostgreSQL server at $DB_HOST:$DB_PORT"
log_error "Check credentials, network connectivity, and pg_hba.conf settings"
exit 1
fi
log_debug "Database connection successful"
}
# Create backup directory structure
create_backup_dirs() {
local timestamp=$(date +%Y%m%d)
local backup_subdir="$BACKUP_DIR/$timestamp"
log_debug "Creating backup directory: $backup_subdir"
mkdir -p "$backup_subdir"
if [ ! -w "$backup_subdir" ]; then
log_error "Backup directory is not writable: $backup_subdir"
exit 1
fi
echo "$backup_subdir"
}
# Get list of databases to backup
get_databases() {
log_debug "Retrieving database list..."
# Get all databases except system databases
local databases=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -t -c \
"SELECT datname FROM pg_database
WHERE datname NOT IN ('postgres', 'template0', 'template1')
AND datistemplate = false
ORDER BY datname;")
if [ -z "$databases" ]; then
log_warn "No user databases found to backup"
return 1
fi
echo "$databases"
}
# Backup a single database
backup_database() {
local db_name="$1"
local backup_dir="$2"
local timestamp=$(date +%Y%m%d_%H%M%S)
local backup_file="$backup_dir/${db_name}_${timestamp}.sql"
log_info "Backing up database: $db_name"
# Add compression extension if enabled
if [ "$COMPRESS" = true ]; then
backup_file="${backup_file}.gz"
fi
# Perform backup
local start_time=$(date +%s)
if [ "$COMPRESS" = true ]; then
if pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$db_name" \
--no-owner --no-privileges --create --clean | gzip > "$backup_file"; then
local status="SUCCESS"
else
local status="FAILED"
fi
else
if pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$db_name" \
--no-owner --no-privileges --create --clean > "$backup_file"; then
local status="SUCCESS"
else
local status="FAILED"
fi
fi
local end_time=$(date +%s)
local duration=$((end_time - start_time))
if [ "$status" = "SUCCESS" ]; then
# Verify backup file exists and has content
if [ ! -s "$backup_file" ]; then
log_error "Backup file is empty: $backup_file"
return 1
fi
# Verify compressed file integrity if compression is enabled
if [ "$COMPRESS" = true ]; then
if ! gzip -t "$backup_file" 2>/dev/null; then
log_error "Backup file is corrupted: $backup_file"
return 1
fi
fi
local file_size=$(du -h "$backup_file" | cut -f1)
log_info "$db_name backup completed - Size: $file_size, Duration: ${duration}s"
log_debug " File: $backup_file"
return 0
else
log_error "$db_name backup failed"
# Remove failed backup file
rm -f "$backup_file"
return 1
fi
}
# Backup global objects (roles, tablespaces, etc.)
backup_globals() {
local backup_dir="$1"
local timestamp=$(date +%Y%m%d_%H%M%S)
local backup_file="$backup_dir/globals_${timestamp}.sql"
log_info "Backing up global objects (roles, tablespaces)..."
if [ "$COMPRESS" = true ]; then
backup_file="${backup_file}.gz"
fi
if [ "$COMPRESS" = true ]; then
if pg_dumpall -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" --globals-only | gzip > "$backup_file"; then
local status="SUCCESS"
else
local status="FAILED"
fi
else
if pg_dumpall -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" --globals-only > "$backup_file"; then
local status="SUCCESS"
else
local status="FAILED"
fi
fi
if [ "$status" = "SUCCESS" ]; then
local file_size=$(du -h "$backup_file" | cut -f1)
log_info "✓ Global objects backup completed - Size: $file_size"
return 0
else
log_error "✗ Global objects backup failed"
rm -f "$backup_file"
return 1
fi
}
# Clean up old backups
cleanup_old_backups() {
log_info "Cleaning up backups older than $RETENTION_DAYS days..."
local deleted_count=0
# Find and delete old backup directories
while IFS= read -r old_dir; do
log_debug "Deleting old backup directory: $old_dir"
rm -rf "$old_dir"
((deleted_count++))
done < <(find "$BACKUP_DIR" -maxdepth 1 -type d -name "????????" -mtime +$RETENTION_DAYS 2>/dev/null)
if [ $deleted_count -gt 0 ]; then
log_info "Deleted $deleted_count old backup directories"
else
log_debug "No old backups to delete"
fi
}
# Generate backup summary
generate_summary() {
local backup_dir="$1"
local total_dbs="$2"
local successful_dbs="$3"
local failed_dbs="$4"
local total_size=$(du -sh "$backup_dir" 2>/dev/null | cut -f1)
echo ""
log_info "================================================"
log_info "Backup Summary"
log_info "================================================"
log_info "Backup Directory: $backup_dir"
log_info "Total Databases: $total_dbs"
log_info "Successful: $successful_dbs"
log_info "Failed: $failed_dbs"
log_info "Total Size: $total_size"
log_info "Retention: $RETENTION_DAYS days"
log_info "Compression: $([ "$COMPRESS" = true ] && echo "Enabled" || echo "Disabled")"
log_info "================================================"
echo ""
}
# Send notification (optional)
send_notification() {
local status="$1"
local summary="$2"
# Uncomment and configure to enable email notifications
# if command -v mail &> /dev/null; then
# echo "$summary" | mail -s "PostgreSQL Backup $status - $(hostname)" your-email@example.com
# fi
# Uncomment and configure to enable Slack notifications
# if [ -n "$SLACK_WEBHOOK_URL" ]; then
# curl -X POST "$SLACK_WEBHOOK_URL" \
# -H 'Content-Type: application/json' \
# -d "{\"text\":\"PostgreSQL Backup $status\n$summary\"}"
# fi
}
# Main execution
main() {
local start_time=$(date +%s)
log_info "================================================"
log_info "PostgreSQL All Databases Backup Script"
log_info "================================================"
log_info "Host: $DB_HOST:$DB_PORT"
log_info "User: $DB_USER"
log_info "Backup Directory: $BACKUP_DIR"
log_info "Compression: $([ "$COMPRESS" = true ] && echo "Enabled" || echo "Disabled")"
log_info "Retention: $RETENTION_DAYS days"
log_info "================================================"
echo ""
# Perform checks
check_dependencies
test_connection
# Create backup directory
local backup_subdir=$(create_backup_dirs)
# Get list of databases
local databases=$(get_databases)
if [ -z "$databases" ]; then
log_warn "No databases to backup. Exiting."
exit 0
fi
# Backup global objects first
backup_globals "$backup_subdir"
# Backup each database
local total_dbs=0
local successful_dbs=0
local failed_dbs=0
while IFS= read -r db; do
# Trim whitespace
db=$(echo "$db" | xargs)
if [ -n "$db" ]; then
((total_dbs++))
if backup_database "$db" "$backup_subdir"; then
((successful_dbs++))
else
((failed_dbs++))
fi
fi
done <<< "$databases"
# Cleanup old backups
cleanup_old_backups
# Calculate total execution time
local end_time=$(date +%s)
local total_duration=$((end_time - start_time))
# Generate summary
generate_summary "$backup_subdir" "$total_dbs" "$successful_dbs" "$failed_dbs"
log_info "Total execution time: ${total_duration}s"
# Send notification
if [ $failed_dbs -gt 0 ]; then
send_notification "COMPLETED WITH ERRORS" "$(generate_summary "$backup_subdir" "$total_dbs" "$successful_dbs" "$failed_dbs")"
exit 1
else
send_notification "SUCCESS" "$(generate_summary "$backup_subdir" "$total_dbs" "$successful_dbs" "$failed_dbs")"
log_info "All backups completed successfully! ✓"
exit 0
fi
}
# Run main function
main

View File

@@ -0,0 +1,396 @@
#!/bin/bash
#
# PostgreSQL Database Restore Script
# Restores a single database from backup created by backup-all-postgres-databases.sh
#
# Usage:
# ./restore-postgres-database.sh <backup_file> [options]
#
# Options:
# -h HOST Database host (default: localhost)
# -p PORT Database port (default: 5432)
# -U USER Database user (default: postgres)
# -d DBNAME Target database name (default: extracted from backup filename)
# -f Force restore (skip confirmation)
# -v Verbose output
#
# Examples:
# ./restore-postgres-database.sh /var/backups/postgresql/20260120/mydb_20260120_020001.sql.gz
# ./restore-postgres-database.sh backup.sql -d mydb -f
set -e
set -o pipefail
# Default configuration
DB_HOST="localhost"
DB_PORT="5432"
DB_USER="postgres"
DB_NAME=""
FORCE=false
VERBOSE=false
# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" >&2
}
log_debug() {
if [ "$VERBOSE" = true ]; then
echo -e "${BLUE}[DEBUG]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
fi
}
# Show usage
show_usage() {
echo "PostgreSQL Database Restore Script"
echo ""
echo "Usage: $0 <backup_file> [options]"
echo ""
echo "Options:"
echo " -h HOST Database host (default: localhost)"
echo " -p PORT Database port (default: 5432)"
echo " -U USER Database user (default: postgres)"
echo " -d DBNAME Target database name (default: extracted from filename)"
echo " -f Force restore (skip confirmation)"
echo " -v Verbose output"
echo " -H Show this help"
echo ""
echo "Examples:"
echo " $0 /var/backups/postgresql/20260120/mydb_20260120_020001.sql.gz"
echo " $0 backup.sql -d mydb -f"
echo ""
}
# Extract database name from backup filename
extract_db_name() {
local filename=$(basename "$1")
# Remove extension(s) and timestamp
# Format: dbname_YYYYMMDD_HHMMSS.sql[.gz]
echo "$filename" | sed -E 's/_[0-9]{8}_[0-9]{6}\.sql(\.gz)?$//'
}
# Check if file is compressed
is_compressed() {
[[ "$1" == *.gz ]]
}
# Verify backup file
verify_backup() {
local backup_file="$1"
log_debug "Verifying backup file: $backup_file"
if [ ! -f "$backup_file" ]; then
log_error "Backup file not found: $backup_file"
exit 1
fi
if [ ! -r "$backup_file" ]; then
log_error "Backup file is not readable: $backup_file"
exit 1
fi
if [ ! -s "$backup_file" ]; then
log_error "Backup file is empty: $backup_file"
exit 1
fi
# Verify compressed file integrity
if is_compressed "$backup_file"; then
log_debug "Verifying gzip integrity..."
if ! gzip -t "$backup_file" 2>/dev/null; then
log_error "Backup file is corrupted (gzip test failed)"
exit 1
fi
fi
log_debug "Backup file verification passed"
}
# Test database connection
test_connection() {
log_debug "Testing database connection..."
if ! psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -c "SELECT version();" &> /dev/null; then
log_error "Cannot connect to PostgreSQL server at $DB_HOST:$DB_PORT"
log_error "Check credentials, network connectivity, and pg_hba.conf settings"
exit 1
fi
log_debug "Database connection successful"
}
# Check if database exists
database_exists() {
local db_name="$1"
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -t -c \
"SELECT 1 FROM pg_database WHERE datname='$db_name';" | grep -q 1
}
# Create safety backup
create_safety_backup() {
local db_name="$1"
local timestamp=$(date +%Y%m%d_%H%M%S)
local safety_file="/tmp/${db_name}_pre-restore_${timestamp}.sql.gz"
log_info "Creating safety backup before restore..."
if pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$db_name" | gzip > "$safety_file"; then
log_info "Safety backup created: $safety_file"
echo "$safety_file"
return 0
else
log_error "Failed to create safety backup"
return 1
fi
}
# Drop and recreate database
recreate_database() {
local db_name="$1"
log_info "Dropping and recreating database: $db_name"
# Terminate existing connections
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres <<EOF
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = '$db_name' AND pid <> pg_backend_pid();
EOF
# Drop and recreate
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres <<EOF
DROP DATABASE IF EXISTS $db_name;
CREATE DATABASE $db_name;
EOF
log_debug "Database recreated successfully"
}
# Restore database
restore_database() {
local backup_file="$1"
local db_name="$2"
log_info "Restoring database from: $backup_file"
local start_time=$(date +%s)
# Restore based on compression
if is_compressed "$backup_file"; then
if gunzip -c "$backup_file" | psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -v ON_ERROR_STOP=1; then
local status="SUCCESS"
else
local status="FAILED"
fi
else
if psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -f "$backup_file" -v ON_ERROR_STOP=1; then
local status="SUCCESS"
else
local status="FAILED"
fi
fi
local end_time=$(date +%s)
local duration=$((end_time - start_time))
if [ "$status" = "SUCCESS" ]; then
log_info "✓ Database restore completed in ${duration}s"
return 0
else
log_error "✗ Database restore failed"
return 1
fi
}
# Verify restore
verify_restore() {
local db_name="$1"
log_info "Verifying restored database..."
# Check if database exists
if ! database_exists "$db_name"; then
log_error "Database not found after restore: $db_name"
return 1
fi
# Get table count
local table_count=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$db_name" -t -c \
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';")
table_count=$(echo "$table_count" | xargs)
# Get row count estimate
local row_count=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$db_name" -t -c \
"SELECT SUM(n_live_tup) FROM pg_stat_user_tables;")
row_count=$(echo "$row_count" | xargs)
row_count=${row_count:-0}
log_info "Database: $db_name"
log_info "Tables: $table_count"
log_info "Approximate rows: $row_count"
return 0
}
# Parse command line arguments
BACKUP_FILE=""
while [[ $# -gt 0 ]]; do
case $1 in
-h)
DB_HOST="$2"
shift 2
;;
-p)
DB_PORT="$2"
shift 2
;;
-U)
DB_USER="$2"
shift 2
;;
-d)
DB_NAME="$2"
shift 2
;;
-f)
FORCE=true
shift
;;
-v)
VERBOSE=true
shift
;;
-H)
show_usage
exit 0
;;
-*)
log_error "Unknown option: $1"
show_usage
exit 1
;;
*)
if [ -z "$BACKUP_FILE" ]; then
BACKUP_FILE="$1"
else
log_error "Multiple backup files specified"
show_usage
exit 1
fi
shift
;;
esac
done
# Main execution
main() {
# Check if backup file was provided
if [ -z "$BACKUP_FILE" ]; then
log_error "No backup file specified"
show_usage
exit 1
fi
# Verify backup file
verify_backup "$BACKUP_FILE"
# Extract database name if not provided
if [ -z "$DB_NAME" ]; then
DB_NAME=$(extract_db_name "$BACKUP_FILE")
log_debug "Extracted database name: $DB_NAME"
fi
log_info "================================================"
log_info "PostgreSQL Database Restore"
log_info "================================================"
log_info "Backup File: $BACKUP_FILE"
log_info "Target Database: $DB_NAME"
log_info "Host: $DB_HOST:$DB_PORT"
log_info "User: $DB_USER"
log_info "================================================"
echo ""
# Test connection
test_connection
# Check if database exists
local db_exists=false
if database_exists "$DB_NAME"; then
db_exists=true
log_warn "Database '$DB_NAME' already exists and will be DROPPED"
fi
# Confirmation prompt (unless force flag is set)
if [ "$FORCE" != true ]; then
echo ""
echo -e "${RED}WARNING: This will destroy all current data in database: $DB_NAME${NC}"
echo ""
read -p "Are you sure you want to continue? (type 'yes' to confirm): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
log_info "Restore cancelled by user"
exit 0
fi
fi
# Create safety backup if database exists
local safety_file=""
if [ "$db_exists" = true ]; then
safety_file=$(create_safety_backup "$DB_NAME")
fi
# Recreate database
recreate_database "$DB_NAME"
# Restore from backup
if restore_database "$BACKUP_FILE" "$DB_NAME"; then
verify_restore "$DB_NAME"
echo ""
log_info "================================================"
log_info "Restore completed successfully! ✓"
log_info "================================================"
if [ -n "$safety_file" ]; then
echo ""
log_info "A safety backup was created before restore:"
log_info " $safety_file"
echo ""
log_info "To rollback to the previous state, run:"
log_info " $0 $safety_file -d $DB_NAME -f"
echo ""
fi
exit 0
else
log_error "Restore failed!"
if [ -n "$safety_file" ]; then
echo ""
log_warn "You can restore the previous state using:"
log_warn " $0 $safety_file -d $DB_NAME -f"
fi
exit 1
fi
}
# Run main function
main