403Webshell
Server IP : 3.147.158.171  /  Your IP : 216.73.216.216
Web Server : Apache/2.4.67 (Amazon Linux) OpenSSL/3.5.5
System : Linux ip-172-31-2-178.us-east-2.compute.internal 6.1.172-216.329.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Wed May 20 06:31:34 UTC 2026 x86_64
User : ec2-user ( 1000)
PHP Version : 8.4.21
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /tsai/repo/builder/backend/scripts/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /tsai/repo/builder/backend/scripts/db_restore.py
import subprocess
import sys
import os
import re
import argparse
import gzip
from datetime import datetime
from pathlib import Path
from dotenv import load_dotenv, find_dotenv

# Load environment variables from .env file
if not find_dotenv():
    print("Error: .env file not found.")
    sys.exit(1)
load_dotenv()

MYSQL_HOST = os.getenv('MYSQL_HOST')
MYSQL_USER = os.getenv('MYSQL_USER')
MYSQL_PASSWORD = os.getenv('MYSQL_PASSWORD')
BACKUP_DIR = os.getenv('BACKUP_DIR')

# Check if any required environment variable is missing
missing = [name for name, var in [
    ('MYSQL_HOST', MYSQL_HOST),
    ('MYSQL_USER', MYSQL_USER),
    ('MYSQL_PASSWORD', MYSQL_PASSWORD),
    ('BACKUP_DIR', BACKUP_DIR)
] if var is None]
if missing:
    print(f"Error: Missing environment variables: {', '.join(missing)}")
    sys.exit(1)

# Type assertions for type checkers
assert MYSQL_HOST is not None
assert MYSQL_USER is not None
assert MYSQL_PASSWORD is not None
assert BACKUP_DIR is not None


def find_backups(database, backup_dir):
    """Find all backup files for the specified database."""
    if not os.path.isdir(backup_dir):
        print(f"Error: Backup directory does not exist: {backup_dir}")
        return []

    # Sanitize database name same way backup script does
    safe_db_name = database.replace('/', '_').replace('\\', '_').replace(' ', '_')

    # Match exact db name followed by timestamp: {name}_YYYY-MM-DD_HHMMSS.sql[.gz]
    # Prevents e.g. "scike" matching "scike_guitar_*" backups.
    pattern = re.compile(rf"^{re.escape(safe_db_name)}_\d{{4}}-\d{{2}}-\d{{2}}_\d{{6}}\.sql(\.gz)?$")
    backups = []
    for file in os.listdir(backup_dir):
        if pattern.match(file):
            file_path = os.path.join(backup_dir, file)
            # Extract timestamp from filename (format: dbname_YYYY-MM-DD_HHMMSS.sql[.gz])
            try:
                # Get file stats
                stat = os.stat(file_path)
                backups.append({
                    'path': file_path,
                    'filename': file,
                    'size': stat.st_size,
                    'mtime': stat.st_mtime
                })
            except Exception as e:
                print(f"Warning: Could not read file {file}: {e}")
                continue

    # Sort by modification time (newest first)
    backups.sort(key=lambda x: x['mtime'], reverse=True)
    return backups


def format_size(size_bytes):
    """Format file size in human-readable format."""
    if size_bytes < 1024:
        return f"{size_bytes} B"
    elif size_bytes < 1024 * 1024:
        return f"{size_bytes / 1024:.2f} KB"
    else:
        return f"{size_bytes / (1024 * 1024):.2f} MB"


def format_age(mtime):
    """Format file age in human-readable format."""
    age_seconds = datetime.now().timestamp() - mtime
    age_hours = age_seconds / 3600

    if age_hours < 1:
        return f"{int(age_seconds / 60)} minutes ago"
    elif age_hours < 24:
        return f"{int(age_hours)} hours ago"
    else:
        age_days = int(age_hours / 24)
        return f"{age_days} day{'s' if age_days > 1 else ''} ago"


