This commit is contained in:
Brasdrive 2025-11-09 23:57:08 -04:00
parent e4bff1f14f
commit b974fb340a
3 changed files with 260 additions and 177 deletions

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}