""" Database restore script from R2 storage Downloads compressed backup from R2 and restores to PostgreSQL """ import os import subprocess import gzip from pathlib import Path import boto3 from botocore.config import Config from dotenv import load_dotenv # Load environment variables load_dotenv() # R2 Configuration R2_ENDPOINT = os.getenv("R2_ENDPOINT") R2_ACCESS_KEY = os.getenv("R2_ACCESS_KEY") R2_SECRET_KEY = os.getenv("R2_SECRET_KEY") R2_BUCKET = os.getenv("R2_BUCKET") # Database Configuration DB_HOST = os.getenv("DB_HOST", "localhost") DB_PORT = os.getenv("DB_PORT", "5432") DB_NAME = os.getenv("DB_NAME", "recipes_db") DB_USER = os.getenv("DB_USER", "recipes_user") DB_PASSWORD = os.getenv("DB_PASSWORD", "recipes_password") # Restore directory RESTORE_DIR = Path(__file__).parent / "restores" RESTORE_DIR.mkdir(exist_ok=True) def list_r2_backups(): """List all available backups in R2""" s3_client = boto3.client( 's3', endpoint_url=R2_ENDPOINT, aws_access_key_id=R2_ACCESS_KEY, aws_secret_access_key=R2_SECRET_KEY, config=Config(signature_version='s3v4'), region_name='auto' ) try: response = s3_client.list_objects_v2(Bucket=R2_BUCKET) if 'Contents' not in response: return [] backups = sorted(response['Contents'], key=lambda x: x['LastModified'], reverse=True) return backups except Exception as e: print(f"✗ Error listing backups: {e}") return [] def download_from_r2(backup_name): """Download backup file from R2""" local_file = RESTORE_DIR / backup_name print(f"Downloading {backup_name} from R2...") s3_client = boto3.client( 's3', endpoint_url=R2_ENDPOINT, aws_access_key_id=R2_ACCESS_KEY, aws_secret_access_key=R2_SECRET_KEY, config=Config(signature_version='s3v4'), region_name='auto' ) try: s3_client.download_file(R2_BUCKET, backup_name, str(local_file)) size_mb = local_file.stat().st_size / (1024 * 1024) print(f"✓ Downloaded: {local_file.name} ({size_mb:.2f} MB)") return local_file except Exception as e: print(f"✗ Error downloading from R2: {e}") raise def decompress_file(compressed_file): """Decompress gzip file""" decompressed_file = Path(str(compressed_file).replace('.gz', '')) print(f"Decompressing {compressed_file.name}...") with gzip.open(compressed_file, 'rb') as f_in: with open(decompressed_file, 'wb') as f_out: f_out.write(f_in.read()) compressed_size = compressed_file.stat().st_size decompressed_size = decompressed_file.stat().st_size print(f"✓ Decompressed to {decompressed_file.name}") print(f" Compressed: {compressed_size / 1024:.2f} KB") print(f" Decompressed: {decompressed_size / 1024:.2f} KB") return decompressed_file def restore_database(sql_file): """Restore PostgreSQL database from SQL file""" print(f"\nRestoring database from {sql_file.name}...") print("WARNING: This will overwrite the current database!") response = input("Are you sure you want to continue? (yes/no): ") if response.lower() != 'yes': print("Restore cancelled") return False # Set PGPASSWORD environment variable env = os.environ.copy() env['PGPASSWORD'] = DB_PASSWORD # Drop and recreate database (optional, comment out if you want to merge) print("Dropping existing tables...") drop_cmd = [ "psql", "-h", DB_HOST, "-p", DB_PORT, "-U", DB_USER, "-d", DB_NAME, "-c", "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" ] try: subprocess.run(drop_cmd, env=env, check=True, capture_output=True, text=True) except subprocess.CalledProcessError as e: print(f"Warning: Could not drop schema: {e.stderr}") # Restore from backup print("Restoring database...") restore_cmd = [ "psql", "-h", DB_HOST, "-p", DB_PORT, "-U", DB_USER, "-d", DB_NAME, "-f", str(sql_file) ] try: subprocess.run(restore_cmd, env=env, check=True, capture_output=True, text=True) print("✓ Database restored successfully!") return True except subprocess.CalledProcessError as e: print(f"✗ Error restoring database: {e.stderr}") raise def main(): """Main restore process""" print("=" * 60) print("Database Restore from Cloudflare R2") print("=" * 60) print() try: # Verify R2 credentials if not all([R2_ENDPOINT, R2_ACCESS_KEY, R2_SECRET_KEY, R2_BUCKET]): raise ValueError("Missing R2 credentials in environment variables") # List available backups print("Available backups:") backups = list_r2_backups() if not backups: print("No backups found in R2") return for i, backup in enumerate(backups, 1): size_mb = backup['Size'] / (1024 * 1024) print(f"{i}. {backup['Key']}") print(f" Size: {size_mb:.2f} MB, Date: {backup['LastModified']}") print() # Select backup choice = input(f"Select backup to restore (1-{len(backups)}) or 'q' to quit: ") if choice.lower() == 'q': print("Restore cancelled") return try: backup_index = int(choice) - 1 if backup_index < 0 or backup_index >= len(backups): raise ValueError() except ValueError: print("Invalid selection") return selected_backup = backups[backup_index]['Key'] # Download backup compressed_file = download_from_r2(selected_backup) # Decompress backup sql_file = decompress_file(compressed_file) # Restore database restore_database(sql_file) print("\n" + "=" * 60) print("✓ Restore completed successfully!") print("=" * 60) except Exception as e: print("\n" + "=" * 60) print(f"✗ Restore failed: {e}") print("=" * 60) raise if __name__ == "__main__": main()