def display_backup_options(backups, limit=5):
    """Display backup options and return user selection."""
    print("\n" + "=" * 70)
    print(f"Found {len(backups)} backup(s)")
    print("=" * 70)

    display_count = min(limit, len(backups))
    for i, backup in enumerate(backups[:display_count], 1):
        timestamp = datetime.fromtimestamp(backup['mtime']).strftime('%Y-%m-%d %H:%M:%S')
        size = format_size(backup['size'])
        age = format_age(backup['mtime'])
        compressed = " [compressed]" if backup['filename'].endswith('.gz') else ""
        print(f"{i}. {backup['filename']}")
        print(f"   Date: {timestamp} ({age})")
        print(f"   Size: {size}{compressed}")
        print()

    if len(backups) > display_count:
        print(f"... and {len(backups) - display_count} more backup(s)\n")

    print("=" * 70)
    print(f"Press Enter to use most recent backup, or enter 1-{display_count}")
    if len(backups) > display_count:
        print(f"Enter 'all' to see all backups, or 'custom' to specify a file path")
    else:
        print("Enter 'custom' to specify a custom file path")
    print("=" * 70)

    choice = input("\nYour choice [Enter for most recent]: ").strip().lower()

    if choice == '':
        return backups[0]['path']
    elif choice == 'all':
        return display_backup_options(backups, limit=len(backups))
    elif choice == 'custom':
        custom_path = input("Enter full path to backup file: ").strip()
        if not os.path.isfile(custom_path):
            print(f"Error: File not found: {custom_path}")
            sys.exit(1)
        return custom_path
    else:
        try:
            index = int(choice) - 1
            if 0 <= index < display_count:
                return backups[index]['path']
            else:
                print(f"Error: Invalid choice. Please enter 1-{display_count}")
                sys.exit(1)
        except ValueError:
            print("Error: Invalid input. Please enter a number or 'custom'")
            sys.exit(1)


def confirm_restore(db_name, file_path):
    """Ask user to confirm before dropping and restoring database."""
    file_size = format_size(os.path.getsize(file_path))
    file_time = datetime.fromtimestamp(os.path.getmtime(file_path)).strftime('%Y-%m-%d %H:%M:%S')

    print("\n" + "!" * 70)
    print("WARNING: This will DROP the existing database and restore from backup")
    print("!" * 70)
    print(f"Database: {db_name}")
    print(f"Backup file: {os.path.basename(file_path)}")
    print(f"Backup date: {file_time}")
    print(f"File size: {file_size}")
    print("!" * 70)

    response = input("\nType 'yes' to continue or anything else to cancel: ").strip().lower()
    if response != 'yes':
        print("Restore cancelled by user.")
        sys.exit(0)


