init(); }); } public static function instance(): self { return self::$instance ??= new self(); } public static function activate(): void { $defaults = [ 'adapter' => 'local', 'base_prefix' => '', 'adapters' => [ 's3' => [ 'access_key' => '', 'secret_key' => '', 'region' => '', 'bucket' => '', 'endpoint' => '', 'cdn_url' => '' ], 'sftp' => [ 'host' => '', 'port' => 22, 'username' => '', 'password' => '', 'root' => '/uploads' ], 'gcs' => [ 'project_id' => '', 'bucket' => '', 'key_file_path' => '' ], 'azure' => [ 'account_name' => '', 'account_key' => '', 'container' => '', 'prefix' => '' ], 'webdav' => [ 'base_uri' => '', 'username' => '', 'password' => '', 'path_prefix' => '' ], 'googledrive' => [], 'onedrive' => [], 'dropbox' => [] ] ]; add_option('flysystem_offload_settings', $defaults); } public static function deactivate(): void { if (in_array('fly', stream_get_wrappers(), true)) { stream_wrapper_unregister('fly'); } } public function init(): void { $this->settings = get_option('flysystem_offload_settings', []); add_action( 'update_option_flysystem_offload_settings', function ($oldValue, $newValue) { $this->settings = $newValue; $this->filesystem = null; $this->streamRegistered = false; $this->registerStreamWrapper(); }, 10, 2 ); $this->registerStreamWrapper(); add_filter('upload_dir', [$this, 'filterUploadDir'], 20); add_filter('wp_get_attachment_url', [$this, 'filterAttachmentUrl'], 20, 2); add_filter('wp_get_attachment_metadata', [$this, 'filterAttachmentMetadata'], 20); add_filter('wp_get_original_image_path', [$this, 'filterOriginalImagePath'], 20); add_filter('wp_delete_file', [$this, 'handleDeleteFile'], 20); add_action('delete_attachment', [$this, 'handleDeleteAttachment'], 20); add_action('switch_blog', function () { $this->filesystem = null; $this->streamRegistered = false; $this->settings = get_option('flysystem_offload_settings', []); $this->registerStreamWrapper(); }); if (is_admin()) { (new SettingsPage(self::$pluginFile))->boot(); } if (defined('WP_CLI') && WP_CLI) { \WP_CLI::add_command('flysystem-offload health-check', [Admin\HealthCheck::class, 'run']); } } public function getFilesystem(): FilesystemOperator { if (!$this->filesystem) { $factory = new FilesystemFactory($this->settings); $result = $factory->make(); if (is_wp_error($result)) { throw new \RuntimeException($result->get_error_message()); } $this->filesystem = $result; } return $this->filesystem; } private function registerStreamWrapper(): void { if ($this->streamRegistered) { return; } try { FlysystemStreamWrapper::register($this->getFilesystem(), 'fly', PathHelper::normalizePrefix($this->settings['base_prefix'] ?? '')); $this->streamRegistered = true; } catch (\Throwable $e) { error_log('[Flysystem Offload] No se pudo registrar el stream wrapper: ' . $e->getMessage()); } } public function filterUploadDir(array $dirs): array { $remoteBase = $this->getRemoteUrlBase(); $prefix = PathHelper::normalizePrefix($this->settings['base_prefix'] ?? ''); $subdir = $dirs['subdir'] ?? ''; $dirs['path'] = "fly://{$prefix}{$subdir}"; $dirs['basedir'] = "fly://{$prefix}"; $dirs['url'] = trailingslashit($remoteBase) . ltrim($subdir, '/'); $dirs['baseurl'] = $remoteBase; return $dirs; } public function filterAttachmentUrl(string $url, int $postId): string { $localBase = trailingslashit(wp_get_upload_dir()['baseurl']); $remoteBase = trailingslashit($this->getRemoteUrlBase()); return str_replace($localBase, $remoteBase, $url); } public function filterAttachmentMetadata(array $metadata): array { if (!empty($metadata['file'])) { $metadata['file'] = PathHelper::stripProtocol($metadata['file']); } if (!empty($metadata['sizes'])) { foreach ($metadata['sizes'] as &$size) { if (!empty($size['file'])) { $size['file'] = ltrim($size['file'], '/'); } } } return $metadata; } public function filterOriginalImagePath(string $path): string { return PathHelper::ensureFlyProtocol($path); } public function handleDeleteFile(string $file): string|false { $flyPath = PathHelper::stripProtocol($file); try { $this->getFilesystem()->delete($flyPath); } catch (\Throwable $e) { error_log('[Flysystem Offload] Error al borrar archivo: ' . $flyPath . ' - ' . $e->getMessage()); } return false; } public function handleDeleteAttachment(int $postId): void { $files = PathHelper::collectFilesFromAttachment($postId); foreach ($files as $relativePath) { try { $this->getFilesystem()->delete($relativePath); } catch (\Throwable $e) { error_log('[Flysystem Offload] Error al borrar attachment: ' . $relativePath . ' - ' . $e->getMessage()); } } } private function getRemoteUrlBase(): string { $adapterKey = $this->settings['adapter'] ?? 'local'; $config = $this->settings['adapters'][$adapterKey] ?? []; return (new FilesystemFactory($this->settings))->resolvePublicBaseUrl($adapterKey, $config); } }