filesystem = $filesystem; $this->config = $config; $this->protocol = (string) ($config['stream']['protocol'] ?? 'flysystem'); $this->streamRootPrefix = PathHelper::normalize((string) ($config['stream']['root_prefix'] ?? '')); $this->streamHost = PathHelper::normalize((string) ($config['stream']['host'] ?? '')); $this->s3Prefix = PathHelper::normalize((string) ($config['s3']['prefix'] ?? '')); $this->baseUrl = $this->normaliseBaseUrl((string) ($config['uploads']['base_url'] ?? content_url('uploads'))); $this->remoteUrlPathPrefix = $this->buildRemoteUrlPathPrefix(); $this->effectiveBaseUrl = $this->calculateEffectiveBaseUrl($this->baseUrl, $this->remoteUrlPathPrefix); $this->deleteRemote = (bool) ($config['uploads']['delete_remote'] ?? true); $this->preferLocal = (bool) ($config['uploads']['prefer_local_for_missing'] ?? false); error_log(sprintf( '[MediaHooks] Initialized - protocol: %s, host: %s, root_prefix: %s, base_url: %s, effective_base_url: %s', $this->protocol, $this->streamHost, $this->streamRootPrefix, $this->baseUrl, $this->effectiveBaseUrl )); } public function register(): void { add_filter('upload_dir', [$this, 'filterUploadDir'], 20); add_filter('pre_option_upload_path', '__return_false'); add_filter('pre_option_upload_url_path', '__return_false'); add_filter('wp_get_attachment_url', [$this, 'rewriteAttachmentUrl'], 9, 2); add_filter('image_downsize', [$this, 'filterImageDownsize'], 10, 3); add_action('delete_attachment', [$this, 'deleteRemoteFiles']); } public function filterUploadDir(array $uploads): array { $subdir = $uploads['subdir'] ?? ''; $normalizedSubdir = $subdir !== '' ? PathHelper::normalize($subdir) : ''; $streamSubdir = $normalizedSubdir !== '' ? '/' . $normalizedSubdir : ''; $streamBase = sprintf('%s://%s', $this->protocol, $this->streamHost ?: 'default'); $uploads['path'] = $streamBase . $streamSubdir; $uploads['basedir'] = $streamBase; $uploads['baseurl'] = rtrim($this->effectiveBaseUrl, '/'); $uploads['url'] = $this->buildPublicUrl($normalizedSubdir); $uploads['subdir'] = $normalizedSubdir !== '' ? '/' . $normalizedSubdir : ''; $uploads['error'] = false; $uploads['flysystem_protocol'] = $this->protocol; $uploads['flysystem_host'] = $this->streamHost; $uploads['flysystem_root_prefix'] = $this->streamRootPrefix; error_log(sprintf( '[MediaHooks] Upload dir filtered - path: %s, url: %s', $uploads['path'], $uploads['url'] )); return $uploads; } public function rewriteAttachmentUrl(string $url, int $attachmentId): string { $file = get_post_meta($attachmentId, '_wp_attached_file', true); if (! $file) { return $url; } $relativePath = PathHelper::normalize($file); if ($relativePath === '') { return $url; } if ($this->remoteUrlPathPrefix !== '') { $prefixWithSlash = $this->remoteUrlPathPrefix . '/'; if (str_starts_with($relativePath, $prefixWithSlash)) { $relativePath = substr($relativePath, strlen($prefixWithSlash)); } elseif ($relativePath === $this->remoteUrlPathPrefix) { $relativePath = ''; } } $remoteUrl = $this->buildPublicUrl($relativePath); if (! $this->preferLocal) { return $remoteUrl; } try { if ($this->filesystem->fileExists($this->toRemotePath($relativePath))) { return $remoteUrl; } } catch (\Throwable $exception) { error_log(sprintf( '[Flysystem Offload] No se pudo verificar la existencia remota de "%s": %s', $relativePath, $exception->getMessage() )); } return $url; } public function filterImageDownsize(bool|array $out, int $attachmentId, array|string $size): bool|array { return false; } public function deleteRemoteFiles(int $attachmentId): void { if (! $this->deleteRemote) { return; } $files = $this->gatherAttachmentFiles($attachmentId); foreach ($files as $file) { $key = $this->toRemotePath($file); try { if ($this->filesystem->fileExists($key)) { $this->filesystem->delete($key); } } catch (\Throwable $exception) { error_log(sprintf( '[Flysystem Offload] No se pudo eliminar el archivo remoto "%s": %s', $key, $exception->getMessage() )); } } } /** * @return list */ private function gatherAttachmentFiles(int $attachmentId): array { $files = []; $attachedFile = get_post_meta($attachmentId, '_wp_attached_file', true); if ($attachedFile) { $files[] = $attachedFile; } $meta = wp_get_attachment_metadata($attachmentId); if (is_array($meta)) { if (! empty($meta['file'])) { $files[] = $meta['file']; } if (! empty($meta['sizes']) && is_array($meta['sizes'])) { $baseDir = $this->dirName($meta['file'] ?? ''); foreach ($meta['sizes'] as $sizeMeta) { if (! empty($sizeMeta['file'])) { $files[] = ($baseDir !== '' ? $baseDir . '/' : '') . $sizeMeta['file']; } } } } $files = array_filter($files, static fn ($file) => is_string($file) && $file !== ''); return array_values(array_unique($files, SORT_STRING)); } private function dirName(string $path): string { $directory = dirname($path); return $directory === '.' ? '' : $directory; } private function toRemotePath(string $file): string { $segments = []; // ✅ Solo agregar si NO están vacíos if ($this->streamRootPrefix !== '') { $segments[] = $this->streamRootPrefix; } if ($this->streamHost !== '') { $segments[] = $this->streamHost; } $segments[] = $file; $remotePath = PathHelper::join(...$segments); error_log(sprintf( '[MediaHooks] toRemotePath - file: %s, remote: %s', $file, $remotePath )); return $remotePath; } private function normaliseBaseUrl(string $baseUrl): string { $baseUrl = trim($baseUrl); if ($baseUrl === '') { $baseUrl = content_url('uploads'); } return rtrim($baseUrl, '/'); } private function buildRemoteUrlPathPrefix(): string { $segments = array_filter( [$this->s3Prefix, $this->streamRootPrefix, $this->streamHost], static fn (string $segment): bool => $segment !== '' ); return PathHelper::join(...$segments); } private function calculateEffectiveBaseUrl(string $baseUrl, string $pathPrefix): string { $baseUrl = rtrim($baseUrl, '/'); if ($pathPrefix === '') { return $baseUrl; } $basePath = trim((string) parse_url($baseUrl, PHP_URL_PATH), '/'); if ($basePath !== '' && str_ends_with($basePath, $pathPrefix)) { return $baseUrl; } return $baseUrl; } private function buildPublicUrl(string $relativePath): string { $base = rtrim($this->effectiveBaseUrl, '/'); if ($relativePath === '') { return $base; } return $base . '/' . PathHelper::normalize($relativePath); } }