def drop_database(db_name):
    """Drop the database if it exists."""
    env = os.environ.copy()
    env['MYSQL_PWD'] = MYSQL_PASSWORD

    try:
        drop_db_command = [
            'mysql',
            f'--host={MYSQL_HOST}',
            f'--user={MYSQL_USER}',
            '-e',
            f'DROP DATABASE IF EXISTS {db_name};'
        ]
        subprocess.run(drop_db_command, check=True, env=env,
                      stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
        print(f"✓ Database '{db_name}' dropped successfully")
    except subprocess.CalledProcessError as e:
        print(f"Error dropping database: {e}")
        if e.stderr:
            print(f"MySQL error: {e.stderr}")
        sys.exit(1)


def create_database(db_name):
    """Create a new empty database."""
    env = os.environ.copy()
    env['MYSQL_PWD'] = MYSQL_PASSWORD

    try:
        create_db_command = [
            'mysql',
            f'--host={MYSQL_HOST}',
            f'--user={MYSQL_USER}',
            '-e',
            f'CREATE DATABASE {db_name};'
        ]
        subprocess.run(create_db_command, check=True, env=env,
                      stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
        print(f"✓ Database '{db_name}' created successfully")
    except subprocess.CalledProcessError as e:
        print(f"Error creating database: {e}")
        if e.stderr:
            print(f"MySQL error: {e.stderr}")
        sys.exit(1)


def load_database_from_file(db_name, file_path):
    """Load database from SQL file (handles both .sql and .sql.gz files)."""
    env = os.environ.copy()
    env['MYSQL_PWD'] = MYSQL_PASSWORD

    is_compressed = file_path.endswith('.gz')

    try:
        print(f"✓ Loading database from backup {('(decompressing)' if is_compressed else '')}...")
        start_time = datetime.now()

        if is_compressed:
            # Use gunzip to decompress and pipe to mysql
            # This is more reliable than Python's gzip module with subprocess
            gunzip_command = ['gunzip', '-c', file_path]
            mysql_command = [
                'mysql',
                '--binary-mode',
                f'--host={MYSQL_HOST}',
                f'--user={MYSQL_USER}',
                db_name
            ]

            # Start gunzip process
            gunzip_process = subprocess.Popen(gunzip_command, stdout=subprocess.PIPE,
                                              stderr=subprocess.PIPE, env=env)

            # Pipe gunzip output to mysql
            mysql_process = subprocess.Popen(mysql_command, stdin=gunzip_process.stdout,
                                            stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)

            # Close gunzip stdout in parent to allow gunzip to receive SIGPIPE if mysql exits
            gunzip_process.stdout.close()

            # Wait for mysql to complete and get output
            mysql_stdout, mysql_stderr = mysql_process.communicate()

            # Check for errors
            if mysql_process.returncode != 0:
                error_msg = mysql_stderr.decode('utf-8', errors='replace') if mysql_stderr else 'Unknown error'
                raise subprocess.CalledProcessError(mysql_process.returncode, mysql_command,
                                                   output=mysql_stdout, stderr=mysql_stderr)

        else:
            # Read regular SQL file and pipe to mysql
            mysql_command = [
                'mysql',
                '--binary-mode',
                f'--host={MYSQL_HOST}',
                f'--user={MYSQL_USER}',
                db_name
            ]

            with open(file_path, 'rb') as f:
                result = subprocess.run(mysql_command, stdin=f, check=True, env=env,
                                      stderr=subprocess.PIPE, stdout=subprocess.PIPE)

        elapsed = (datetime.now() - start_time).total_seconds()
        print(f"✓ Database loaded successfully in {elapsed:.2f} seconds")

    except subprocess.CalledProcessError as e:
        print(f"Error loading database from file: {e}")
        if e.stderr:
            # Decode stderr for display (mysql error messages are usually UTF-8)
            error_msg = e.stderr.decode('utf-8', errors='replace') if isinstance(e.stderr, bytes) else e.stderr
            print(f"MySQL error: {error_msg}")
        sys.exit(1)
    except Exception as e:
        print(f"Unexpected error loading database: {e}")
        sys.exit(1)


def main():
    # Parse arguments
    parser = argparse.ArgumentParser(
        description='Restore a MySQL database from backup file.',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Interactive: choose from recent backups
  python db_restore.py mydatabase

  # Use specific backup file
  python db_restore.py mydatabase --file /path/to/backup.sql.gz

  # Non-interactive: use most recent backup without prompting
  python db_restore.py mydatabase --yes

  # Restore from a different backup directory
  python db_restore.py mydatabase --backup-dir /path/to/backups --yes
        """)
    parser.add_argument('database', type=str,
                        help='Name of the database to restore')
    parser.add_argument('--file', type=str, dest='sqlfile',
                        help='Path to specific SQL file (optional, will auto-discover if not provided)')
    parser.add_argument('--backup-dir', type=str,
                        help='Override backup directory for finding backups (default: BACKUP_DIR from .env)')
    parser.add_argument('--yes', '-y', action='store_true',
                        help='Auto-confirm restore (use most recent backup without prompting)')
    args = parser.parse_args()

    db_name = args.database

    # Use command-line backup dir if provided, otherwise use env var
    backup_dir = args.backup_dir if args.backup_dir else BACKUP_DIR

    # Determine which backup file to use
    if args.sqlfile:
        # User specified a file
        sql_file_path = args.sqlfile
        if not os.path.isfile(sql_file_path):
            print(f"Error: File not found: {sql_file_path}")
            sys.exit(1)
    else:
        # Auto-discover backups
        print(f"Searching for backups of '{db_name}' in {backup_dir}...")
        backups = find_backups(db_name, backup_dir)

        if not backups:
            print(f"Error: No backups found for database '{db_name}' in {backup_dir}")
            sys.exit(1)

        if args.yes:
            # Non-interactive mode: use most recent
            sql_file_path = backups[0]['path']
            print(f"Using most recent backup: {os.path.basename(sql_file_path)}")
        else:
            # Interactive mode: let user choose
            sql_file_path = display_backup_options(backups)

    # Confirm before proceeding (unless --yes flag)
    if not args.yes:
        confirm_restore(db_name, sql_file_path)

    # Perform restore
    print("\nStarting restore process...")
    print("-" * 70)
    drop_database(db_name)
    create_database(db_name)
    load_database_from_file(db_name, sql_file_path)

    # Success summary
    print("-" * 70)
    print("\n" + "=" * 70)
    print("RESTORE COMPLETED SUCCESSFULLY")
    print("=" * 70)
    print(f"Database: {db_name}")
    print(f"Restored from: {os.path.basename(sql_file_path)}")
    print("=" * 70)


if __name__ == "__main__":
    main()

Youez - 2016 - github.com/yon3zu
LinuXploit