NOXSHELL
Server: nginx/1.26.3
System: Linux vultr 6.8.0-54-generic #56-Ubuntu SMP PREEMPT_DYNAMIC Sat Feb 8 00:37:57 UTC 2025 x86_64
User: gisha-group (1019)
PHP: 8.0.30
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: //home/gisha985666/htdocs/BackupSystem/wp_restore.py
#!/usr/bin/env python3

import os
import sys
import zipfile
import shutil
import subprocess
import logging
from pathlib import Path
import configparser
from datetime import datetime

try:
    from googleapiclient.discovery import build
    from googleapiclient.http import MediaIoBaseDownload
    from google.oauth2.service_account import Credentials
except ImportError:
    print("Error: Google API client libraries not installed.")
    print("Run: pip install google-api-python-client google-auth")
    sys.exit(1)

class WordPressRestore:
    def __init__(self, config_file='config.ini'):
        self.config = configparser.ConfigParser()
        self.config.read(config_file)

        # Setup logging
        log_level = self.config.get('general', 'log_level', fallback='INFO')
        logging.basicConfig(
            level=getattr(logging, log_level),
            format='%(asctime)s - %(levelname)s - %(message)s',
            handlers=[
                logging.FileHandler('wp_restore.log'),
                logging.StreamHandler()
            ]
        )
        self.logger = logging.getLogger(__name__)

        # Configuration
        self.wp_path = self.config.get('wordpress', 'wp_path')
        self.backup_dir = self.config.get('general', 'backup_dir', fallback='/tmp/wp_backups')

        # Database config
        self.db_host = self.config.get('database', 'host')
        self.db_name = self.config.get('database', 'name')
        self.db_user = self.config.get('database', 'user')
        self.db_password = self.config.get('database', 'password')

        # Google Drive config
        self.credentials_file = self.config.get('google_drive', 'credentials_file')
        self.drive_folder_id = self.config.get('google_drive', 'folder_id', fallback=None)

        # Ensure backup directory exists
        Path(self.backup_dir).mkdir(parents=True, exist_ok=True)

        # Initialize Google Drive service
        self.drive_service = self._init_drive_service()

    def _init_drive_service(self):
        """Initialize Google Drive API service"""
        try:
            self.logger.info("Initializing Google Drive connection...")
            credentials = Credentials.from_service_account_file(
                self.credentials_file,
                scopes=['https://www.googleapis.com/auth/drive.file']
            )
            service = build('drive', 'v3', credentials=credentials)
            self.logger.info("✓ Connected to Google Drive successfully")
            return service
        except Exception as e:
            self.logger.error(f"✗ Failed to initialize Google Drive service: {e}")
            return None

    def list_backups(self):
        """List all backup files from Google Drive"""
        if not self.drive_service:
            self.logger.error("✗ Google Drive service not initialized")
            return None

        try:
            self.logger.info("Fetching backup list from Google Drive...")

            query = f"'{self.drive_folder_id}' in parents and trashed=false"

            results = self.drive_service.files().list(
                q=query,
                fields="files(id, name, createdTime, size)",
                orderBy="createdTime desc",
                supportsAllDrives=True,
                includeItemsFromAllDrives=True
            ).execute()

            files = results.get('files', [])

            if not files:
                self.logger.info("No backup files found in Google Drive")
                return []

            # Separate files and database backups
            file_backups = []
            db_backups = []

            for f in files:
                if f['name'].startswith('wp_files_backup_'):
                    file_backups.append(f)
                elif f['name'].startswith('wp_db_backup_'):
                    db_backups.append(f)

            self.logger.info(f"✓ Found {len(file_backups)} file backups and {len(db_backups)} database backups")

            return {'files': file_backups, 'databases': db_backups}

        except Exception as e:
            self.logger.error(f"✗ Failed to list backups: {e}")
            return None

    def download_backup(self, file_id, file_name):
        """Download backup file from Google Drive"""
        if not self.drive_service:
            self.logger.error("✗ Google Drive service not initialized")
            return None

        try:
            self.logger.info(f"Downloading {file_name}...")

            request = self.drive_service.files().get_media(
                fileId=file_id,
                supportsAllDrives=True
            )

            file_path = os.path.join(self.backup_dir, file_name)

            with open(file_path, 'wb') as f:
                downloader = MediaIoBaseDownload(f, request)
                done = False
                while not done:
                    status, done = downloader.next_chunk()
                    if status:
                        progress = int(status.progress() * 100)
                        self.logger.info(f"  Download progress: {progress}%")

            self.logger.info(f"✓ Downloaded successfully to {file_path}")
            return file_path

        except Exception as e:
            self.logger.error(f"✗ Failed to download file: {e}")
            return None

    def delete_wordpress_files(self):
        """Delete all files in WordPress directory"""
        self.logger.warning(f"⚠ DELETING all files in {self.wp_path}")

        try:
            if not os.path.exists(self.wp_path):
                self.logger.info(f"Directory {self.wp_path} does not exist, creating it...")
                os.makedirs(self.wp_path)
                return True

            # Count files before deletion
            file_count = sum(len(files) for _, _, files in os.walk(self.wp_path))
            self.logger.info(f"Found {file_count} files to delete")

            for item in os.listdir(self.wp_path):
                item_path = os.path.join(self.wp_path, item)
                if os.path.isfile(item_path):
                    os.remove(item_path)
                elif os.path.isdir(item_path):
                    shutil.rmtree(item_path)

            self.logger.info(f"✓ Successfully deleted all files from {self.wp_path}")
            return True

        except Exception as e:
            self.logger.error(f"✗ Failed to delete WordPress files: {e}")
            return False

    def restore_wordpress_files(self, backup_file):
        """Extract WordPress files from backup"""
        self.logger.info(f"Restoring WordPress files from {backup_file}...")

        try:
            self.logger.info("Extracting backup archive...")

            with zipfile.ZipFile(backup_file, 'r') as zip_ref:
                file_list = zip_ref.namelist()
                total_files = len(file_list)
                self.logger.info(f"  Archive contains {total_files} files")

                for i, file in enumerate(file_list, 1):
                    zip_ref.extract(file, self.wp_path)
                    if i % 100 == 0 or i == total_files:
                        progress = int((i / total_files) * 100)
                        self.logger.info(f"  Extraction progress: {progress}% ({i}/{total_files} files)")

            self.logger.info(f"✓ Successfully restored {total_files} files to {self.wp_path}")

            # Fix file ownership
            self.fix_file_ownership()

            return True

        except Exception as e:
            self.logger.error(f"✗ Failed to restore WordPress files: {e}")
            return False

    def fix_file_ownership(self):
        """Fix file ownership after restore"""
        self.logger.info("Fixing file ownership...")

        try:
            # Get the owner of the parent directory
            parent_dir = os.path.dirname(self.wp_path)
            stat_info = os.stat(parent_dir)
            uid = stat_info.st_uid
            gid = stat_info.st_gid

            # Get username from uid
            import pwd
            import grp
            username = pwd.getpwuid(uid).pw_name
            groupname = grp.getgrgid(gid).gr_name

            self.logger.info(f"  Setting ownership to {username}:{groupname}")

            # Change ownership recursively
            cmd = ['chown', '-R', f'{username}:{groupname}', self.wp_path]
            result = subprocess.run(cmd, capture_output=True, text=True)

            if result.returncode == 0:
                self.logger.info(f"✓ File ownership fixed: {username}:{groupname}")
                return True
            else:
                self.logger.warning(f"⚠ Failed to change ownership: {result.stderr}")
                return False

        except Exception as e:
            self.logger.warning(f"⚠ Could not fix file ownership: {e}")
            self.logger.info(f"  You may need to manually run: chown -R user:group {self.wp_path}")
            return False

    def database_exists(self):
        """Check if database exists"""
        try:
            cmd = [
                'mysql',
                f'--host={self.db_host}',
                f'--user={self.db_user}',
                f'--password={self.db_password}',
                '-e',
                f"SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '{self.db_name}'"
            ]

            result = subprocess.run(cmd, capture_output=True, text=True, check=True)
            return self.db_name in result.stdout

        except subprocess.CalledProcessError:
            return False

    def create_database(self):
        """Create database if it doesn't exist"""
        self.logger.info(f"Creating database '{self.db_name}'...")

        try:
            cmd = [
                'mysql',
                f'--host={self.db_host}',
                f'--user={self.db_user}',
                f'--password={self.db_password}',
                '-e',
                f"CREATE DATABASE IF NOT EXISTS `{self.db_name}` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
            ]

            subprocess.run(cmd, check=True, capture_output=True)
            self.logger.info(f"✓ Database '{self.db_name}' created successfully")
            return True

        except subprocess.CalledProcessError as e:
            self.logger.error(f"✗ Failed to create database: {e}")
            return False

    def restore_database(self, backup_file):
        """Restore database from backup"""
        self.logger.info(f"Restoring database from {backup_file}...")

        try:
            # Extract SQL file from zip
            self.logger.info("Extracting database backup archive...")

            with zipfile.ZipFile(backup_file, 'r') as zip_ref:
                sql_files = [f for f in zip_ref.namelist() if f.endswith('.sql')]

                if not sql_files:
                    self.logger.error("✗ No SQL file found in backup archive")
                    return False

                sql_file = sql_files[0]
                self.logger.info(f"  Found SQL file: {sql_file}")

                extract_path = os.path.join(self.backup_dir, sql_file)
                zip_ref.extract(sql_file, self.backup_dir)
                self.logger.info(f"✓ Extracted to {extract_path}")

            # Check if database exists, create if not
            if not self.database_exists():
                self.logger.warning(f"⚠ Database '{self.db_name}' does not exist")
                if not self.create_database():
                    return False
            else:
                self.logger.info(f"✓ Database '{self.db_name}' exists")

            # Drop all tables in database
            self.logger.info("Dropping all existing tables...")
            try:
                cmd = [
                    'mysql',
                    f'--host={self.db_host}',
                    f'--user={self.db_user}',
                    f'--password={self.db_password}',
                    self.db_name,
                    '-e',
                    "SET FOREIGN_KEY_CHECKS = 0; "
                    "SET @tables = NULL; "
                    "SELECT GROUP_CONCAT(table_name) INTO @tables FROM information_schema.tables WHERE table_schema = DATABASE(); "
                    "SET @tables = CONCAT('DROP TABLE IF EXISTS ', @tables); "
                    "PREPARE stmt FROM @tables; "
                    "EXECUTE stmt; "
                    "DEALLOCATE PREPARE stmt; "
                    "SET FOREIGN_KEY_CHECKS = 1;"
                ]
                subprocess.run(cmd, check=True, capture_output=True)
                self.logger.info("✓ Dropped all existing tables")
            except subprocess.CalledProcessError as e:
                self.logger.warning(f"⚠ Warning during table drop: {e}")

            # Restore database
            self.logger.info("Importing database backup...")

            with open(extract_path, 'r') as f:
                cmd = [
                    'mysql',
                    f'--host={self.db_host}',
                    f'--user={self.db_user}',
                    f'--password={self.db_password}',
                    self.db_name
                ]

                subprocess.run(cmd, stdin=f, check=True, capture_output=True)

            # Clean up extracted SQL file
            os.remove(extract_path)
            self.logger.info(f"✓ Database restored successfully to '{self.db_name}'")
            return True

        except subprocess.CalledProcessError as e:
            self.logger.error(f"✗ Failed to restore database: {e}")
            return False
        except Exception as e:
            self.logger.error(f"✗ Unexpected error during database restore: {e}")
            return False

    def cleanup_downloaded_files(self, files):
        """Remove downloaded backup files"""
        self.logger.info("Cleaning up downloaded files...")
        for file_path in files:
            try:
                if os.path.exists(file_path):
                    os.remove(file_path)
                    self.logger.info(f"  ✓ Removed {file_path}")
            except Exception as e:
                self.logger.warning(f"  ⚠ Failed to remove {file_path}: {e}")

    def run_restore(self):
        """Run complete restore process"""
        self.logger.info("=" * 60)
        self.logger.info("WORDPRESS RESTORE PROCESS STARTED")
        self.logger.info("=" * 60)

        # List available backups
        backups = self.list_backups()

        if not backups or (not backups['files'] and not backups['databases']):
            self.logger.error("✗ No backups found. Exiting.")
            return False

        # Display file backups
        print("\n" + "=" * 60)
        print("AVAILABLE FILE BACKUPS:")
        print("=" * 60)

        for i, backup in enumerate(backups['files'], 1):
            size_mb = int(backup['size']) / (1024 * 1024)
            created = backup['createdTime'][:19].replace('T', ' ')
            print(f"{i}. {backup['name']}")
            print(f"   Size: {size_mb:.2f} MB | Created: {created}")

        # Display database backups
        print("\n" + "=" * 60)
        print("AVAILABLE DATABASE BACKUPS:")
        print("=" * 60)

        for i, backup in enumerate(backups['databases'], 1):
            size_mb = int(backup['size']) / (1024 * 1024)
            created = backup['createdTime'][:19].replace('T', ' ')
            print(f"{i}. {backup['name']}")
            print(f"   Size: {size_mb:.2f} MB | Created: {created}")

        # Get user selection for file backup
        print("\n" + "=" * 60)
        file_choice = input("Enter the FILE backup number to restore (or 0 to skip): ").strip()

        selected_file_backup = None
        if file_choice != '0':
            try:
                idx = int(file_choice) - 1
                if 0 <= idx < len(backups['files']):
                    selected_file_backup = backups['files'][idx]
                else:
                    self.logger.error("✗ Invalid file backup selection")
                    return False
            except ValueError:
                self.logger.error("✗ Invalid input")
                return False

        # Get user selection for database backup
        db_choice = input("Enter the DATABASE backup number to restore (or 0 to skip): ").strip()

        selected_db_backup = None
        if db_choice != '0':
            try:
                idx = int(db_choice) - 1
                if 0 <= idx < len(backups['databases']):
                    selected_db_backup = backups['databases'][idx]
                else:
                    self.logger.error("✗ Invalid database backup selection")
                    return False
            except ValueError:
                self.logger.error("✗ Invalid input")
                return False

        if not selected_file_backup and not selected_db_backup:
            self.logger.warning("⚠ No backups selected. Exiting.")
            return False

        # Confirm restore
        print("\n" + "=" * 60)
        print("⚠  WARNING: This will DELETE and REPLACE existing data!")
        print("=" * 60)
        if selected_file_backup:
            print(f"File backup: {selected_file_backup['name']}")
            print(f"Restore to: {self.wp_path}")
        if selected_db_backup:
            print(f"Database backup: {selected_db_backup['name']}")
            print(f"Restore to database: {self.db_name}")
        print("=" * 60)

        confirm = input("Type 'YES' to confirm restore: ").strip()

        if confirm != 'YES':
            self.logger.info("Restore cancelled by user")
            return False

        downloaded_files = []
        success = True

        # Restore files
        if selected_file_backup:
            self.logger.info("\n" + "=" * 60)
            self.logger.info("STARTING FILE RESTORE")
            self.logger.info("=" * 60)

            file_path = self.download_backup(
                selected_file_backup['id'],
                selected_file_backup['name']
            )

            if file_path:
                downloaded_files.append(file_path)

                if self.delete_wordpress_files():
                    if not self.restore_wordpress_files(file_path):
                        success = False
                else:
                    success = False
            else:
                success = False

        # Restore database
        if selected_db_backup:
            self.logger.info("\n" + "=" * 60)
            self.logger.info("STARTING DATABASE RESTORE")
            self.logger.info("=" * 60)

            db_path = self.download_backup(
                selected_db_backup['id'],
                selected_db_backup['name']
            )

            if db_path:
                downloaded_files.append(db_path)

                if not self.restore_database(db_path):
                    success = False
            else:
                success = False

        # Cleanup
        self.cleanup_downloaded_files(downloaded_files)

        # Final status
        print("\n" + "=" * 60)
        if success:
            self.logger.info("✓ RESTORE PROCESS COMPLETED SUCCESSFULLY")
        else:
            self.logger.error("✗ RESTORE PROCESS COMPLETED WITH ERRORS")
        self.logger.info("=" * 60)

        return success

def main():
    config_file = sys.argv[1] if len(sys.argv) > 1 else 'config.ini'

    if not os.path.exists(config_file):
        print(f"Error: Configuration file '{config_file}' not found")
        sys.exit(1)

    restore = WordPressRestore(config_file)
    success = restore.run_restore()

    sys.exit(0 if success else 1)

if __name__ == '__main__':
    main()