tigerstyle-life9/includes/class-storage-manager.php
Ryan Malloy e92b7f8700 Initial commit: TigerStyle Life9 v1.0.0
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.
2026-05-27 14:32:00 -06:00

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;
}
}