Because cats have 9 lives, but servers don't - so they need backup-restore! Complete backup solution with S3/MinIO support. - Full WordPress backup (files + database) - S3 / MinIO / S3-compatible storage backends - Scheduled automatic backups - Disaster recovery / one-click restore - Backup integrity validation - Cat-themed admin interface Includes build.sh and .distignore for WordPress-installable release ZIPs.
604 lines
19 KiB
PHP
604 lines
19 KiB
PHP
<?php
|
|
/**
|
|
* Storage Manager
|
|
*
|
|
* Manages multiple storage backends for backup files
|
|
* Supports local, S3, Google Drive, and other cloud storage
|
|
*
|
|
* @package TigerStyleLife9
|
|
* @since 1.0.0
|
|
*/
|
|
|
|
// Exit if accessed directly
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Storage manager class
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
class TigerStyle_Life9_Storage_Manager {
|
|
|
|
/**
|
|
* Security instance
|
|
*
|
|
* @var TigerStyle_Life9_Security
|
|
*/
|
|
private $security;
|
|
|
|
/**
|
|
* Available storage backends
|
|
*
|
|
* @var array
|
|
*/
|
|
private $backends;
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct() {
|
|
// Don't initialize security here to avoid circular dependency
|
|
// Will be loaded lazily when needed
|
|
$this->init_backends();
|
|
}
|
|
|
|
/**
|
|
* Get security instance (lazy loading)
|
|
*
|
|
* @return TigerStyle_Life9_Security
|
|
*/
|
|
private function get_security() {
|
|
if (!$this->security) {
|
|
$this->security = tigerstyle_life9()->get_security();
|
|
}
|
|
return $this->security;
|
|
}
|
|
|
|
/**
|
|
* Initialize storage backends
|
|
*/
|
|
private function init_backends() {
|
|
$this->backends = [
|
|
'local' => [
|
|
'name' => __('Local Storage', 'tigerstyle-life9'),
|
|
'description' => __('Store backups on the local server', 'tigerstyle-life9'),
|
|
'class' => 'TigerStyle_Life9_Storage_Local',
|
|
'enabled' => true,
|
|
'config_fields' => []
|
|
],
|
|
's3' => [
|
|
'name' => __('Amazon S3', 'tigerstyle-life9'),
|
|
'description' => __('Store backups on Amazon S3 or compatible services', 'tigerstyle-life9'),
|
|
'class' => 'TigerStyle_Life9_Storage_S3',
|
|
'enabled' => class_exists('Aws\S3\S3Client'),
|
|
'config_fields' => [
|
|
'access_key' => __('Access Key ID', 'tigerstyle-life9'),
|
|
'secret_key' => __('Secret Access Key', 'tigerstyle-life9'),
|
|
'bucket' => __('Bucket Name', 'tigerstyle-life9'),
|
|
'region' => __('Region', 'tigerstyle-life9'),
|
|
'endpoint' => __('Custom Endpoint (optional)', 'tigerstyle-life9')
|
|
]
|
|
],
|
|
'google_drive' => [
|
|
'name' => __('Google Drive', 'tigerstyle-life9'),
|
|
'description' => __('Store backups on Google Drive', 'tigerstyle-life9'),
|
|
'class' => 'TigerStyle_Life9_Storage_GoogleDrive',
|
|
'enabled' => false, // Would require Google API client
|
|
'config_fields' => [
|
|
'client_id' => __('Client ID', 'tigerstyle-life9'),
|
|
'client_secret' => __('Client Secret', 'tigerstyle-life9'),
|
|
'folder_id' => __('Folder ID (optional)', 'tigerstyle-life9')
|
|
]
|
|
],
|
|
'ftp' => [
|
|
'name' => __('FTP/SFTP', 'tigerstyle-life9'),
|
|
'description' => __('Store backups on FTP or SFTP server', 'tigerstyle-life9'),
|
|
'class' => 'TigerStyle_Life9_Storage_FTP',
|
|
'enabled' => extension_loaded('ftp'),
|
|
'config_fields' => [
|
|
'host' => __('Host', 'tigerstyle-life9'),
|
|
'port' => __('Port', 'tigerstyle-life9'),
|
|
'username' => __('Username', 'tigerstyle-life9'),
|
|
'password' => __('Password', 'tigerstyle-life9'),
|
|
'path' => __('Remote Path', 'tigerstyle-life9'),
|
|
'passive' => __('Use Passive Mode', 'tigerstyle-life9'),
|
|
'ssl' => __('Use SSL/SFTP', 'tigerstyle-life9')
|
|
]
|
|
]
|
|
];
|
|
|
|
// Allow plugins to register additional backends
|
|
$this->backends = apply_filters('tigerstyle_life9_storage_backends', $this->backends);
|
|
}
|
|
|
|
/**
|
|
* Store backup file to configured storage locations
|
|
*
|
|
* @param string $file_path Local file path
|
|
* @param array $storage_config Storage configuration
|
|
* @return array Storage results
|
|
*/
|
|
public function store_backup($file_path, $storage_config = []) {
|
|
if (!file_exists($file_path) || !is_readable($file_path)) {
|
|
throw new Exception('Backup file not found or not readable');
|
|
}
|
|
|
|
// Validate file path
|
|
if (!$this->get_security()->validate_path($file_path)) {
|
|
throw new Exception('Invalid file path');
|
|
}
|
|
|
|
$results = [];
|
|
$enabled_storages = $this->get_enabled_storage_locations($storage_config);
|
|
|
|
foreach ($enabled_storages as $storage_type => $config) {
|
|
try {
|
|
$backend = $this->get_storage_backend($storage_type);
|
|
if (!$backend) {
|
|
$results[$storage_type] = [
|
|
'success' => false,
|
|
'error' => 'Storage backend not available'
|
|
];
|
|
continue;
|
|
}
|
|
|
|
$this->log_info("Storing backup to {$storage_type}", [
|
|
'file_path' => $file_path,
|
|
'file_size' => filesize($file_path)
|
|
]);
|
|
|
|
$result = $backend->store($file_path, $config);
|
|
|
|
$results[$storage_type] = [
|
|
'success' => true,
|
|
'url' => $result['url'] ?? null,
|
|
'remote_path' => $result['remote_path'] ?? null,
|
|
'storage_id' => $result['storage_id'] ?? null,
|
|
'metadata' => $result['metadata'] ?? []
|
|
];
|
|
|
|
$this->log_info("Backup stored successfully to {$storage_type}");
|
|
|
|
} catch (Exception $e) {
|
|
$this->log_error("Failed to store backup to {$storage_type}", [
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
$results[$storage_type] = [
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Retrieve backup file from storage
|
|
*
|
|
* @param string $storage_type Storage type
|
|
* @param string $remote_path Remote file path or ID
|
|
* @param string $local_path Local destination path
|
|
* @param array $config Storage configuration
|
|
* @return bool Success status
|
|
*/
|
|
public function retrieve_backup($storage_type, $remote_path, $local_path, $config = []) {
|
|
try {
|
|
$backend = $this->get_storage_backend($storage_type);
|
|
if (!$backend) {
|
|
throw new Exception('Storage backend not available');
|
|
}
|
|
|
|
// Validate local path
|
|
if (!$this->get_security()->validate_path(dirname($local_path))) {
|
|
throw new Exception('Invalid local destination path');
|
|
}
|
|
|
|
$this->log_info("Retrieving backup from {$storage_type}", [
|
|
'remote_path' => $remote_path,
|
|
'local_path' => $local_path
|
|
]);
|
|
|
|
$success = $backend->retrieve($remote_path, $local_path, $config);
|
|
|
|
if ($success) {
|
|
$this->log_info("Backup retrieved successfully from {$storage_type}");
|
|
} else {
|
|
throw new Exception('Retrieval failed');
|
|
}
|
|
|
|
return $success;
|
|
|
|
} catch (Exception $e) {
|
|
$this->log_error("Failed to retrieve backup from {$storage_type}", [
|
|
'error' => $e->getMessage()
|
|
]);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete backup from storage
|
|
*
|
|
* @param string $storage_type Storage type
|
|
* @param string $remote_path Remote file path or ID
|
|
* @param array $config Storage configuration
|
|
* @return bool Success status
|
|
*/
|
|
public function delete_backup($storage_type, $remote_path, $config = []) {
|
|
try {
|
|
$backend = $this->get_storage_backend($storage_type);
|
|
if (!$backend) {
|
|
throw new Exception('Storage backend not available');
|
|
}
|
|
|
|
$this->log_info("Deleting backup from {$storage_type}", [
|
|
'remote_path' => $remote_path
|
|
]);
|
|
|
|
$success = $backend->delete($remote_path, $config);
|
|
|
|
if ($success) {
|
|
$this->log_info("Backup deleted successfully from {$storage_type}");
|
|
}
|
|
|
|
return $success;
|
|
|
|
} catch (Exception $e) {
|
|
$this->log_error("Failed to delete backup from {$storage_type}", [
|
|
'error' => $e->getMessage()
|
|
]);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test storage connection
|
|
*
|
|
* @param string $storage_type Storage type
|
|
* @param array $config Storage configuration
|
|
* @return array Test result
|
|
*/
|
|
public function test_connection($storage_type, $config = []) {
|
|
try {
|
|
$backend = $this->get_storage_backend($storage_type);
|
|
if (!$backend) {
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Storage backend not available'
|
|
];
|
|
}
|
|
|
|
$result = $backend->test_connection($config);
|
|
|
|
return [
|
|
'success' => $result,
|
|
'message' => $result ? 'Connection successful' : 'Connection failed'
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get storage backend instance
|
|
*
|
|
* @param string $storage_type Storage type
|
|
* @return object|null Storage backend instance
|
|
*/
|
|
private function get_storage_backend($storage_type) {
|
|
if (!isset($this->backends[$storage_type])) {
|
|
return null;
|
|
}
|
|
|
|
$backend_info = $this->backends[$storage_type];
|
|
|
|
if (!$backend_info['enabled']) {
|
|
return null;
|
|
}
|
|
|
|
$class_name = $backend_info['class'];
|
|
|
|
if (!class_exists($class_name)) {
|
|
// Try to load the storage backend class
|
|
$file_name = 'class-storage-' . str_replace('_', '-', strtolower($storage_type)) . '.php';
|
|
$file_path = TIGERSTYLE_LIFE9_PLUGIN_DIR . 'includes/storage/' . $file_name;
|
|
|
|
if (file_exists($file_path)) {
|
|
require_once $file_path;
|
|
}
|
|
}
|
|
|
|
if (!class_exists($class_name)) {
|
|
return null;
|
|
}
|
|
|
|
return new $class_name();
|
|
}
|
|
|
|
/**
|
|
* Get enabled storage locations
|
|
*
|
|
* @param array $storage_config Storage configuration
|
|
* @return array Enabled storage locations
|
|
*/
|
|
private function get_enabled_storage_locations($storage_config = []) {
|
|
$default_config = get_option('tigerstyle_life9_storage_locations', ['local' => true]);
|
|
|
|
if (!empty($storage_config)) {
|
|
$enabled_storages = $storage_config;
|
|
} else {
|
|
$enabled_storages = $default_config;
|
|
}
|
|
|
|
$result = [];
|
|
|
|
foreach ($enabled_storages as $storage_type => $config) {
|
|
if ($config === true || (is_array($config) && !empty($config))) {
|
|
$result[$storage_type] = is_array($config) ? $config : [];
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Get available storage backends
|
|
*
|
|
* @return array Available backends
|
|
*/
|
|
public function get_available_backends() {
|
|
return $this->backends;
|
|
}
|
|
|
|
/**
|
|
* Get storage configuration for backend
|
|
*
|
|
* @param string $storage_type Storage type
|
|
* @return array Storage configuration
|
|
*/
|
|
public function get_storage_config($storage_type) {
|
|
$config = get_option("tigerstyle_life9_storage_{$storage_type}", []);
|
|
|
|
// Decrypt sensitive fields
|
|
if (!empty($config)) {
|
|
$sensitive_fields = ['password', 'secret_key', 'access_key', 'client_secret'];
|
|
|
|
foreach ($sensitive_fields as $field) {
|
|
if (isset($config[$field]) && !empty($config[$field])) {
|
|
$decrypted = $this->get_security()->decrypt($config[$field]);
|
|
if ($decrypted !== false) {
|
|
$config[$field] = $decrypted;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $config;
|
|
}
|
|
|
|
/**
|
|
* Save storage configuration
|
|
*
|
|
* @param string $storage_type Storage type
|
|
* @param array $config Configuration data
|
|
* @return bool Success status
|
|
*/
|
|
public function save_storage_config($storage_type, $config) {
|
|
try {
|
|
// Validate storage type
|
|
if (!isset($this->backends[$storage_type])) {
|
|
throw new Exception('Invalid storage type');
|
|
}
|
|
|
|
// Sanitize configuration
|
|
$sanitizer = new TigerStyle_Life9_Sanitizer();
|
|
$clean_config = $sanitizer->sanitize_array($config);
|
|
|
|
// Encrypt sensitive fields
|
|
$sensitive_fields = ['password', 'secret_key', 'access_key', 'client_secret'];
|
|
|
|
foreach ($sensitive_fields as $field) {
|
|
if (isset($clean_config[$field]) && !empty($clean_config[$field])) {
|
|
$encrypted = $this->get_security()->encrypt($clean_config[$field]);
|
|
if ($encrypted !== false) {
|
|
$clean_config[$field] = $encrypted;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save configuration
|
|
$result = update_option("tigerstyle_life9_storage_{$storage_type}", $clean_config);
|
|
|
|
$this->get_security()->log_security_event('storage_config_updated', [
|
|
'storage_type' => $storage_type
|
|
]);
|
|
|
|
return $result;
|
|
|
|
} catch (Exception $e) {
|
|
$this->log_error('Failed to save storage configuration', [
|
|
'storage_type' => $storage_type,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get storage usage information
|
|
*
|
|
* @param string $storage_type Storage type
|
|
* @param array $config Storage configuration
|
|
* @return array Usage information
|
|
*/
|
|
public function get_storage_usage($storage_type, $config = []) {
|
|
try {
|
|
$backend = $this->get_storage_backend($storage_type);
|
|
if (!$backend || !method_exists($backend, 'get_usage')) {
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Usage information not available'
|
|
];
|
|
}
|
|
|
|
$usage = $backend->get_usage($config);
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => $usage
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up old backups from storage
|
|
*
|
|
* @param string $storage_type Storage type
|
|
* @param int $retention_days Number of days to retain backups
|
|
* @param array $config Storage configuration
|
|
* @return array Cleanup results
|
|
*/
|
|
public function cleanup_old_backups($storage_type, $retention_days, $config = []) {
|
|
try {
|
|
$backend = $this->get_storage_backend($storage_type);
|
|
if (!$backend || !method_exists($backend, 'cleanup_old_files')) {
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Cleanup not supported'
|
|
];
|
|
}
|
|
|
|
$cutoff_date = date('Y-m-d', strtotime("-{$retention_days} days"));
|
|
|
|
$this->log_info("Cleaning up old backups from {$storage_type}", [
|
|
'retention_days' => $retention_days,
|
|
'cutoff_date' => $cutoff_date
|
|
]);
|
|
|
|
$result = $backend->cleanup_old_files($cutoff_date, $config);
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => $result
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
$this->log_error("Failed to cleanup old backups from {$storage_type}", [
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log info message
|
|
*
|
|
* @param string $message Log message
|
|
* @param array $context Additional context
|
|
*/
|
|
private function log_info($message, $context = []) {
|
|
error_log("TigerStyle Life9 Storage [INFO]: {$message}");
|
|
}
|
|
|
|
/**
|
|
* Log error message
|
|
*
|
|
* @param string $message Log message
|
|
* @param array $context Additional context
|
|
*/
|
|
private function log_error($message, $context = []) {
|
|
error_log("TigerStyle Life9 Storage [ERROR]: {$message}");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abstract storage backend class
|
|
*
|
|
* Base class for all storage backends
|
|
*/
|
|
abstract class TigerStyle_Life9_Storage_Backend {
|
|
|
|
/**
|
|
* Store file to storage
|
|
*
|
|
* @param string $file_path Local file path
|
|
* @param array $config Storage configuration
|
|
* @return array Storage result
|
|
*/
|
|
abstract public function store($file_path, $config = []);
|
|
|
|
/**
|
|
* Retrieve file from storage
|
|
*
|
|
* @param string $remote_path Remote file path or ID
|
|
* @param string $local_path Local destination path
|
|
* @param array $config Storage configuration
|
|
* @return bool Success status
|
|
*/
|
|
abstract public function retrieve($remote_path, $local_path, $config = []);
|
|
|
|
/**
|
|
* Delete file from storage
|
|
*
|
|
* @param string $remote_path Remote file path or ID
|
|
* @param array $config Storage configuration
|
|
* @return bool Success status
|
|
*/
|
|
abstract public function delete($remote_path, $config = []);
|
|
|
|
/**
|
|
* Test storage connection
|
|
*
|
|
* @param array $config Storage configuration
|
|
* @return bool Connection status
|
|
*/
|
|
abstract public function test_connection($config = []);
|
|
|
|
/**
|
|
* Generate remote file path
|
|
*
|
|
* @param string $file_path Local file path
|
|
* @return string Remote file path
|
|
*/
|
|
protected function generate_remote_path($file_path) {
|
|
$filename = basename($file_path);
|
|
$date_path = date('Y/m/d');
|
|
|
|
return "tigerstyle-life9/{$date_path}/{$filename}";
|
|
}
|
|
|
|
/**
|
|
* Validate configuration
|
|
*
|
|
* @param array $config Configuration to validate
|
|
* @param array $required_fields Required configuration fields
|
|
* @return bool Validation status
|
|
*/
|
|
protected function validate_config($config, $required_fields) {
|
|
foreach ($required_fields as $field) {
|
|
if (empty($config[$field])) {
|
|
throw new Exception("Missing required configuration: {$field}");
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
} |