init(); } /** * Initialize storage manager */ private function init() { $this->logger = TigerStyleSEO_Backup_Logger::instance(); $this->register_storage_backends(); } /** * Register available storage backends */ private function register_storage_backends() { // Local storage (always available) $this->storage_backends['local'] = array( 'name' => 'Local Storage', 'description' => 'Store backups locally on server', 'available' => true, 'settings' => array( 'path' => get_option('backup_local_path', WP_CONTENT_DIR . '/tigerstyle-backups/') ) ); // S3 Compatible storage $s3_enabled = get_option('backup_s3_enabled', false); $this->storage_backends['s3'] = array( 'name' => 'S3 Compatible Storage', 'description' => 'Store backups in AWS S3 or compatible services (MinIO, DigitalOcean Spaces, etc.)', 'available' => $s3_enabled && $this->check_s3_requirements(), 'settings' => array( 'endpoint' => get_option('backup_s3_endpoint', ''), 'bucket' => get_option('backup_s3_bucket', ''), 'access_key' => get_option('backup_s3_access_key', ''), 'secret_key' => get_option('backup_s3_secret_key', ''), 'region' => get_option('backup_s3_region', 'us-east-1'), 'storage_class' => get_option('backup_s3_storage_class', 'STANDARD_IA'), 'encryption' => get_option('backup_s3_encryption', true), 'prefix' => get_option('backup_s3_prefix', 'tigerstyle-backups/') ) ); } /** * Check S3 requirements */ private function check_s3_requirements() { return extension_loaded('curl') && function_exists('openssl_encrypt'); } /** * Store backup using configured storage backends */ public function store_backup($backup_file, $backup_id, $manifest) { $storage_results = array(); $primary_backend = get_option('backup_primary_storage', 'local'); $secondary_backends = get_option('backup_secondary_storage', array()); // Store to primary backend if (isset($this->storage_backends[$primary_backend]) && $this->storage_backends[$primary_backend]['available']) { $this->logger->info("Storing backup to primary storage", array( 'backend' => $primary_backend, 'backup_id' => $backup_id )); $result = $this->store_to_backend($backup_file, $backup_id, $manifest, $primary_backend); if ($result['success']) { $storage_results['primary'] = $result; } else { $this->logger->error("Primary storage failed", array( 'backend' => $primary_backend, 'error' => $result['error'] )); } } // Store to secondary backends foreach ($secondary_backends as $backend) { if (isset($this->storage_backends[$backend]) && $this->storage_backends[$backend]['available']) { $this->logger->info("Storing backup to secondary storage", array( 'backend' => $backend, 'backup_id' => $backup_id )); $result = $this->store_to_backend($backup_file, $backup_id, $manifest, $backend); if ($result['success']) { $storage_results['secondary'][$backend] = $result; } else { $this->logger->warning("Secondary storage failed", array( 'backend' => $backend, 'error' => $result['error'] )); } } } return $storage_results; } /** * Store backup to specific backend */ private function store_to_backend($backup_file, $backup_id, $manifest, $backend) { $start_time = microtime(true); try { switch ($backend) { case 'local': $result = $this->store_to_local($backup_file, $backup_id, $manifest); break; case 's3': $result = $this->store_to_s3($backup_file, $backup_id, $manifest); break; default: throw new Exception("Unknown storage backend: " . $backend); } $duration = microtime(true) - $start_time; $this->logger->info("Backup stored successfully", array( 'backend' => $backend, 'backup_id' => $backup_id, 'duration' => round($duration, 2) . 's', 'location' => $result['location'] ?? 'unknown' )); return array( 'success' => true, 'backend' => $backend, 'location' => $result['location'], 'duration' => $duration, 'metadata' => $result['metadata'] ?? array() ); } catch (Exception $e) { $this->logger->error("Storage backend failed", array( 'backend' => $backend, 'backup_id' => $backup_id, 'error' => $e->getMessage() )); return array( 'success' => false, 'backend' => $backend, 'error' => $e->getMessage() ); } } /** * Store backup to local storage */ private function store_to_local($backup_file, $backup_id, $manifest) { $settings = $this->storage_backends['local']['settings']; $destination_dir = $settings['path']; // Ensure destination directory exists if (!is_dir($destination_dir)) { wp_mkdir_p($destination_dir); // Protect backup directory $htaccess_content = "Order deny,allow\nDeny from all\n"; file_put_contents($destination_dir . '.htaccess', $htaccess_content); } $destination_file = $destination_dir . basename($backup_file); // Copy backup file if (!copy($backup_file, $destination_file)) { throw new Exception("Failed to copy backup to local storage"); } // Create manifest file $manifest_file = $destination_dir . $backup_id . '-manifest.json'; file_put_contents($manifest_file, json_encode($manifest, JSON_PRETTY_PRINT)); return array( 'location' => $destination_file, 'manifest_file' => $manifest_file, 'metadata' => array( 'size' => filesize($destination_file), 'checksum' => md5_file($destination_file) ) ); } /** * Store backup to S3 compatible storage */ private function store_to_s3($backup_file, $backup_id, $manifest) { $settings = $this->storage_backends['s3']['settings']; if (empty($settings['bucket']) || empty($settings['access_key']) || empty($settings['secret_key'])) { throw new Exception("S3 storage not properly configured"); } $s3_key = $settings['prefix'] . $backup_id . '/' . basename($backup_file); $manifest_key = $settings['prefix'] . $backup_id . '/manifest.json'; // Upload backup file $backup_upload = $this->s3_put_object($backup_file, $s3_key, $settings); if (!$backup_upload['success']) { throw new Exception("Failed to upload backup to S3: " . $backup_upload['error']); } // Upload manifest file $manifest_content = json_encode($manifest, JSON_PRETTY_PRINT); $manifest_upload = $this->s3_put_object_content($manifest_content, $manifest_key, $settings); if (!$manifest_upload['success']) { $this->logger->warning("Failed to upload manifest to S3", array( 'error' => $manifest_upload['error'] )); } return array( 'location' => $s3_key, 'manifest_location' => $manifest_key, 'metadata' => array( 'bucket' => $settings['bucket'], 'region' => $settings['region'], 'storage_class' => $settings['storage_class'], 'encryption' => $settings['encryption'], 'etag' => $backup_upload['etag'] ?? '', 'version_id' => $backup_upload['version_id'] ?? '' ) ); } /** * Upload file to S3 */ private function s3_put_object($file_path, $key, $settings) { if (!file_exists($file_path)) { return array('success' => false, 'error' => 'File not found'); } $file_content = file_get_contents($file_path); return $this->s3_put_object_content($file_content, $key, $settings); } /** * Upload content to S3 */ private function s3_put_object_content($content, $key, $settings) { $endpoint = $settings['endpoint'] ?: 'https://s3.' . $settings['region'] . '.amazonaws.com'; $bucket = $settings['bucket']; $url = $endpoint . '/' . $bucket . '/' . $key; // Prepare headers $headers = array(); $headers['Date'] = gmdate('D, d M Y H:i:s T'); $headers['Content-Type'] = 'application/octet-stream'; $headers['Content-Length'] = strlen($content); if ($settings['storage_class'] && $settings['storage_class'] !== 'STANDARD') { $headers['x-amz-storage-class'] = $settings['storage_class']; } if ($settings['encryption']) { $headers['x-amz-server-side-encryption'] = 'AES256'; } // Calculate content hash for authentication $content_hash = hash('sha256', $content); $headers['x-amz-content-sha256'] = $content_hash; // Create authorization signature $auth_header = $this->create_s3_auth_header('PUT', $key, $headers, $settings); $headers['Authorization'] = $auth_header; // Prepare curl request $ch = curl_init(); curl_setopt_array($ch, array( CURLOPT_URL => $url, CURLOPT_CUSTOMREQUEST => 'PUT', CURLOPT_POSTFIELDS => $content, CURLOPT_HTTPHEADER => $this->format_headers($headers), CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => true, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_TIMEOUT => 300, CURLOPT_FOLLOWLOCATION => false )); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curl_error = curl_error($ch); curl_close($ch); if ($curl_error) { return array('success' => false, 'error' => 'CURL error: ' . $curl_error); } if ($http_code >= 200 && $http_code < 300) { // Extract ETag from response headers $etag = ''; if (preg_match('/ETag: "([^"]+)"/', $response, $matches)) { $etag = $matches[1]; } return array( 'success' => true, 'http_code' => $http_code, 'etag' => $etag, 'response' => $response ); } else { return array( 'success' => false, 'error' => 'HTTP ' . $http_code . ': ' . $this->extract_s3_error($response) ); } } /** * Create S3 authorization header (AWS Signature Version 4) */ private function create_s3_auth_header($method, $key, $headers, $settings) { $access_key = $settings['access_key']; $secret_key = $settings['secret_key']; $region = $settings['region']; $service = 's3'; $timestamp = gmdate('Ymd\THis\Z'); $date = gmdate('Ymd'); // Create canonical request $canonical_uri = '/' . $key; $canonical_querystring = ''; $canonical_headers = ''; $signed_headers = array(); ksort($headers); foreach ($headers as $name => $value) { $name_lower = strtolower($name); $canonical_headers .= $name_lower . ':' . $value . "\n"; $signed_headers[] = $name_lower; } $signed_headers_string = implode(';', $signed_headers); $payload_hash = $headers['x-amz-content-sha256']; $canonical_request = $method . "\n" . $canonical_uri . "\n" . $canonical_querystring . "\n" . $canonical_headers . "\n" . $signed_headers_string . "\n" . $payload_hash; // Create string to sign $algorithm = 'AWS4-HMAC-SHA256'; $credential_scope = $date . '/' . $region . '/' . $service . '/aws4_request'; $string_to_sign = $algorithm . "\n" . $timestamp . "\n" . $credential_scope . "\n" . hash('sha256', $canonical_request); // Calculate signature $k_date = hash_hmac('sha256', $date, 'AWS4' . $secret_key, true); $k_region = hash_hmac('sha256', $region, $k_date, true); $k_service = hash_hmac('sha256', $service, $k_region, true); $k_signing = hash_hmac('sha256', 'aws4_request', $k_service, true); $signature = hash_hmac('sha256', $string_to_sign, $k_signing); // Create authorization header $authorization = $algorithm . ' ' . 'Credential=' . $access_key . '/' . $credential_scope . ', ' . 'SignedHeaders=' . $signed_headers_string . ', ' . 'Signature=' . $signature; return $authorization; } /** * Format headers for curl */ private function format_headers($headers) { $formatted = array(); foreach ($headers as $name => $value) { $formatted[] = $name . ': ' . $value; } return $formatted; } /** * Extract error message from S3 response */ private function extract_s3_error($response) { if (preg_match('/([^<]+)<\/Code>/', $response, $matches)) { $code = $matches[1]; if (preg_match('/([^<]+)<\/Message>/', $response, $msg_matches)) { return $code . ': ' . $msg_matches[1]; } return $code; } return 'Unknown S3 error'; } /** * Test S3 connection */ public function test_s3_connection($settings = null) { if (!$settings) { $settings = $this->storage_backends['s3']['settings']; } try { // Test by trying to list bucket contents $test_content = 'TigerStyle SEO Backup Connection Test'; $test_key = $settings['prefix'] . 'connection-test.txt'; $result = $this->s3_put_object_content($test_content, $test_key, $settings); if ($result['success']) { // Clean up test file $this->s3_delete_object($test_key, $settings); return array('success' => true, 'message' => 'S3 connection successful'); } else { return array('success' => false, 'error' => $result['error']); } } catch (Exception $e) { return array('success' => false, 'error' => $e->getMessage()); } } /** * Delete object from S3 */ private function s3_delete_object($key, $settings) { $endpoint = $settings['endpoint'] ?: 'https://s3.' . $settings['region'] . '.amazonaws.com'; $bucket = $settings['bucket']; $url = $endpoint . '/' . $bucket . '/' . $key; $headers = array(); $headers['Date'] = gmdate('D, d M Y H:i:s T'); $headers['x-amz-content-sha256'] = hash('sha256', ''); $auth_header = $this->create_s3_auth_header('DELETE', $key, $headers, $settings); $headers['Authorization'] = $auth_header; $ch = curl_init(); curl_setopt_array($ch, array( CURLOPT_URL => $url, CURLOPT_CUSTOMREQUEST => 'DELETE', CURLOPT_HTTPHEADER => $this->format_headers($headers), CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_TIMEOUT => 60 )); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return $http_code >= 200 && $http_code < 300; } /** * Retrieve backup from storage */ public function retrieve_backup($backup_id, $destination_path, $backend = null) { if (!$backend) { $backend = $this->get_backup_location($backup_id); } if (!$backend) { throw new Exception("Cannot determine backup location for: " . $backup_id); } $this->logger->info("Retrieving backup from storage", array( 'backup_id' => $backup_id, 'backend' => $backend, 'destination' => $destination_path )); switch ($backend) { case 'local': return $this->retrieve_from_local($backup_id, $destination_path); case 's3': return $this->retrieve_from_s3($backup_id, $destination_path); default: throw new Exception("Unknown storage backend: " . $backend); } } /** * Retrieve backup from local storage */ private function retrieve_from_local($backup_id, $destination_path) { $settings = $this->storage_backends['local']['settings']; $backup_dir = $settings['path']; // Find backup file $pattern = $backup_dir . $backup_id . '.*'; $files = glob($pattern); if (empty($files)) { throw new Exception("Backup file not found: " . $backup_id); } $backup_file = $files[0]; if (!copy($backup_file, $destination_path)) { throw new Exception("Failed to copy backup from local storage"); } return array( 'success' => true, 'source' => $backup_file, 'destination' => $destination_path, 'size' => filesize($destination_path) ); } /** * Retrieve backup from S3 */ private function retrieve_from_s3($backup_id, $destination_path) { // Implementation would go here for S3 download // For brevity, this is a placeholder throw new Exception("S3 backup retrieval not yet implemented"); } /** * Get backup location from database */ private function get_backup_location($backup_id) { global $wpdb; $table_name = $wpdb->prefix . 'tigerstyle_backups'; $backup = $wpdb->get_row( $wpdb->prepare("SELECT storage_location FROM {$table_name} WHERE backup_id = %s", $backup_id), ARRAY_A ); return $backup ? $backup['storage_location'] : null; } /** * Get available storage backends */ public function get_available_backends() { return array_filter($this->storage_backends, function($backend) { return $backend['available']; }); } /** * Get storage backend settings */ public function get_backend_settings($backend) { return isset($this->storage_backends[$backend]) ? $this->storage_backends[$backend]['settings'] : null; } /** * List all available backups */ public function list_backups($limit = 50, $offset = 0) { global $wpdb; $table_name = $wpdb->prefix . 'tigerstyle_backup_metadata'; // Get total count $total_count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}"); // Get backup list with pagination $backups = $wpdb->get_results( $wpdb->prepare( "SELECT backup_id, storage_type, file_path, s3_bucket, s3_key, s3_url, file_size, created_at, metadata_json FROM {$table_name} ORDER BY created_at DESC LIMIT %d OFFSET %d", $limit, $offset ), ARRAY_A ); // Process backup data $processed_backups = array(); foreach ($backups as $backup) { $metadata = array(); if (!empty($backup['metadata_json'])) { $metadata = json_decode($backup['metadata_json'], true) ?: array(); } // Determine storage location and availability $storage_info = $this->get_backup_storage_info($backup); $processed_backups[] = array( 'backup_id' => $backup['backup_id'], 'storage_type' => $backup['storage_type'], 'file_size' => intval($backup['file_size']), 'file_size_formatted' => $this->format_file_size($backup['file_size']), 'created_at' => $backup['created_at'], 'created_at_formatted' => $this->format_backup_date($backup['created_at']), 'storage_location' => $storage_info['location'], 'is_available' => $storage_info['available'], 'metadata' => $metadata, 'actions' => $this->get_backup_actions($backup) ); } return array( 'backups' => $processed_backups, 'total_count' => intval($total_count), 'limit' => $limit, 'offset' => $offset, 'has_more' => ($offset + $limit) < $total_count ); } /** * Get backup storage information */ private function get_backup_storage_info($backup) { $info = array( 'location' => '', 'available' => false ); switch ($backup['storage_type']) { case 'local': if (!empty($backup['file_path'])) { $info['location'] = basename($backup['file_path']); $info['available'] = file_exists($backup['file_path']); } else { // Fallback: check local storage directory $local_path = $this->storage_backends['local']['settings']['path']; $pattern = $local_path . $backup['backup_id'] . '.*'; $files = glob($pattern); if (!empty($files)) { $info['location'] = basename($files[0]); $info['available'] = true; } } break; case 's3': if (!empty($backup['s3_key'])) { $info['location'] = $backup['s3_bucket'] . '/' . $backup['s3_key']; $info['available'] = true; // Assume available unless proven otherwise } elseif (!empty($backup['s3_url'])) { $info['location'] = $backup['s3_url']; $info['available'] = true; } break; default: $info['location'] = __('Unknown storage type', 'tigerstyle-heat'); break; } return $info; } /** * Format file size for display */ private function format_file_size($bytes) { $bytes = floatval($bytes); $units = array('B', 'KB', 'MB', 'GB', 'TB'); for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) { $bytes /= 1024; } return round($bytes, 2) . ' ' . $units[$i]; } /** * Format backup date for display */ private function format_backup_date($datetime) { $timestamp = strtotime($datetime); if (!$timestamp) { return $datetime; } $now = current_time('timestamp'); $diff = $now - $timestamp; if ($diff < HOUR_IN_SECONDS) { $minutes = floor($diff / MINUTE_IN_SECONDS); return sprintf(_n('%d minute ago', '%d minutes ago', $minutes, 'tigerstyle-heat'), $minutes); } elseif ($diff < DAY_IN_SECONDS) { $hours = floor($diff / HOUR_IN_SECONDS); return sprintf(_n('%d hour ago', '%d hours ago', $hours, 'tigerstyle-heat'), $hours); } elseif ($diff < WEEK_IN_SECONDS) { $days = floor($diff / DAY_IN_SECONDS); return sprintf(_n('%d day ago', '%d days ago', $days, 'tigerstyle-heat'), $days); } else { return date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $timestamp); } } /** * Get available actions for a backup */ private function get_backup_actions($backup) { $actions = array(); // Restore action $actions['restore'] = array( 'label' => __('Restore', 'tigerstyle-heat'), 'url' => admin_url('admin.php?page=tigerstyle-heat&action=restore&backup_id=' . urlencode($backup['backup_id'])), 'class' => 'button button-primary' ); // Download action (for local backups) if ($backup['storage_type'] === 'local' && !empty($backup['file_path']) && file_exists($backup['file_path'])) { $actions['download'] = array( 'label' => __('Download', 'tigerstyle-heat'), 'url' => admin_url('admin.php?page=tigerstyle-heat&action=download&backup_id=' . urlencode($backup['backup_id'])), 'class' => 'button' ); } // Delete action $actions['delete'] = array( 'label' => __('Delete', 'tigerstyle-heat'), 'url' => admin_url('admin.php?page=tigerstyle-heat&action=delete&backup_id=' . urlencode($backup['backup_id'])), 'class' => 'button button-link-delete', 'confirm' => __('Are you sure you want to delete this backup? This action cannot be undone.', 'tigerstyle-heat') ); return $actions; } /** * Get storage statistics */ public function get_storage_stats() { $stats = array( 'total_count' => 0, 'total_size' => 0, 'usage_percent' => 0, 'available_space' => 0, 'backends' => array() ); // Calculate local storage stats if (isset($this->storage_backends['local']) && $this->storage_backends['local']['available']) { $local_path = $this->storage_backends['local']['settings']['path']; if (is_dir($local_path)) { $total_size = 0; $file_count = 0; $files = glob($local_path . '*.{zip,tar,gz,bz2}', GLOB_BRACE); if ($files) { foreach ($files as $file) { if (is_file($file)) { $total_size += filesize($file); $file_count++; } } } $stats['total_count'] = $file_count; $stats['total_size'] = $total_size; // Calculate disk usage percentage $disk_total = disk_total_space($local_path); $disk_free = disk_free_space($local_path); if ($disk_total && $disk_free) { $stats['usage_percent'] = round((($disk_total - $disk_free) / $disk_total) * 100, 2); $stats['available_space'] = $disk_free; } $stats['backends']['local'] = array( 'count' => $file_count, 'size' => $total_size, 'path' => $local_path ); } } // Add S3 stats if available (placeholder for now) if (isset($this->storage_backends['s3']) && $this->storage_backends['s3']['available']) { $stats['backends']['s3'] = array( 'count' => 0, 'size' => 0, 'bucket' => $this->storage_backends['s3']['settings']['bucket'] ); } return $stats; } }