Compare commits
2 Commits
b755cb707e
...
b974fb340a
| Author | SHA1 | Date |
|---|---|---|
|
|
b974fb340a | |
|
|
e4bff1f14f |
|
|
@ -4,110 +4,408 @@ declare(strict_types=1);
|
|||
|
||||
namespace FlysystemOffload\Filesystem\Adapters;
|
||||
|
||||
use FlysystemOffload\Filesystem\AdapterInterface;
|
||||
use League\Flysystem\FilesystemAdapter;
|
||||
use League\Flysystem\WebDAV\WebDAVAdapter as LeagueWebDAVAdapter;
|
||||
use League\Flysystem\Config;
|
||||
use League\Flysystem\FileAttributes;
|
||||
use League\Flysystem\DirectoryAttributes;
|
||||
use League\Flysystem\UnableToReadFile;
|
||||
use League\Flysystem\UnableToWriteFile;
|
||||
use League\Flysystem\UnableToDeleteFile;
|
||||
use League\Flysystem\UnableToCreateDirectory;
|
||||
use League\Flysystem\UnableToSetVisibility;
|
||||
use League\Flysystem\UnableToRetrieveMetadata;
|
||||
use League\Flysystem\Visibility;
|
||||
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
|
||||
use Sabre\DAV\Client;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Adaptador WebDAV para Flysystem Offload
|
||||
*/
|
||||
class WebdavAdapter implements AdapterInterface
|
||||
class WebdavAdapter implements FilesystemAdapter
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createAdapter(array $config): FilesystemAdapter
|
||||
{
|
||||
$this->validateConfig($config);
|
||||
private Client $client;
|
||||
private string $prefix;
|
||||
private PortableVisibilityConverter $visibilityConverter;
|
||||
|
||||
// Configurar cliente WebDAV
|
||||
$clientConfig = [
|
||||
'baseUri' => rtrim($config['base_uri'], '/') . '/',
|
||||
'userName' => $config['username'] ?? null,
|
||||
'password' => $config['password'] ?? null,
|
||||
];
|
||||
|
||||
// Configurar tipo de autenticación
|
||||
if (!empty($config['auth_type'])) {
|
||||
$authType = strtolower($config['auth_type']);
|
||||
$clientConfig['authType'] = match ($authType) {
|
||||
'basic' => Client::AUTH_BASIC,
|
||||
'digest' => Client::AUTH_DIGEST,
|
||||
'ntlm' => Client::AUTH_NTLM,
|
||||
default => Client::AUTH_BASIC,
|
||||
};
|
||||
}
|
||||
|
||||
// Crear cliente
|
||||
$client = new Client($clientConfig);
|
||||
|
||||
// El tercer parámetro debe ser un string: 'public' o 'private'
|
||||
// Por defecto usamos 'public' para compatibilidad
|
||||
$visibilityHandling = $config['visibility'] ?? 'public';
|
||||
public function __construct(
|
||||
Client $client,
|
||||
string $prefix = '',
|
||||
?PortableVisibilityConverter $visibilityConverter = null
|
||||
) {
|
||||
$this->client = $client;
|
||||
$this->prefix = trim($prefix, '/');
|
||||
$this->visibilityConverter = $visibilityConverter ?? new PortableVisibilityConverter();
|
||||
|
||||
error_log(sprintf(
|
||||
'[WebDAV Adapter] Creating adapter with prefix: %s, visibility: %s',
|
||||
$config['prefix'] ?? '',
|
||||
$visibilityHandling
|
||||
'[WebdavAdapter] Initialized with prefix: "%s"',
|
||||
$this->prefix
|
||||
));
|
||||
}
|
||||
|
||||
private function prefixPath(string $path): string
|
||||
{
|
||||
if ($this->prefix === '') {
|
||||
$prefixed = '/' . ltrim($path, '/');
|
||||
} else {
|
||||
$prefixed = '/' . $this->prefix . '/' . ltrim($path, '/');
|
||||
}
|
||||
|
||||
error_log(sprintf(
|
||||
'[WebdavAdapter] prefixPath - input: "%s", output: "%s"',
|
||||
$path,
|
||||
$prefixed
|
||||
));
|
||||
|
||||
// Crear adaptador - el tercer parámetro DEBE ser string
|
||||
$adapter = new LeagueWebDAVAdapter(
|
||||
$client,
|
||||
$config['prefix'] ?? '',
|
||||
$visibilityHandling // ← DEBE SER STRING: 'public' o 'private'
|
||||
);
|
||||
|
||||
error_log('[WebDAV Adapter] Adapter created successfully');
|
||||
|
||||
return $adapter;
|
||||
return $prefixed;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRequiredConfigKeys(): array
|
||||
public function fileExists(string $path): bool
|
||||
{
|
||||
return ['base_uri'];
|
||||
try {
|
||||
$response = $this->client->propFind($this->prefixPath($path), ['{DAV:}resourcetype'], 0);
|
||||
$exists = !empty($response);
|
||||
|
||||
error_log(sprintf(
|
||||
'[WebdavAdapter] fileExists - path: "%s", exists: %s',
|
||||
$path,
|
||||
$exists ? 'true' : 'false'
|
||||
));
|
||||
|
||||
return $exists;
|
||||
} catch (\Exception $e) {
|
||||
error_log(sprintf(
|
||||
'[WebdavAdapter] fileExists error - path: "%s", error: %s',
|
||||
$path,
|
||||
$e->getMessage()
|
||||
));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getOptionalConfigKeys(): array
|
||||
public function directoryExists(string $path): bool
|
||||
{
|
||||
return [
|
||||
'username',
|
||||
'password',
|
||||
'auth_type',
|
||||
'prefix',
|
||||
'visibility', // 'public' o 'private'
|
||||
];
|
||||
return $this->fileExists($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida la configuración
|
||||
*
|
||||
* @param array $config
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function validateConfig(array $config): void
|
||||
public function write(string $path, string $contents, Config $config): void
|
||||
{
|
||||
foreach ($this->getRequiredConfigKeys() as $key) {
|
||||
if (empty($config[$key])) {
|
||||
throw new InvalidArgumentException("WebDAV config key '{$key}' is required");
|
||||
$prefixedPath = $this->prefixPath($path);
|
||||
|
||||
error_log(sprintf(
|
||||
'[WebdavAdapter] write - path: "%s", prefixed: "%s", size: %d bytes',
|
||||
$path,
|
||||
$prefixedPath,
|
||||
strlen($contents)
|
||||
));
|
||||
|
||||
$this->ensureDirectoryExists(dirname($path), $config);
|
||||
|
||||
try {
|
||||
$response = $this->client->request('PUT', $prefixedPath, $contents);
|
||||
|
||||
if ($response['statusCode'] >= 400) {
|
||||
throw UnableToWriteFile::atLocation(
|
||||
$path,
|
||||
"WebDAV returned status {$response['statusCode']}"
|
||||
);
|
||||
}
|
||||
|
||||
error_log(sprintf(
|
||||
'[WebdavAdapter] write success - path: "%s", status: %d',
|
||||
$path,
|
||||
$response['statusCode']
|
||||
));
|
||||
} catch (\Exception $e) {
|
||||
error_log(sprintf(
|
||||
'[WebdavAdapter] write error - path: "%s", error: %s',
|
||||
$path,
|
||||
$e->getMessage()
|
||||
));
|
||||
throw UnableToWriteFile::atLocation($path, $e->getMessage(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
public function writeStream(string $path, $contents, Config $config): void
|
||||
{
|
||||
$this->write($path, stream_get_contents($contents), $config);
|
||||
}
|
||||
|
||||
public function read(string $path): string
|
||||
{
|
||||
try {
|
||||
$response = $this->client->request('GET', $this->prefixPath($path));
|
||||
|
||||
if ($response['statusCode'] >= 400) {
|
||||
throw UnableToReadFile::fromLocation(
|
||||
$path,
|
||||
"WebDAV returned status {$response['statusCode']}"
|
||||
);
|
||||
}
|
||||
|
||||
return $response['body'];
|
||||
} catch (\Exception $e) {
|
||||
throw UnableToReadFile::fromLocation($path, $e->getMessage(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
public function readStream(string $path)
|
||||
{
|
||||
$resource = fopen('php://temp', 'r+');
|
||||
if ($resource === false) {
|
||||
throw UnableToReadFile::fromLocation($path, 'Unable to create temp stream');
|
||||
}
|
||||
|
||||
fwrite($resource, $this->read($path));
|
||||
rewind($resource);
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
public function delete(string $path): void
|
||||
{
|
||||
try {
|
||||
$response = $this->client->request('DELETE', $this->prefixPath($path));
|
||||
|
||||
if ($response['statusCode'] >= 400 && $response['statusCode'] !== 404) {
|
||||
throw UnableToDeleteFile::atLocation(
|
||||
$path,
|
||||
"WebDAV returned status {$response['statusCode']}"
|
||||
);
|
||||
}
|
||||
|
||||
error_log(sprintf(
|
||||
'[WebdavAdapter] delete success - path: "%s"',
|
||||
$path
|
||||
));
|
||||
} catch (\Exception $e) {
|
||||
error_log(sprintf(
|
||||
'[WebdavAdapter] delete error - path: "%s", error: %s',
|
||||
$path,
|
||||
$e->getMessage()
|
||||
));
|
||||
throw UnableToDeleteFile::atLocation($path, $e->getMessage(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteDirectory(string $path): void
|
||||
{
|
||||
$this->delete($path);
|
||||
}
|
||||
|
||||
public function createDirectory(string $path, Config $config): void
|
||||
{
|
||||
$prefixedPath = $this->prefixPath($path);
|
||||
|
||||
error_log(sprintf(
|
||||
'[WebdavAdapter] createDirectory - path: "%s", prefixed: "%s"',
|
||||
$path,
|
||||
$prefixedPath
|
||||
));
|
||||
|
||||
try {
|
||||
$response = $this->client->request('MKCOL', $prefixedPath);
|
||||
|
||||
if ($response['statusCode'] >= 400 && $response['statusCode'] !== 405) {
|
||||
throw UnableToCreateDirectory::atLocation(
|
||||
$path,
|
||||
"WebDAV returned status {$response['statusCode']}"
|
||||
);
|
||||
}
|
||||
|
||||
error_log(sprintf(
|
||||
'[WebdavAdapter] createDirectory success - path: "%s", status: %d',
|
||||
$path,
|
||||
$response['statusCode']
|
||||
));
|
||||
} catch (\Exception $e) {
|
||||
error_log(sprintf(
|
||||
'[WebdavAdapter] createDirectory error - path: "%s", error: %s',
|
||||
$path,
|
||||
$e->getMessage()
|
||||
));
|
||||
|
||||
if ($e->getCode() !== 405) {
|
||||
throw UnableToCreateDirectory::atLocation($path, $e->getMessage(), $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function ensureDirectoryExists(string $dirname, Config $config): void
|
||||
{
|
||||
if ($dirname === '' || $dirname === '.') {
|
||||
return;
|
||||
}
|
||||
|
||||
$parts = explode('/', trim($dirname, '/'));
|
||||
$path = '';
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if ($part === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path .= ($path !== '' ? '/' : '') . $part;
|
||||
|
||||
if (!$this->directoryExists($path)) {
|
||||
$this->createDirectory($path, $config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function setVisibility(string $path, string $visibility): void
|
||||
{
|
||||
// WebDAV no soporta visibilidad de forma nativa
|
||||
// Se podría implementar con propiedades personalizadas si es necesario
|
||||
}
|
||||
|
||||
public function visibility(string $path): FileAttributes
|
||||
{
|
||||
return new FileAttributes($path, null, Visibility::PUBLIC);
|
||||
}
|
||||
|
||||
public function mimeType(string $path): FileAttributes
|
||||
{
|
||||
try {
|
||||
$response = $this->client->propFind(
|
||||
$this->prefixPath($path),
|
||||
['{DAV:}getcontenttype'],
|
||||
0
|
||||
);
|
||||
|
||||
$mimeType = $response['{DAV:}getcontenttype'] ?? null;
|
||||
|
||||
return new FileAttributes($path, null, null, null, $mimeType);
|
||||
} catch (\Exception $e) {
|
||||
throw UnableToRetrieveMetadata::mimeType($path, $e->getMessage(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
public function lastModified(string $path): FileAttributes
|
||||
{
|
||||
try {
|
||||
$response = $this->client->propFind(
|
||||
$this->prefixPath($path),
|
||||
['{DAV:}getlastmodified'],
|
||||
0
|
||||
);
|
||||
|
||||
$lastModified = $response['{DAV:}getlastmodified'] ?? null;
|
||||
$timestamp = $lastModified ? strtotime($lastModified) : null;
|
||||
|
||||
return new FileAttributes($path, null, null, $timestamp);
|
||||
} catch (\Exception $e) {
|
||||
throw UnableToRetrieveMetadata::lastModified($path, $e->getMessage(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
public function fileSize(string $path): FileAttributes
|
||||
{
|
||||
try {
|
||||
$response = $this->client->propFind(
|
||||
$this->prefixPath($path),
|
||||
['{DAV:}getcontentlength'],
|
||||
0
|
||||
);
|
||||
|
||||
$size = isset($response['{DAV:}getcontentlength'])
|
||||
? (int) $response['{DAV:}getcontentlength']
|
||||
: null;
|
||||
|
||||
return new FileAttributes($path, $size);
|
||||
} catch (\Exception $e) {
|
||||
throw UnableToRetrieveMetadata::fileSize($path, $e->getMessage(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
public function listContents(string $path, bool $deep): iterable
|
||||
{
|
||||
try {
|
||||
$response = $this->client->propFind(
|
||||
$this->prefixPath($path),
|
||||
[
|
||||
'{DAV:}resourcetype',
|
||||
'{DAV:}getcontentlength',
|
||||
'{DAV:}getlastmodified',
|
||||
'{DAV:}getcontenttype',
|
||||
],
|
||||
$deep ? \Sabre\DAV\Client::DEPTH_INFINITY : 1
|
||||
);
|
||||
|
||||
foreach ($response as $itemPath => $properties) {
|
||||
$relativePath = $this->removePrefix($itemPath);
|
||||
|
||||
if ($relativePath === $path || $relativePath === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isDirectory = isset($properties['{DAV:}resourcetype'])
|
||||
&& strpos($properties['{DAV:}resourcetype']->serialize(new \Sabre\Xml\Writer()), 'collection') !== false;
|
||||
|
||||
if ($isDirectory) {
|
||||
yield new DirectoryAttributes($relativePath);
|
||||
} else {
|
||||
$size = isset($properties['{DAV:}getcontentlength'])
|
||||
? (int) $properties['{DAV:}getcontentlength']
|
||||
: null;
|
||||
$lastModified = isset($properties['{DAV:}getlastmodified'])
|
||||
? strtotime($properties['{DAV:}getlastmodified'])
|
||||
: null;
|
||||
$mimeType = $properties['{DAV:}getcontenttype'] ?? null;
|
||||
|
||||
yield new FileAttributes(
|
||||
$relativePath,
|
||||
$size,
|
||||
null,
|
||||
$lastModified,
|
||||
$mimeType
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log(sprintf(
|
||||
'[WebdavAdapter] listContents error - path: "%s", error: %s',
|
||||
$path,
|
||||
$e->getMessage()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private function removePrefix(string $path): string
|
||||
{
|
||||
$path = '/' . trim($path, '/');
|
||||
|
||||
if ($this->prefix !== '') {
|
||||
$prefixWithSlash = '/' . $this->prefix . '/';
|
||||
if (str_starts_with($path, $prefixWithSlash)) {
|
||||
return substr($path, strlen($prefixWithSlash));
|
||||
}
|
||||
}
|
||||
|
||||
if (!filter_var($config['base_uri'], FILTER_VALIDATE_URL)) {
|
||||
throw new InvalidArgumentException('WebDAV base_uri must be a valid URL');
|
||||
}
|
||||
return ltrim($path, '/');
|
||||
}
|
||||
|
||||
// Validar visibility si está presente
|
||||
if (isset($config['visibility']) && !in_array($config['visibility'], ['public', 'private'], true)) {
|
||||
throw new InvalidArgumentException("WebDAV visibility must be 'public' or 'private'");
|
||||
public function move(string $source, string $destination, Config $config): void
|
||||
{
|
||||
try {
|
||||
$this->client->request(
|
||||
'MOVE',
|
||||
$this->prefixPath($source),
|
||||
null,
|
||||
['Destination' => $this->prefixPath($destination)]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
throw new \RuntimeException("Unable to move file from {$source} to {$destination}: " . $e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
public function copy(string $source, string $destination, Config $config): void
|
||||
{
|
||||
try {
|
||||
$this->client->request(
|
||||
'COPY',
|
||||
$this->prefixPath($source),
|
||||
null,
|
||||
['Destination' => $this->prefixPath($destination)]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
throw new \RuntimeException("Unable to copy file from {$source} to {$destination}: " . $e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,9 @@ final class MediaHooks {
|
|||
private string $protocol;
|
||||
private string $streamHost;
|
||||
private string $streamRootPrefix;
|
||||
private string $s3Prefix;
|
||||
private string $providerPrefix;
|
||||
private string $baseUrl;
|
||||
private string $effectiveBaseUrl;
|
||||
private string $remoteUrlPathPrefix;
|
||||
private bool $deleteRemote;
|
||||
private bool $preferLocal;
|
||||
|
||||
|
|
@ -26,22 +25,25 @@ final class MediaHooks {
|
|||
|
||||
$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'] ?? ''));
|
||||
|
||||
// ✅ Obtener el prefix del provider actual
|
||||
$provider = $config['provider'] ?? 's3';
|
||||
$this->providerPrefix = PathHelper::normalize((string) ($config[$provider]['prefix'] ?? $config['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->effectiveBaseUrl = $this->baseUrl;
|
||||
|
||||
$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',
|
||||
'[MediaHooks] Initialized - provider: %s, protocol: %s, host: %s, root_prefix: %s, provider_prefix: %s, base_url: %s',
|
||||
$provider,
|
||||
$this->protocol,
|
||||
$this->streamHost,
|
||||
$this->streamRootPrefix,
|
||||
$this->baseUrl,
|
||||
$this->effectiveBaseUrl
|
||||
$this->providerPrefix,
|
||||
$this->baseUrl
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +61,13 @@ final class MediaHooks {
|
|||
$normalizedSubdir = $subdir !== '' ? PathHelper::normalize($subdir) : '';
|
||||
|
||||
$streamSubdir = $normalizedSubdir !== '' ? '/' . $normalizedSubdir : '';
|
||||
$streamBase = sprintf('%s://%s', $this->protocol, $this->streamHost ?: 'default');
|
||||
|
||||
// 🚫 No forzar 'default' si no hay streamHost definido
|
||||
$streamBase = $this->protocol . '://';
|
||||
|
||||
if ($this->streamHost !== '') {
|
||||
$streamBase .= $this->streamHost;
|
||||
}
|
||||
|
||||
$uploads['path'] = $streamBase . $streamSubdir;
|
||||
$uploads['basedir'] = $streamBase;
|
||||
|
|
@ -71,11 +79,13 @@ final class MediaHooks {
|
|||
$uploads['flysystem_protocol'] = $this->protocol;
|
||||
$uploads['flysystem_host'] = $this->streamHost;
|
||||
$uploads['flysystem_root_prefix'] = $this->streamRootPrefix;
|
||||
$uploads['flysystem_provider_prefix'] = $this->providerPrefix;
|
||||
|
||||
error_log(sprintf(
|
||||
'[MediaHooks] Upload dir filtered - path: %s, url: %s',
|
||||
'[MediaHooks] Upload dir filtered - path: %s, url: %s, subdir: %s',
|
||||
$uploads['path'],
|
||||
$uploads['url']
|
||||
$uploads['url'],
|
||||
$uploads['subdir']
|
||||
));
|
||||
|
||||
return $uploads;
|
||||
|
|
@ -92,15 +102,6 @@ final class MediaHooks {
|
|||
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) {
|
||||
|
|
@ -188,25 +189,37 @@ final class MediaHooks {
|
|||
return $directory === '.' ? '' : $directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte una ruta relativa de WordPress a la ruta remota en el filesystem
|
||||
*
|
||||
* Para WebDAV con prefix, solo usa el archivo relativo
|
||||
* Para S3 u otros, puede incluir streamRootPrefix y streamHost
|
||||
*/
|
||||
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;
|
||||
// ✅ Si hay providerPrefix (WebDAV, S3, etc.), NO agregar streamRootPrefix ni streamHost
|
||||
// El prefix del provider ya incluye la ruta base completa
|
||||
if ($this->providerPrefix === '') {
|
||||
// Solo para providers sin prefix (local, etc.)
|
||||
if ($this->streamRootPrefix !== '') {
|
||||
$segments[] = $this->streamRootPrefix;
|
||||
}
|
||||
|
||||
if ($this->streamHost !== '') {
|
||||
$segments[] = $this->streamHost;
|
||||
}
|
||||
}
|
||||
|
||||
// Agregar el archivo relativo
|
||||
$segments[] = $file;
|
||||
|
||||
$remotePath = PathHelper::join(...$segments);
|
||||
|
||||
error_log(sprintf(
|
||||
'[MediaHooks] toRemotePath - file: %s, remote: %s',
|
||||
'[MediaHooks] toRemotePath - file: %s, provider_prefix: %s, remote: %s',
|
||||
$file,
|
||||
$this->providerPrefix,
|
||||
$remotePath
|
||||
));
|
||||
|
||||
|
|
@ -222,31 +235,6 @@ final class MediaHooks {
|
|||
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, '/');
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue