diff --git a/src/Filesystem/Adapters/S3Adapter.php b/src/Filesystem/Adapters/S3Adapter.php index 16a939a..060cf01 100644 --- a/src/Filesystem/Adapters/S3Adapter.php +++ b/src/Filesystem/Adapters/S3Adapter.php @@ -1,21 +1,36 @@ mimeTypeDetector = $mimeTypeDetector ?? new ExtensionMimeTypeDetector(); + } + public function validate(array $settings) { $required = ['access_key', 'secret_key', 'region', 'bucket']; foreach ($required as $field) { if (empty($settings[$field])) { - return new WP_Error('flysystem_offload_invalid_s3', sprintf(__('Campo S3 faltante: %s', 'flysystem-offload'), $field)); + return new WP_Error( + 'flysystem_offload_invalid_s3', + sprintf(__('Campo S3 faltante: %s', 'flysystem-offload'), $field) + ); } } @@ -27,21 +42,40 @@ class S3Adapter implements AdapterInterface try { $clientConfig = [ 'credentials' => [ - 'key' => $settings['access_key'], - 'secret' => $settings['secret_key'] + 'key' => $settings['access_key'], + 'secret' => $settings['secret_key'], ], - 'region' => $settings['region'], - 'version' => 'latest' + 'region' => $settings['region'], + 'version' => 'latest', ]; - if (!empty($settings['endpoint'])) { - $clientConfig['endpoint'] = $settings['endpoint']; - $clientConfig['use_path_style_endpoint'] = true; + if (! empty($settings['endpoint'])) { + $clientConfig['endpoint'] = rtrim($settings['endpoint'], '/'); + $clientConfig['use_path_style_endpoint'] = (bool) ($settings['use_path_style_endpoint'] ?? true); + } + + if (! empty($settings['http_client'])) { + $clientConfig['http_client'] = $settings['http_client']; } $client = new S3Client($clientConfig); - return new AwsS3V3Adapter($client, $settings['bucket'], $settings['prefix'] ?? ''); + $adapter = new AwsS3V3Adapter( + $client, + $settings['bucket'], + '', + options: [], + mimeTypeDetector: $this->mimeTypeDetector + ); + + if (! empty($settings['prefix'])) { + $adapter = new PathPrefixedAdapter( + $adapter, + trim($settings['prefix'], '/') + ); + } + + return $adapter; } catch (\Throwable $e) { return new WP_Error('flysystem_offload_s3_error', $e->getMessage()); } diff --git a/src/Filesystem/FilesystemFactory.php b/src/Filesystem/FilesystemFactory.php index f010f44..8570d7c 100644 --- a/src/Filesystem/FilesystemFactory.php +++ b/src/Filesystem/FilesystemFactory.php @@ -11,7 +11,6 @@ use FlysystemOffload\Filesystem\Adapters\OneDriveAdapter; use FlysystemOffload\Filesystem\Adapters\S3Adapter; use FlysystemOffload\Filesystem\Adapters\SftpAdapter; use FlysystemOffload\Filesystem\Adapters\WebdavAdapter; -use FlysystemOffload\Helpers\PathHelper; use League\Flysystem\Filesystem; use League\Flysystem\FilesystemOperator; use League\Flysystem\Local\LocalFilesystemAdapter; diff --git a/src/Helpers/PathHelper.php b/src/Helpers/PathHelper.php index afce594..f566573 100644 --- a/src/Helpers/PathHelper.php +++ b/src/Helpers/PathHelper.php @@ -1,60 +1,42 @@ writeStream($remotePath, $stream); + self::$filesystem?->writeStream($remotePath, $stream); } catch (\Throwable $e) { if (is_resource($stream)) { fclose($stream); @@ -172,7 +172,7 @@ class ImageEditorGD extends \WP_Image_Editor_GD } try { - $source = self::$filesystem->readStream($remotePath); + $source = self::$filesystem?->readStream($remotePath); if (! is_resource($source)) { throw new \RuntimeException('No se pudo abrir el stream remoto.'); } diff --git a/src/Media/ImageEditorImagick.php b/src/Media/ImageEditorImagick.php index fa77552..7db893f 100644 --- a/src/Media/ImageEditorImagick.php +++ b/src/Media/ImageEditorImagick.php @@ -11,17 +11,11 @@ if (! defined('ABSPATH')) { exit; } -if (! class_exists(\WP_Image_Editor::class)) { - require_once ABSPATH . WPINC . '/class-wp-image-editor.php'; -} - if (! class_exists(\WP_Image_Editor_Imagick::class) && file_exists(ABSPATH . WPINC . '/class-wp-image-editor-imagick.php')) { require_once ABSPATH . WPINC . '/class-wp-image-editor-imagick.php'; } if (! class_exists(\WP_Image_Editor_Imagick::class)) { - // Si la clase base no está disponible (p. ej. Imagick deshabilitado), - // salimos silenciosamente para permitir que WordPress siga usando GD. return; } @@ -29,7 +23,7 @@ class ImageEditorImagick extends \WP_Image_Editor_Imagick { protected static ?FilesystemOperator $filesystem = null; protected ?string $remotePath = null; - protected ?string $localPath = null; + protected ?string $localPath = null; public static function bootWithFilesystem(?FilesystemOperator $filesystem): void { @@ -40,7 +34,7 @@ class ImageEditorImagick extends \WP_Image_Editor_Imagick { if ($this->isFlyPath($this->file) && self::$filesystem) { $this->remotePath = PathHelper::stripProtocol($this->file); - $temp = $this->downloadToTemp($this->remotePath); + $temp = $this->downloadToTemp($this->remotePath); if (is_wp_error($temp)) { return $temp; @@ -137,7 +131,7 @@ class ImageEditorImagick extends \WP_Image_Editor_Imagick } try { - self::$filesystem->writeStream($remotePath, $stream); + self::$filesystem?->writeStream($remotePath, $stream); } catch (\Throwable $e) { if (is_resource($stream)) { fclose($stream); @@ -178,7 +172,7 @@ class ImageEditorImagick extends \WP_Image_Editor_Imagick } try { - $source = self::$filesystem->readStream($remotePath); + $source = self::$filesystem?->readStream($remotePath); if (! is_resource($source)) { throw new \RuntimeException('No se pudo abrir el stream remoto.'); } diff --git a/src/Media/MediaHooks.php b/src/Media/MediaHooks.php index 168557d..1e4e8ef 100644 --- a/src/Media/MediaHooks.php +++ b/src/Media/MediaHooks.php @@ -3,354 +3,238 @@ declare(strict_types=1); namespace FlysystemOffload\Media; -use League\Flysystem\FilesystemException; +use FlysystemOffload\Helpers\PathHelper; use League\Flysystem\FilesystemOperator; -use League\Flysystem\StorageAttributes; use WP_Error; class MediaHooks { - private const CUSTOM_IMAGE_EDITOR = 'FlysystemOffload\\Media\\ImageEditorImagick'; - private ?FilesystemOperator $filesystem = null; - private bool $registered = false; - private bool $metadataMirrorInProgress = false; + private string $basePrefix; + + private const IMAGE_EDITOR_IMAGICK = 'FlysystemOffload\\Media\\ImageEditorImagick'; + private const IMAGE_EDITOR_GD = 'FlysystemOffload\\Media\\ImageEditorGD'; + + public function __construct(string $basePrefix = '') + { + $this->basePrefix = PathHelper::trimSlashes($basePrefix); + } + + public function register(): void + { + add_filter('upload_dir', [$this, 'filterUploadDir'], 20); + add_filter('wp_get_attachment_url', [$this, 'filterAttachmentUrl'], 20, 2); + add_filter('get_attached_file', [$this, 'filterGetAttachedFile'], 20, 2); + add_filter('update_attached_file', [$this, 'filterUpdateAttachedFile'], 20, 2); + add_filter('wp_read_image_metadata', [$this, 'ensureLocalPathForMetadata'], 5, 2); + add_filter('image_editors', [$this, 'filterImageEditors'], 5); + add_action('delete_attachment', [$this, 'handleDeleteAttachment'], 20); + } public function setFilesystem(?FilesystemOperator $filesystem): void { $this->filesystem = $filesystem; - if (class_exists(self::CUSTOM_IMAGE_EDITOR)) { - \call_user_func([self::CUSTOM_IMAGE_EDITOR, 'bootWithFilesystem'], $filesystem); + if (class_exists(self::IMAGE_EDITOR_IMAGICK)) { + \call_user_func([self::IMAGE_EDITOR_IMAGICK, 'bootWithFilesystem'], $filesystem); + } + if (class_exists(self::IMAGE_EDITOR_GD)) { + \call_user_func([self::IMAGE_EDITOR_GD, 'bootWithFilesystem'], $filesystem); } } - public function register(): void + public function filterUploadDir(array $uploadDir): array { - if ($this->registered) { - return; - } + $subdir = trim($uploadDir['subdir'] ?? '', '/'); - add_filter('wp_image_editors', [$this, 'filterImageEditors'], 5); - add_filter('pre_move_uploaded_file', [$this, 'handlePreMoveUploadedFile'], 10, 3); - add_filter('wp_read_image_metadata', [$this, 'filterReadImageMetadata'], 10, 2); - add_filter('wp_generate_attachment_metadata', [$this, 'filterGenerateAttachmentMetadata'], 10, 2); - add_filter('pre_wp_unique_filename_file_list', [$this, 'filterUniqueFilenameFileList'], 10, 3); - add_action('delete_attachment', [$this, 'handleDeleteAttachment'], 10); + $remoteBase = $this->basePrefix !== '' ? $this->basePrefix . '/' : ''; + $remoteBase .= $subdir !== '' ? $subdir : ''; - $this->registered = true; + $remoteBase = trim($remoteBase, '/'); + + $uploadDir['path'] = $remoteBase !== '' ? 'fly://' . $remoteBase : 'fly://'; + $uploadDir['basedir'] = $uploadDir['path']; + $uploadDir['subdir'] = $subdir !== '' ? '/' . $subdir : ''; + $uploadDir['url'] = $uploadDir['baseurl'] = $uploadDir['url']; // baseurl se sobrescribe en Plugin::filterUploadDir + + return $uploadDir; } - public function unregister(): void + public function filterAttachmentUrl(string $url, int $attachmentId): string { - if (! $this->registered) { - return; + $file = get_post_meta($attachmentId, '_wp_attached_file', true); + + if (empty($file)) { + return $url; } - remove_filter('wp_image_editors', [$this, 'filterImageEditors'], 5); - remove_filter('pre_move_uploaded_file', [$this, 'handlePreMoveUploadedFile'], 10); - remove_filter('wp_read_image_metadata', [$this, 'filterReadImageMetadata'], 10); - remove_filter('wp_generate_attachment_metadata', [$this, 'filterGenerateAttachmentMetadata'], 10); - remove_filter('pre_wp_unique_filename_file_list', [$this, 'filterUniqueFilenameFileList'], 10); - remove_action('delete_attachment', [$this, 'handleDeleteAttachment']); + $relative = PathHelper::trimLeadingSlash($file); - $this->registered = false; + return trailingslashit($this->getBaseUrl()) . $relative; } - public function filterImageEditors(array $editors): array + public function filterGetAttachedFile(string $file, int $attachmentId): string { - if (! class_exists(self::CUSTOM_IMAGE_EDITOR)) { - return $editors; + $meta = wp_get_attachment_metadata($attachmentId); + $relative = $meta['file'] ?? get_post_meta($attachmentId, '_wp_attached_file', true); + + if (empty($relative)) { + return $file; } - $imagickIndex = array_search(\WP_Image_Editor_Imagick::class, $editors, true); - - if ($imagickIndex !== false) { - unset($editors[$imagickIndex]); - } - - array_unshift($editors, self::CUSTOM_IMAGE_EDITOR); - - return array_values(array_unique($editors)); + return 'fly://' . PathHelper::trimLeadingSlash($relative); } - public function handlePreMoveUploadedFile($override, array $file, string $destination) + public function filterUpdateAttachedFile(string $file, int $attachmentId): string { - if ($override !== null) { - return $override; + if (str_starts_with($file, 'fly://')) { + update_post_meta($attachmentId, '_wp_attached_file', PathHelper::stripProtocol($file)); + } else { + update_post_meta($attachmentId, '_wp_attached_file', PathHelper::trimLeadingSlash($file)); } - if (! $this->isFlyPath($destination)) { - return $override; - } - - if (! $this->filesystem) { - return new WP_Error( - 'flysystem_offload_missing_filesystem', - __('No se pudo acceder al filesystem remoto.', 'flysystem-offload') - ); - } - - $relativePath = $this->relativeFlyPath($destination); - - if ($relativePath === null) { - return new WP_Error( - 'flysystem_offload_invalid_destination', - __('Ruta de destino inválida para el stream fly://.', 'flysystem-offload') - ); - } - - $directory = trim(dirname($relativePath), '.'); - - try { - if ($directory !== '') { - $this->filesystem->createDirectory($directory); - } - - $stream = @fopen($file['tmp_name'], 'rb'); - - if (! $stream) { - return new WP_Error( - 'flysystem_offload_tmp_read_fail', - __('No se pudo leer el archivo temporal subido.', 'flysystem-offload') - ); - } - - $this->filesystem->writeStream($relativePath, $stream); - } catch (\Throwable $e) { - if (isset($stream) && is_resource($stream)) { - fclose($stream); - } - - return new WP_Error( - 'flysystem_offload_write_fail', - sprintf( - __('No se pudo guardar el archivo en el almacenamiento remoto: %s', 'flysystem-offload'), - $e->getMessage() - ) - ); - } - - if (isset($stream) && is_resource($stream)) { - fclose($stream); - } - - @unlink($file['tmp_name']); - - return true; + return $file; } - public function filterReadImageMetadata($metadata, string $file) + public function ensureLocalPathForMetadata($metadata, string $file) { - if ($this->metadataMirrorInProgress || ! $this->isFlyPath($file)) { + if (! str_starts_with($file, 'fly://') || ! $this->filesystem) { return $metadata; } - $this->metadataMirrorInProgress = true; - - $temp = $this->mirrorToLocal($file); + // Fuerza a WP a usar una copia temporal local durante la lectura de EXIF/IPTC + $remotePath = PathHelper::stripProtocol($file); + $temp = $this->downloadToTemp($remotePath); if (! is_wp_error($temp)) { $metadata = wp_read_image_metadata($temp); @unlink($temp); } - $this->metadataMirrorInProgress = false; - return $metadata; } - public function filterGenerateAttachmentMetadata(array $metadata, int $attachmentId): array + public function filterImageEditors(array $editors): array { - if (isset($metadata['filesize'])) { - return $metadata; + if (class_exists(self::IMAGE_EDITOR_IMAGICK)) { + $editors = array_filter( + $editors, + static fn (string $editor) => $editor !== \WP_Image_Editor_Imagick::class + ); + array_unshift($editors, self::IMAGE_EDITOR_IMAGICK); } - $file = get_attached_file($attachmentId); - - if ($file && file_exists($file)) { - $metadata['filesize'] = filesize($file); + if (class_exists(self::IMAGE_EDITOR_GD)) { + $editors = array_filter( + $editors, + static fn (string $editor) => $editor !== \WP_Image_Editor_GD::class + ); + array_unshift($editors, self::IMAGE_EDITOR_GD); } - return $metadata; - } - - public function filterUniqueFilenameFileList($files, string $dir, string $filename) - { - if (! $this->isFlyPath($dir) || ! $this->filesystem) { - return $files; - } - - $relativeDir = $this->relativeFlyPath($dir); - - if ($relativeDir === null) { - return $files; - } - - $existing = []; - - foreach ($this->filesystem->listContents($relativeDir, false) as $item) { - /** @var StorageAttributes $item */ - if ($item->isDir()) { - continue; - } - - $existing[] = basename($item->path()); - } - - return $existing; + return array_values(array_unique($editors)); } public function handleDeleteAttachment(int $attachmentId): void { - $file = get_attached_file($attachmentId); - - if (! $file || ! $this->isFlyPath($file)) { + if (! $this->filesystem) { return; } - $meta = wp_get_attachment_metadata($attachmentId); + $meta = wp_get_attachment_metadata($attachmentId); + $relative = $meta['file'] ?? get_post_meta($attachmentId, '_wp_attached_file', true); + + if (empty($relative)) { + return; + } + + $base = PathHelper::trimLeadingSlash($relative); + $directory = trim(dirname($base), './'); + + $targets = [$base]; if (! empty($meta['sizes'])) { - foreach ($meta['sizes'] as $sizeInfo) { - if (empty($sizeInfo['file'])) { - continue; + foreach ($meta['sizes'] as $size) { + if (! empty($size['file'])) { + $targets[] = ltrim(($directory ? $directory . '/' : '') . $size['file'], '/'); } - - wp_delete_file(str_replace(basename($file), $sizeInfo['file'], $file)); } } - $original = get_post_meta($attachmentId, 'original_image', true); - if ($original) { - wp_delete_file(str_replace(basename($file), $original, $file)); - } - - $backup = get_post_meta($attachmentId, '_wp_attachment_backup_sizes', true); - if (is_array($backup)) { - foreach ($backup as $sizeInfo) { - if (empty($sizeInfo['file'])) { - continue; + foreach ($targets as $target) { + try { + if ($this->filesystem->fileExists($target)) { + $this->filesystem->delete($target); } - - wp_delete_file(str_replace(basename($file), $sizeInfo['file'], $file)); + } catch (\Throwable $e) { + error_log('[Flysystem Offload] Error eliminando ' . $target . ': ' . $e->getMessage()); } } - - wp_delete_file($file); } - protected function mirrorToLocal(string $remotePath) + private function downloadToTemp(string $remotePath) { - if (! $this->filesystem || ! $this->isFlyPath($remotePath)) { - return $this->mirrorViaNativeCopy($remotePath); + if (! $this->filesystem) { + return new WP_Error( + 'flysystem_offload_no_fs', + __('No hay filesystem remoto configurado.', 'flysystem-offload') + ); } if (! function_exists('wp_tempnam')) { require_once ABSPATH . 'wp-admin/includes/file.php'; } - $temp = wp_tempnam(wp_basename($remotePath)); + $temp = wp_tempnam(basename($remotePath)); if (! $temp) { return new WP_Error( 'flysystem_offload_temp_fail', - __('No se pudo crear un archivo temporal para la lectura de metadatos.', 'flysystem-offload') - ); - } - - $relative = $this->relativeFlyPath($remotePath); - - if ($relative === null) { - @unlink($temp); - - return new WP_Error( - 'flysystem_offload_remote_copy_fail', - __('No se pudo determinar la ruta remota del archivo.', 'flysystem-offload') + __('No se pudo crear un archivo temporal.', 'flysystem-offload') ); } try { - $source = $this->filesystem->readStream($relative); - - if (! is_resource($source)) { + $stream = $this->filesystem->readStream($remotePath); + if (! is_resource($stream)) { throw new \RuntimeException('No se pudo abrir el stream remoto.'); } $target = fopen($temp, 'wb'); - if (! $target) { - throw new \RuntimeException('No se pudo abrir el archivo temporal en disco.'); + throw new \RuntimeException('No se pudo abrir el archivo temporal.'); } - stream_copy_to_stream($source, $target); + stream_copy_to_stream($stream, $target); + + fclose($stream); + fclose($target); } catch (\Throwable $e) { - if (isset($source) && is_resource($source)) { - fclose($source); + if (isset($stream) && is_resource($stream)) { + fclose($stream); } if (isset($target) && is_resource($target)) { fclose($target); } - @unlink($temp); return new WP_Error( - 'flysystem_offload_remote_copy_fail', + 'flysystem_offload_download_fail', sprintf( - __('No se pudo copiar la imagen desde el almacenamiento remoto: %s', 'flysystem-offload'), + __('Fallo al descargar "%s" del almacenamiento remoto: %s', 'flysystem-offload'), + $remotePath, $e->getMessage() ) ); } - if (isset($source) && is_resource($source)) { - fclose($source); - } - if (isset($target) && is_resource($target)) { - fclose($target); - } - return $temp; } - private function mirrorViaNativeCopy(string $remotePath) + private function getBaseUrl(): string { - if (! function_exists('wp_tempnam')) { - require_once ABSPATH . 'wp-admin/includes/file.php'; - } + $uploadDir = wp_get_upload_dir(); - $temp = wp_tempnam(wp_basename($remotePath)); - - if (! $temp) { - return new WP_Error( - 'flysystem_offload_temp_fail', - __('No se pudo crear un archivo temporal para la lectura de metadatos.', 'flysystem-offload') - ); - } - - if (@copy($remotePath, $temp)) { - return $temp; - } - - @unlink($temp); - - return new WP_Error( - 'flysystem_offload_remote_copy_fail', - __('No se pudo copiar la imagen desde el almacenamiento remoto.', 'flysystem-offload') - ); - } - - protected function isFlyPath(string $path): bool - { - return strncmp($path, 'fly://', 6) === 0; - } - - protected function relativeFlyPath(string $path): ?string - { - if (! $this->isFlyPath($path)) { - return null; - } - - return ltrim(substr($path, 6), '/'); + return $uploadDir['baseurl'] ?? content_url('/uploads'); } } diff --git a/src/StreamWrapper/FlysystemStreamWrapper.php b/src/StreamWrapper/FlysystemStreamWrapper.php index 2d25037..7cc9d16 100644 --- a/src/StreamWrapper/FlysystemStreamWrapper.php +++ b/src/StreamWrapper/FlysystemStreamWrapper.php @@ -12,18 +12,15 @@ use League\Flysystem\UnableToWriteFile; class FlysystemStreamWrapper { - /** - * @var array - */ + /** @var array */ private static array $filesystems = []; - /** - * @var array - */ + /** @var array */ private static array $prefixes = []; /** @var resource|null */ private $stream = null; + private string $protocol = ''; private string $path = ''; private string $mode = ''; @@ -31,10 +28,10 @@ class FlysystemStreamWrapper private array $dirEntries = []; private int $dirPosition = 0; - /** @var resource|null */ + /** @var resource|array|string|null */ public $context = null; - public static function register(FilesystemOperator $filesystem, string $protocol, string $prefix): void + public static function register(FilesystemOperator $filesystem, string $protocol, string $prefix = ''): void { if (in_array($protocol, stream_get_wrappers(), true)) { stream_wrapper_unregister($protocol); @@ -48,23 +45,25 @@ class FlysystemStreamWrapper public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool { - $protocol = $this->extractProtocol($path); + $protocol = $this->extractProtocol($path); $this->protocol = $protocol; $this->path = $path; $this->mode = $mode; - $this->flyPath = $this->resolveFlyPath($path, $protocol); + $this->flyPath = $this->resolveFlyPath($path); $filesystem = $this->filesystem($protocol); + $binary = str_contains($mode, 'b') ? 'b' : ''; + if (strpbrk($mode, 'waxc')) { - $this->stream = fopen('php://temp', str_contains($mode, 'b') ? 'w+b' : 'w+'); + $this->stream = fopen('php://temp', 'w+' . $binary); if (str_contains($mode, 'a') && $filesystem->fileExists($this->flyPath)) { try { - $contents = $filesystem->readStream($this->flyPath); - if (is_resource($contents)) { - stream_copy_to_stream($contents, $this->stream); - fclose($contents); + $remote = $filesystem->readStream($this->flyPath); + if (is_resource($remote)) { + stream_copy_to_stream($remote, $this->stream); + fclose($remote); } } catch (UnableToReadFile $e) { return false; @@ -77,12 +76,18 @@ class FlysystemStreamWrapper } try { - $resource = $filesystem->readStream($this->flyPath); - if (! is_resource($resource)) { + $remote = $filesystem->readStream($this->flyPath); + if (! is_resource($remote)) { return false; } - $this->stream = $resource; + $local = fopen('php://temp', 'w+' . $binary); + stream_copy_to_stream($remote, $local); + + fclose($remote); + rewind($local); + + $this->stream = $local; return true; } catch (UnableToReadFile $e) { @@ -109,9 +114,16 @@ class FlysystemStreamWrapper $filesystem = $this->filesystem($this->protocol); try { - rewind($this->stream); + $meta = stream_get_meta_data($this->stream); + if ($meta['seekable'] ?? false) { + rewind($this->stream); + } + $filesystem->writeStream($this->flyPath, $this->stream); - rewind($this->stream); + + if ($meta['seekable'] ?? false) { + rewind($this->stream); + } return true; } catch (UnableToWriteFile $e) { @@ -132,6 +144,58 @@ class FlysystemStreamWrapper $this->stream = null; } + public function stream_tell(): int|false + { + return is_resource($this->stream) ? ftell($this->stream) : false; + } + + public function stream_seek(int $offset, int $whence = SEEK_SET): bool + { + if (! is_resource($this->stream)) { + return false; + } + + return fseek($this->stream, $offset, $whence) === 0; + } + + public function stream_eof(): bool + { + return is_resource($this->stream) ? feof($this->stream) : true; + } + + public function stream_metadata(string $path, int $option, mixed $value): bool + { + // WordPress suele invocar chmod/chown/chgrp incluso sobre streams remotos. + // Los tratamos como no-ops y devolvemos true para evitar warnings. + return true; + } + + public function stream_cast(int $cast_as) + { + // Permite que funciones como getimagesize() o getID3 obtengan el recurso subyacente. + if (! is_resource($this->stream)) { + return false; + } + + // Aseguramos que el puntero esté al inicio cuando se castea como stream. + if (in_array($cast_as, [STREAM_CAST_FOR_SELECT, STREAM_CAST_AS_STREAM], true)) { + $meta = stream_get_meta_data($this->stream); + if ($meta['seekable'] ?? false) { + rewind($this->stream); + } + + return $this->stream; + } + + return false; + } + + public function stream_set_option(int $option, int $arg1, int $arg2): bool + { + // No se requieren operaciones especiales; devolvemos false para indicar que no se manejó. + return false; + } + public function stream_stat(): array|false { return $this->url_stat($this->path, 0); @@ -139,12 +203,14 @@ class FlysystemStreamWrapper public function url_stat(string $path, int $flags): array|false { - $protocol = $this->extractProtocol($path); + $protocol = $this->extractProtocol($path); $filesystem = $this->filesystem($protocol); - $flyPath = $this->resolveFlyPath($path, $protocol); + $flyPath = $this->resolveFlyPath($path); try { - if (! $filesystem->fileExists($flyPath) && ! $filesystem->directoryExists($flyPath)) { + $exists = $filesystem->fileExists($flyPath) || $filesystem->directoryExists($flyPath); + + if (! $exists) { if ($flags & STREAM_URL_STAT_QUIET) { return false; } @@ -197,9 +263,9 @@ class FlysystemStreamWrapper public function unlink(string $path): bool { - $protocol = $this->extractProtocol($path); + $protocol = $this->extractProtocol($path); $filesystem = $this->filesystem($protocol); - $flyPath = $this->resolveFlyPath($path, $protocol); + $flyPath = $this->resolveFlyPath($path); try { $filesystem->delete($flyPath); @@ -212,9 +278,9 @@ class FlysystemStreamWrapper public function mkdir(string $path, int $mode, int $options): bool { - $protocol = $this->extractProtocol($path); + $protocol = $this->extractProtocol($path); $filesystem = $this->filesystem($protocol); - $flyPath = $this->resolveFlyPath($path, $protocol); + $flyPath = $this->resolveFlyPath($path); try { $filesystem->createDirectory($flyPath); @@ -227,9 +293,9 @@ class FlysystemStreamWrapper public function rmdir(string $path, int $options): bool { - $protocol = $this->extractProtocol($path); + $protocol = $this->extractProtocol($path); $filesystem = $this->filesystem($protocol); - $flyPath = $this->resolveFlyPath($path, $protocol); + $flyPath = $this->resolveFlyPath($path); try { $filesystem->deleteDirectory($flyPath); @@ -240,13 +306,35 @@ class FlysystemStreamWrapper } } + public function rename(string $oldPath, string $newPath): bool + { + $oldProtocol = $this->extractProtocol($oldPath); + $newProtocol = $this->extractProtocol($newPath); + + if ($oldProtocol !== $newProtocol) { + return false; + } + + $filesystem = $this->filesystem($oldProtocol); + $from = $this->resolveFlyPath($oldPath); + $to = $this->resolveFlyPath($newPath); + + try { + $filesystem->move($from, $to); + + return true; + } catch (FilesystemException $e) { + return false; + } + } + public function dir_opendir(string $path, int $options): bool { $protocol = $this->extractProtocol($path); $this->protocol = $protocol; - $this->flyPath = $this->resolveFlyPath($path, $protocol); + $this->flyPath = $this->resolveFlyPath($path); $filesystem = $this->filesystem($protocol); - $this->dirEntries = []; + $this->dirEntries = ['.', '..']; foreach ($filesystem->listContents($this->flyPath, false) as $item) { if ($item instanceof StorageAttributes) { @@ -285,28 +373,6 @@ class FlysystemStreamWrapper return true; } - public function rename(string $oldPath, string $newPath): bool - { - $oldProtocol = $this->extractProtocol($oldPath); - $newProtocol = $this->extractProtocol($newPath); - - if ($oldProtocol !== $newProtocol) { - return false; - } - - $filesystem = $this->filesystem($oldProtocol); - $from = $this->resolveFlyPath($oldPath, $oldProtocol); - $to = $this->resolveFlyPath($newPath, $newProtocol); - - try { - $filesystem->move($from, $to); - - return true; - } catch (FilesystemException $e) { - return false; - } - } - private function filesystem(string $protocol): FilesystemOperator { if (! isset(self::$filesystems[$protocol])) { @@ -316,24 +382,12 @@ class FlysystemStreamWrapper return self::$filesystems[$protocol]; } - private function resolveFlyPath(string $path, ?string $protocol = null): string - { - $protocol ??= $this->extractProtocol($path); - $prefix = self::$prefixes[$protocol] ?? ''; - $raw = preg_replace('#^[^:]+://#', '', $path) ?? ''; - - $normalized = ltrim($raw, '/'); - - if ($prefix !== '' && str_starts_with($normalized, $prefix . '/')) { - return $normalized; - } - - if ($prefix === '') { - return $normalized; - } - - return $prefix . '/' . $normalized; - } + private function resolveFlyPath(string $path): string + { + $raw = preg_replace('#^[^:]+://#', '', $path) ?? ''; + + return ltrim($raw, '/'); + } private function extractProtocol(string $path): string {