From b974fb340ad7e1f1393ab2ec6b5dfd3ac0ae5f49 Mon Sep 17 00:00:00 2001 From: Brasdrive Date: Sun, 9 Nov 2025 23:57:08 -0400 Subject: [PATCH] 3.0.0 --- src/Filesystem/FilesystemFactory.php | 394 +++++++++++-------- src/Media/MediaHooks.php | 9 +- src/StreamWrapper/FlysystemStreamWrapper.php | 34 ++ 3 files changed, 260 insertions(+), 177 deletions(-) diff --git a/src/Filesystem/FilesystemFactory.php b/src/Filesystem/FilesystemFactory.php index 99663cb..95a4586 100644 --- a/src/Filesystem/FilesystemFactory.php +++ b/src/Filesystem/FilesystemFactory.php @@ -4,47 +4,29 @@ declare(strict_types=1); namespace FlysystemOffload\Filesystem; -use FlysystemOffload\Filesystem\AdapterInterface; -use FlysystemOffload\Filesystem\Adapters\S3Adapter; use FlysystemOffload\Filesystem\Adapters\WebdavAdapter; -use FlysystemOffload\Filesystem\Adapters\SftpAdapter; -use FlysystemOffload\Filesystem\Adapters\GoogleCloudAdapter; -use FlysystemOffload\Filesystem\Adapters\AzureBlobAdapter; -use FlysystemOffload\Filesystem\Adapters\DropboxAdapter; -use FlysystemOffload\Filesystem\Adapters\GoogleDriveAdapter; -use FlysystemOffload\Filesystem\Adapters\OneDriveAdapter; -use FlysystemOffload\Filesystem\Adapters\PrefixedAdapter; use League\Flysystem\Filesystem; +use League\Flysystem\FilesystemOperator; use League\Flysystem\FilesystemAdapter; +use League\Flysystem\AwsS3V3\AwsS3V3Adapter; +use League\Flysystem\UnixVisibility\PortableVisibilityConverter; +use Aws\S3\S3Client; +use Sabre\DAV\Client as SabreClient; use InvalidArgumentException; /** - * Factory para crear instancias de Filesystem con diferentes adaptadores + * Factory para crear instancias de Filesystem */ class FilesystemFactory { /** - * Mapa de proveedores a clases de adaptadores - */ - private const ADAPTER_MAP = [ - 's3' => S3Adapter::class, - 'webdav' => WebdavAdapter::class, - 'sftp' => SftpAdapter::class, - 'gcs' => GoogleCloudAdapter::class, - 'azure' => AzureBlobAdapter::class, - 'dropbox' => DropboxAdapter::class, - 'google-drive' => GoogleDriveAdapter::class, - 'onedrive' => OneDriveAdapter::class, - ]; - - /** - * Crea un Filesystem basado en la configuración proporcionada + * Crea una instancia de Filesystem basada en la configuración * * @param array $config Configuración del filesystem - * @return Filesystem - * @throws InvalidArgumentException Si el proveedor no es válido + * @return FilesystemOperator + * @throws InvalidArgumentException Si el provider no es válido */ - public static function create(array $config): Filesystem + public static function create(array $config): FilesystemOperator { $provider = $config['provider'] ?? ''; @@ -52,184 +34,248 @@ class FilesystemFactory throw new InvalidArgumentException('Provider is required in configuration'); } - $adapter = self::createAdapter($provider, $config); + error_log('[Flysystem Offload] Creating filesystem for provider: ' . $provider); - // Aplicar prefijo global si está configurado - if (!empty($config['prefix'])) { - $adapter = new PrefixedAdapter($adapter, $config['prefix']); - } + $adapter = self::createAdapter($provider, $config); return new Filesystem($adapter); } /** - * Crea un adaptador específico basado en el proveedor + * Crea el adaptador según el provider * - * @param string $provider Nombre del proveedor - * @param array $config Configuración del adaptador + * @param string $provider Nombre del provider + * @param array $config Configuración completa * @return FilesystemAdapter - * @throws InvalidArgumentException Si el proveedor no es soportado + * @throws InvalidArgumentException Si el provider no es soportado */ private static function createAdapter(string $provider, array $config): FilesystemAdapter { - $provider = strtolower(trim($provider)); + error_log('[Flysystem Offload] Creating adapter for: ' . $provider); - if (!isset(self::ADAPTER_MAP[$provider])) { - throw new InvalidArgumentException( - sprintf( - 'Unsupported provider: %s. Supported providers: %s', - $provider, - implode(', ', array_keys(self::ADAPTER_MAP)) - ) - ); - } - - $adapterClass = self::ADAPTER_MAP[$provider]; - - if (!class_exists($adapterClass)) { - throw new InvalidArgumentException( - sprintf('Adapter class not found: %s', $adapterClass) - ); - } - - /** @var AdapterInterface $adapterInstance */ - $adapterInstance = new $adapterClass(); - - if (!$adapterInstance instanceof AdapterInterface) { - throw new InvalidArgumentException( - sprintf('Adapter must implement AdapterInterface: %s', $adapterClass) - ); - } - - // Normalizar configuración específica del proveedor - $normalizedConfig = self::normalizeConfig($provider, $config); - - return $adapterInstance->createAdapter($normalizedConfig); + return match ($provider) { + 's3' => self::createS3Adapter($config), + 'webdav' => self::createWebdavAdapter($config), + 'sftp' => self::createSftpAdapter($config), + 'gcs' => self::createGcsAdapter($config), + 'azure' => self::createAzureAdapter($config), + 'dropbox' => self::createDropboxAdapter($config), + 'google-drive' => self::createGoogleDriveAdapter($config), + 'onedrive' => self::createOneDriveAdapter($config), + default => throw new InvalidArgumentException("Unsupported provider: {$provider}"), + }; } /** - * Normaliza la configuración según el proveedor + * Crea adaptador S3 * - * @param string $provider Nombre del proveedor - * @param array $config Configuración original - * @return array Configuración normalizada + * @param array $config + * @return AwsS3V3Adapter */ - private static function normalizeConfig(string $provider, array $config): array + private static function createS3Adapter(array $config): AwsS3V3Adapter { - $normalized = $config; + $clientConfig = [ + 'credentials' => [ + 'key' => $config['key'] ?? '', + 'secret' => $config['secret'] ?? '', + ], + 'region' => $config['region'] ?? 'us-east-1', + 'version' => 'latest', + ]; - // Normalizar permisos para WebDAV y SFTP - if (in_array($provider, ['webdav', 'sftp'], true)) { - $normalized = self::normalizePermissions($normalized); + if (!empty($config['endpoint'])) { + $clientConfig['endpoint'] = $config['endpoint']; } - // Normalizar configuración específica de S3 - if ($provider === 's3') { - $normalized = self::normalizeS3Config($normalized); + if (!empty($config['use_path_style_endpoint'])) { + $clientConfig['use_path_style_endpoint'] = true; } - return $normalized; + $client = new S3Client($clientConfig); + + $bucket = $config['bucket'] ?? ''; + $prefix = $config['prefix'] ?? ''; + + error_log(sprintf( + '[Flysystem Offload] S3 adapter created - bucket: %s, prefix: %s', + $bucket, + $prefix + )); + + return new AwsS3V3Adapter( + $client, + $bucket, + $prefix + ); } /** - * Normaliza permisos de archivos y directorios + * Crea adaptador WebDAV * - * @param array $config Configuración original - * @return array Configuración con permisos normalizados + * @param array $config + * @return WebdavAdapter */ - private static function normalizePermissions(array $config): array + private static function createWebdavAdapter(array $config): WebdavAdapter { - $permissionKeys = ['file_public', 'file_private', 'dir_public', 'dir_private']; + $baseUri = $config['base_uri'] ?? ''; + $username = $config['username'] ?? ''; + $password = $config['password'] ?? ''; + $authType = $config['auth_type'] ?? 'basic'; + $prefix = $config['prefix'] ?? ''; - foreach ($permissionKeys as $key) { - if (isset($config[$key])) { - // Si es string, mantenerlo como string (el adaptador lo convertirá) - // Si es int, mantenerlo como int - // Esto permite flexibilidad en la configuración - continue; + if (empty($baseUri)) { + throw new InvalidArgumentException('WebDAV base_uri is required'); + } + + error_log(sprintf( + '[Flysystem Offload] Creating WebDAV client - base_uri: %s, username: %s, prefix: %s', + $baseUri, + $username, + $prefix + )); + + // Configurar cliente Sabre + $settings = [ + 'baseUri' => $baseUri, + ]; + + // Agregar autenticación si está configurada + if (!empty($username)) { + $settings['userName'] = $username; + $settings['password'] = $password; + $settings['authType'] = constant('Sabre\DAV\Client::AUTH_' . strtoupper($authType)); + } + + $client = new SabreClient($settings); + + // Normalizar permisos a formato int octal + $filePublic = self::normalizePermission($config['file_public'] ?? 0644); + $filePrivate = self::normalizePermission($config['file_private'] ?? 0600); + $dirPublic = self::normalizePermission($config['dir_public'] ?? 0755); + $dirPrivate = self::normalizePermission($config['dir_private'] ?? 0700); + + error_log(sprintf( + '[Flysystem Offload] WebDAV permissions - file_public: %o (%d), file_private: %o (%d), dir_public: %o (%d), dir_private: %o (%d)', + $filePublic, $filePublic, + $filePrivate, $filePrivate, + $dirPublic, $dirPublic, + $dirPrivate, $dirPrivate + )); + + // Crear converter de visibilidad + // Los primeros 4 parámetros son int, el 5to es string (visibility: 'public' o 'private') + $visibilityConverter = new PortableVisibilityConverter( + filePublic: $filePublic, + filePrivate: $filePrivate, + directoryPublic: $dirPublic, + directoryPrivate: $dirPrivate, + defaultForDirectories: 'public' // String: 'public' o 'private' + ); + + error_log(sprintf( + '[Flysystem Offload] WebDAV adapter created - prefix: %s', + $prefix + )); + + return new WebdavAdapter($client, $prefix, $visibilityConverter); + } + + /** + * Crea adaptador SFTP + * + * @param array $config + * @return FilesystemAdapter + */ + private static function createSftpAdapter(array $config): FilesystemAdapter + { + throw new InvalidArgumentException('SFTP adapter not yet implemented'); + } + + /** + * Crea adaptador Google Cloud Storage + * + * @param array $config + * @return FilesystemAdapter + */ + private static function createGcsAdapter(array $config): FilesystemAdapter + { + throw new InvalidArgumentException('GCS adapter not yet implemented'); + } + + /** + * Crea adaptador Azure Blob Storage + * + * @param array $config + * @return FilesystemAdapter + */ + private static function createAzureAdapter(array $config): FilesystemAdapter + { + throw new InvalidArgumentException('Azure adapter not yet implemented'); + } + + /** + * Crea adaptador Dropbox + * + * @param array $config + * @return FilesystemAdapter + */ + private static function createDropboxAdapter(array $config): FilesystemAdapter + { + throw new InvalidArgumentException('Dropbox adapter not yet implemented'); + } + + /** + * Crea adaptador Google Drive + * + * @param array $config + * @return FilesystemAdapter + */ + private static function createGoogleDriveAdapter(array $config): FilesystemAdapter + { + throw new InvalidArgumentException('Google Drive adapter not yet implemented'); + } + + /** + * Crea adaptador OneDrive + * + * @param array $config + * @return FilesystemAdapter + */ + private static function createOneDriveAdapter(array $config): FilesystemAdapter + { + throw new InvalidArgumentException('OneDrive adapter not yet implemented'); + } + + /** + * Normaliza un permiso a formato int octal + * + * @param string|int $permission Permiso en formato string u octal + * @return int Permiso en formato int octal (ej: 0644) + */ + private static function normalizePermission($permission): int + { + // Si ya es int, retornar tal cual + if (is_int($permission)) { + return $permission; + } + + // Si es string, convertir + if (is_string($permission)) { + $permission = trim($permission); + + // Si tiene el formato 0xxx, convertir desde octal + if (preg_match('/^0[0-7]{3}$/', $permission)) { + return intval($permission, 8); + } + + // Si es solo dígitos sin el 0 inicial, añadirlo y convertir + if (preg_match('/^[0-7]{3}$/', $permission)) { + return intval('0' . $permission, 8); } } - // Establecer valores por defecto si no existen - $config['file_public'] = $config['file_public'] ?? 0644; - $config['file_private'] = $config['file_private'] ?? 0600; - $config['dir_public'] = $config['dir_public'] ?? 0755; - $config['dir_private'] = $config['dir_private'] ?? 0700; - - return $config; - } - - /** - * Normaliza configuración específica de S3 - * - * @param array $config Configuración original - * @return array Configuración normalizada - */ - private static function normalizeS3Config(array $config): array - { - // Asegurar que use_path_style_endpoint sea booleano - if (isset($config['use_path_style_endpoint'])) { - $config['use_path_style_endpoint'] = filter_var( - $config['use_path_style_endpoint'], - FILTER_VALIDATE_BOOLEAN - ); - } - - // Normalizar región - if (isset($config['region'])) { - $config['region'] = strtolower(trim($config['region'])); - } - - return $config; - } - - /** - * Obtiene la lista de proveedores soportados - * - * @return array - */ - public static function getSupportedProviders(): array - { - return array_keys(self::ADAPTER_MAP); - } - - /** - * Verifica si un proveedor es soportado - * - * @param string $provider Nombre del proveedor - * @return bool - */ - public static function isProviderSupported(string $provider): bool - { - return isset(self::ADAPTER_MAP[strtolower(trim($provider))]); - } - - /** - * Obtiene información sobre un proveedor específico - * - * @param string $provider Nombre del proveedor - * @return array Información del proveedor - * @throws InvalidArgumentException Si el proveedor no es soportado - */ - public static function getProviderInfo(string $provider): array - { - $provider = strtolower(trim($provider)); - - if (!self::isProviderSupported($provider)) { - throw new InvalidArgumentException("Unsupported provider: {$provider}"); - } - - $adapterClass = self::ADAPTER_MAP[$provider]; - $adapter = new $adapterClass(); - - return [ - 'name' => $provider, - 'class' => $adapterClass, - 'required_keys' => $adapter->getRequiredConfigKeys(), - 'optional_keys' => $adapter->getOptionalConfigKeys(), - 'description' => method_exists($adapter, 'getDescription') - ? $adapter->getDescription() - : '', - ]; + // Valor por defecto (0644 en octal = 420 en decimal) + error_log('[Flysystem Offload] Invalid permission format: ' . print_r($permission, true) . ', using default 0644'); + return 0644; } } diff --git a/src/Media/MediaHooks.php b/src/Media/MediaHooks.php index c4b4899..22728d5 100644 --- a/src/Media/MediaHooks.php +++ b/src/Media/MediaHooks.php @@ -62,9 +62,12 @@ final class MediaHooks { $streamSubdir = $normalizedSubdir !== '' ? '/' . $normalizedSubdir : ''; - // ✅ Usar 'default' solo si NO hay streamHost configurado - $streamHostPart = $this->streamHost !== '' ? $this->streamHost : 'default'; - $streamBase = sprintf('%s://%s', $this->protocol, $streamHostPart); + // 🚫 No forzar 'default' si no hay streamHost definido + $streamBase = $this->protocol . '://'; + + if ($this->streamHost !== '') { + $streamBase .= $this->streamHost; + } $uploads['path'] = $streamBase . $streamSubdir; $uploads['basedir'] = $streamBase; diff --git a/src/StreamWrapper/FlysystemStreamWrapper.php b/src/StreamWrapper/FlysystemStreamWrapper.php index a564a4a..1a376d3 100644 --- a/src/StreamWrapper/FlysystemStreamWrapper.php +++ b/src/StreamWrapper/FlysystemStreamWrapper.php @@ -72,6 +72,13 @@ final class FlysystemStreamWrapper { $this->resource = fopen('php://temp', 'w+b'); $this->dirty = false; + error_log(sprintf( + '[FlysystemStreamWrapper] stream_open - uri: "%s", path: "%s", mode: "%s"', + $this->uri, + $this->path, + $this->mode + )); + if ($this->resource === false) { return false; } @@ -127,9 +134,17 @@ final class FlysystemStreamWrapper { $this->dirty = true; } + error_log(sprintf( + '[FlysystemStreamWrapper] stream_write - path: "%s", bytes: %d, dirty: %s', + $this->path, + $written !== false ? $written : 0, + $this->dirty ? 'true' : 'false' + )); + return $written; } + public function stream_tell(): int|false { if (! $this->resource) { return false; @@ -155,6 +170,13 @@ final class FlysystemStreamWrapper { } public function stream_flush(): bool { + error_log(sprintf( + '[FlysystemStreamWrapper] stream_flush - path: "%s", dirty: %s, write_mode: %s', + $this->path, + $this->dirty ? 'true' : 'false', + $this->isWriteMode($this->mode) ? 'true' : 'false' + )); + if (! $this->resource || ! $this->dirty || ! $this->isWriteMode($this->mode)) { return true; } @@ -178,10 +200,22 @@ final class FlysystemStreamWrapper { (string) $contents, ['visibility' => self::$defaultVisibility] ); + + error_log(sprintf( + '[FlysystemStreamWrapper] stream_flush SUCCESS - path: "%s", size: %d bytes', + $this->path, + strlen((string) $contents) + )); + $this->dirty = false; return true; } catch (UnableToWriteFile|FilesystemException $exception) { + error_log(sprintf( + '[FlysystemStreamWrapper] stream_flush ERROR - path: "%s", error: %s', + $this->path, + $exception->getMessage() + )); trigger_error($exception->getMessage(), E_USER_WARNING); return false; }