mediaHooks = new MediaHooks(); } public static function bootstrap(string $pluginFile): void { self::$pluginFile = $pluginFile; register_activation_hook($pluginFile, [self::class, 'activate']); register_deactivation_hook($pluginFile, [self::class, 'deactivate']); add_action('plugins_loaded', static function () { self::instance()->init(); }); } public static function instance(): self { return self::$instance ??= new self(); } public static function activate(): void { wp_mkdir_p(WP_CONTENT_DIR . '/flysystem-uploads'); if (! defined('FLYSYSTEM_OFFLOAD_CONFIG') && ! file_exists(WP_CONTENT_DIR . '/flysystem-offload.php')) { error_log('[Flysystem Offload] No se encontró un archivo de configuración. Copia config/flysystem-offload.example.php a wp-content/flysystem-offload.php y ajústalo.'); } } public static function deactivate(): void { if ($instance = self::$instance) { $instance->mediaHooks->unregister(); $instance->mediaHooks->setFilesystem(null); } if (in_array('fly', stream_get_wrappers(), true)) { stream_wrapper_unregister('fly'); } } public function init(): void { $this->configLoader = new ConfigLoader(self::$pluginFile); $this->reloadConfig(); 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', [$this, 'handleSwitchBlog']); add_action('flysystem_offload_reload_config', [$this, 'reloadConfig']); if (defined('WP_CLI') && WP_CLI) { \WP_CLI::add_command('flysystem-offload health-check', [Admin\HealthCheck::class, 'run']); } } public function reloadConfig(): void { try { $this->config = $this->configLoader->load(); } catch (\Throwable $e) { error_log('[Flysystem Offload] Error cargando configuración: ' . $e->getMessage()); $this->config = $this->configLoader->defaults(); } $this->filesystem = null; $this->streamRegistered = false; $this->mediaHooks->unregister(); $this->mediaHooks->setFilesystem(null); $this->registerStreamWrapper(); } public function handleSwitchBlog(): void { $this->reloadConfig(); } public function getFilesystem(): FilesystemOperator { if (! $this->filesystem) { $factory = new FilesystemFactory($this->config); $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 { $filesystem = $this->getFilesystem(); } catch (\Throwable $e) { error_log('[Flysystem Offload] No se pudo obtener el filesystem: ' . $e->getMessage()); return; } try { FlysystemStreamWrapper::register( $filesystem, 'fly', PathHelper::normalizePrefix($this->config['base_prefix'] ?? '') ); $this->streamRegistered = true; } catch (\Throwable $e) { error_log('[Flysystem Offload] No se pudo registrar el stream wrapper: ' . $e->getMessage()); } $this->mediaHooks->setFilesystem($filesystem); $this->mediaHooks->register(); } public function filterUploadDir(array $dirs): array { $remoteBase = $this->getRemoteUrlBase(); $prefix = PathHelper::normalizePrefix($this->config['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'], '/'); } } unset($size); } 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->config['adapter'] ?? 'local'; $settings = $this->config['adapters'][$adapterKey] ?? []; return (new FilesystemFactory($this->config))->resolvePublicBaseUrl($adapterKey, $settings); } }