3.0.0
This commit is contained in:
parent
56d96b32cb
commit
79fa886ee1
|
|
@ -1,7 +1,14 @@
|
||||||
{
|
{
|
||||||
"name": "brasdrive/flysystem-offload",
|
"name": "brasdrive/flysystem-offload",
|
||||||
"description": "Universal storage offloading for WordPress vía Flysystem",
|
"description": "Universal storage offloading for WordPress via Flysystem",
|
||||||
"type": "wordpress-plugin",
|
"type": "wordpress-plugin",
|
||||||
|
"license": "GPL-2.0-or-later",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Brasdrive",
|
||||||
|
"email": "jdavidcamejo@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.1",
|
"php": ">=8.1",
|
||||||
"league/flysystem": "^3.24",
|
"league/flysystem": "^3.24",
|
||||||
|
|
@ -16,9 +23,36 @@
|
||||||
"azure-oss/storage-blob-flysystem": "^1.3.0",
|
"azure-oss/storage-blob-flysystem": "^1.3.0",
|
||||||
"sabre/dav": "^4.5"
|
"sabre/dav": "^4.5"
|
||||||
},
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^10.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.7"
|
||||||
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"FlysystemOffload\\": "src/"
|
"FlysystemOffload\\": "src/"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"FlysystemOffload\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"optimize-autoloader": true,
|
||||||
|
"sort-packages": true,
|
||||||
|
"allow-plugins": {
|
||||||
|
"composer/installers": true
|
||||||
|
},
|
||||||
|
"platform": {
|
||||||
|
"php": "8.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "phpunit",
|
||||||
|
"cs": "phpcs --standard=PSR12 src/",
|
||||||
|
"cbf": "phpcbf --standard=PSR12 src/",
|
||||||
|
"dump": "composer dump-autoload --optimize"
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,530 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace FlysystemOffload\Config;
|
namespace FlysystemOffload\Config;
|
||||||
|
|
||||||
use RuntimeException;
|
use InvalidArgumentException;
|
||||||
use UnexpectedValueException;
|
|
||||||
|
|
||||||
final class ConfigLoader {
|
/**
|
||||||
private string $configDirectory;
|
* Cargador de configuración para Flysystem Offload
|
||||||
|
*/
|
||||||
|
class ConfigLoader
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Carga la configuración desde archivo o base de datos
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function load(): array
|
||||||
|
{
|
||||||
|
// Intentar cargar desde archivo de configuración primero
|
||||||
|
$fileConfig = self::loadFromFile();
|
||||||
|
|
||||||
public function __construct(string $configDirectory) {
|
// Cargar desde opciones de WordPress
|
||||||
$this->configDirectory = rtrim($configDirectory, '/\\');
|
$dbConfig = self::loadFromDatabase();
|
||||||
|
|
||||||
|
// Merge con prioridad a la base de datos sobre el archivo
|
||||||
|
// Si hay configuración en BD, usar esa; si no, usar archivo
|
||||||
|
$config = !empty($dbConfig['provider'])
|
||||||
|
? array_merge($fileConfig, $dbConfig)
|
||||||
|
: $fileConfig;
|
||||||
|
|
||||||
|
// Normalizar configuración
|
||||||
|
return self::normalize($config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function load(): array {
|
/**
|
||||||
$candidateFiles = [
|
* Carga configuración desde archivo PHP
|
||||||
$this->configDirectory . '/flysystem-offload.local.php',
|
*
|
||||||
$this->configDirectory . '/flysystem-offload.php',
|
* @return array
|
||||||
$this->configDirectory . '/flysystem-offload.example.php',
|
*/
|
||||||
];
|
private static function loadFromFile(): array
|
||||||
|
{
|
||||||
|
$configFile = defined('FLYSYSTEM_OFFLOAD_CONFIG_PATH')
|
||||||
|
? FLYSYSTEM_OFFLOAD_CONFIG_PATH . '/flysystem-offload.php'
|
||||||
|
: '';
|
||||||
|
|
||||||
$configFile = $this->resolveFirstExisting($candidateFiles);
|
if (empty($configFile) || !file_exists($configFile)) {
|
||||||
|
error_log('[Flysystem Offload] Config file not found: ' . $configFile);
|
||||||
if ($configFile === null) {
|
return [];
|
||||||
throw new RuntimeException(
|
|
||||||
sprintf(
|
|
||||||
'No se pudo localizar un archivo de configuración para Flysystem Offload. Esperado en: %s',
|
|
||||||
implode(', ', $candidateFiles)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$config = include $configFile;
|
$rawConfig = require $configFile;
|
||||||
|
|
||||||
if ($config instanceof \Closure) {
|
if (!is_array($rawConfig)) {
|
||||||
$config = $config();
|
error_log('[Flysystem Offload] Config file must return an array');
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! is_array($config)) {
|
error_log('[Flysystem Offload] Loaded config from file: ' . print_r($rawConfig, true));
|
||||||
throw new UnexpectedValueException(
|
|
||||||
sprintf('El archivo de configuración debe retornar un array. Archivo: %s', $configFile)
|
// Convertir estructura del archivo al formato esperado
|
||||||
);
|
return self::normalizeFileConfig($rawConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normaliza la configuración del archivo al formato esperado
|
||||||
|
*
|
||||||
|
* @param array $rawConfig Configuración raw del archivo
|
||||||
|
* @return array Configuración normalizada
|
||||||
|
*/
|
||||||
|
private static function normalizeFileConfig(array $rawConfig): array
|
||||||
|
{
|
||||||
|
$config = [];
|
||||||
|
|
||||||
|
// Convertir 'driver' a 'provider'
|
||||||
|
if (isset($rawConfig['driver'])) {
|
||||||
|
$config['provider'] = $rawConfig['driver'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extraer prefijo global si existe
|
||||||
|
if (isset($rawConfig['stream']['root_prefix'])) {
|
||||||
|
$config['prefix'] = $rawConfig['stream']['root_prefix'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalizar configuración según el driver/provider
|
||||||
|
$provider = $config['provider'] ?? '';
|
||||||
|
|
||||||
|
switch ($provider) {
|
||||||
|
case 's3':
|
||||||
|
if (isset($rawConfig['s3'])) {
|
||||||
|
$s3Config = $rawConfig['s3'];
|
||||||
|
$config['key'] = $s3Config['key'] ?? '';
|
||||||
|
$config['secret'] = $s3Config['secret'] ?? '';
|
||||||
|
$config['region'] = $s3Config['region'] ?? 'us-east-1';
|
||||||
|
$config['bucket'] = $s3Config['bucket'] ?? '';
|
||||||
|
$config['endpoint'] = $s3Config['endpoint'] ?? '';
|
||||||
|
$config['use_path_style_endpoint'] = $s3Config['use_path_style_endpoint'] ?? false;
|
||||||
|
|
||||||
|
// Usar prefix específico de S3 si existe, sino el global
|
||||||
|
if (isset($s3Config['prefix'])) {
|
||||||
|
$config['prefix'] = $s3Config['prefix'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// CDN URL desde uploads.base_url
|
||||||
|
if (isset($rawConfig['uploads']['base_url'])) {
|
||||||
|
$config['cdn_url'] = $rawConfig['uploads']['base_url'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'webdav':
|
||||||
|
if (isset($rawConfig['webdav'])) {
|
||||||
|
$webdavConfig = $rawConfig['webdav'];
|
||||||
|
|
||||||
|
// Determinar base_uri desde endpoint o base_url
|
||||||
|
$config['base_uri'] = $webdavConfig['endpoint']
|
||||||
|
?? $webdavConfig['base_url']
|
||||||
|
?? '';
|
||||||
|
|
||||||
|
// Credenciales
|
||||||
|
if (isset($webdavConfig['credentials'])) {
|
||||||
|
$config['username'] = $webdavConfig['credentials']['username'] ?? '';
|
||||||
|
$config['password'] = $webdavConfig['credentials']['password'] ?? '';
|
||||||
|
$config['auth_type'] = $webdavConfig['credentials']['auth_type'] ?? 'basic';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix específico de WebDAV
|
||||||
|
if (isset($webdavConfig['prefix'])) {
|
||||||
|
$config['prefix'] = $webdavConfig['prefix'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permisos (usar valores por defecto si no están definidos)
|
||||||
|
$config['file_public'] = '0644';
|
||||||
|
$config['file_private'] = '0600';
|
||||||
|
$config['dir_public'] = '0755';
|
||||||
|
$config['dir_private'] = '0700';
|
||||||
|
|
||||||
|
// Visibilidad por defecto
|
||||||
|
if (isset($webdavConfig['default_visibility'])) {
|
||||||
|
$config['default_visibility'] = $webdavConfig['default_visibility'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'sftp':
|
||||||
|
if (isset($rawConfig['sftp'])) {
|
||||||
|
$sftpConfig = $rawConfig['sftp'];
|
||||||
|
$config['host'] = $sftpConfig['host'] ?? '';
|
||||||
|
$config['port'] = $sftpConfig['port'] ?? 22;
|
||||||
|
$config['username'] = $sftpConfig['username'] ?? '';
|
||||||
|
$config['password'] = $sftpConfig['password'] ?? '';
|
||||||
|
$config['private_key'] = $sftpConfig['private_key'] ?? '';
|
||||||
|
$config['passphrase'] = $sftpConfig['passphrase'] ?? '';
|
||||||
|
$config['root'] = $sftpConfig['root'] ?? '/';
|
||||||
|
$config['timeout'] = $sftpConfig['timeout'] ?? 10;
|
||||||
|
|
||||||
|
// Permisos
|
||||||
|
$config['file_public'] = $sftpConfig['file_public'] ?? '0644';
|
||||||
|
$config['file_private'] = $sftpConfig['file_private'] ?? '0600';
|
||||||
|
$config['dir_public'] = $sftpConfig['dir_public'] ?? '0755';
|
||||||
|
$config['dir_private'] = $sftpConfig['dir_private'] ?? '0700';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log('[Flysystem Offload] Normalized file config: ' . print_r($config, true));
|
||||||
|
|
||||||
return $config;
|
return $config;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveFirstExisting(array $files): ?string {
|
/**
|
||||||
foreach ($files as $file) {
|
* Carga configuración desde la base de datos de WordPress
|
||||||
if ($file && is_readable($file)) {
|
*
|
||||||
return $file;
|
* @return array
|
||||||
|
*/
|
||||||
|
private static function loadFromDatabase(): array
|
||||||
|
{
|
||||||
|
$provider = get_option('flysystem_offload_provider', '');
|
||||||
|
|
||||||
|
if (empty($provider)) {
|
||||||
|
error_log('[Flysystem Offload] No provider found in database');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log('[Flysystem Offload] Loading config from database for provider: ' . $provider);
|
||||||
|
|
||||||
|
$config = [
|
||||||
|
'provider' => $provider,
|
||||||
|
'prefix' => get_option('flysystem_offload_prefix', ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Cargar configuración específica del proveedor
|
||||||
|
switch ($provider) {
|
||||||
|
case 's3':
|
||||||
|
$config = array_merge($config, self::loadS3Config());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'webdav':
|
||||||
|
$config = array_merge($config, self::loadWebdavConfig());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'sftp':
|
||||||
|
$config = array_merge($config, self::loadSftpConfig());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'gcs':
|
||||||
|
$config = array_merge($config, self::loadGcsConfig());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'azure':
|
||||||
|
$config = array_merge($config, self::loadAzureConfig());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'dropbox':
|
||||||
|
$config = array_merge($config, self::loadDropboxConfig());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'google-drive':
|
||||||
|
$config = array_merge($config, self::loadGoogleDriveConfig());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'onedrive':
|
||||||
|
$config = array_merge($config, self::loadOneDriveConfig());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log('[Flysystem Offload] Database config loaded: ' . print_r($config, true));
|
||||||
|
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carga configuración de S3
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private static function loadS3Config(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'key' => get_option('flysystem_offload_s3_key', ''),
|
||||||
|
'secret' => get_option('flysystem_offload_s3_secret', ''),
|
||||||
|
'region' => get_option('flysystem_offload_s3_region', 'us-east-1'),
|
||||||
|
'bucket' => get_option('flysystem_offload_s3_bucket', ''),
|
||||||
|
'endpoint' => get_option('flysystem_offload_s3_endpoint', ''),
|
||||||
|
'use_path_style_endpoint' => (bool) get_option('flysystem_offload_s3_path_style', false),
|
||||||
|
'cdn_url' => get_option('flysystem_offload_s3_cdn_url', ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carga configuración de WebDAV
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private static function loadWebdavConfig(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'base_uri' => get_option('flysystem_offload_webdav_base_uri', ''),
|
||||||
|
'username' => get_option('flysystem_offload_webdav_username', ''),
|
||||||
|
'password' => get_option('flysystem_offload_webdav_password', ''),
|
||||||
|
'auth_type' => get_option('flysystem_offload_webdav_auth_type', 'basic'),
|
||||||
|
'prefix' => get_option('flysystem_offload_webdav_prefix', ''),
|
||||||
|
'file_public' => self::normalizePermissionFromDb(
|
||||||
|
get_option('flysystem_offload_webdav_file_public', '0644')
|
||||||
|
),
|
||||||
|
'file_private' => self::normalizePermissionFromDb(
|
||||||
|
get_option('flysystem_offload_webdav_file_private', '0600')
|
||||||
|
),
|
||||||
|
'dir_public' => self::normalizePermissionFromDb(
|
||||||
|
get_option('flysystem_offload_webdav_dir_public', '0755')
|
||||||
|
),
|
||||||
|
'dir_private' => self::normalizePermissionFromDb(
|
||||||
|
get_option('flysystem_offload_webdav_dir_private', '0700')
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carga configuración de SFTP
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private static function loadSftpConfig(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'host' => get_option('flysystem_offload_sftp_host', ''),
|
||||||
|
'port' => (int) get_option('flysystem_offload_sftp_port', 22),
|
||||||
|
'username' => get_option('flysystem_offload_sftp_username', ''),
|
||||||
|
'password' => get_option('flysystem_offload_sftp_password', ''),
|
||||||
|
'private_key' => get_option('flysystem_offload_sftp_private_key', ''),
|
||||||
|
'passphrase' => get_option('flysystem_offload_sftp_passphrase', ''),
|
||||||
|
'root' => get_option('flysystem_offload_sftp_root', '/'),
|
||||||
|
'timeout' => (int) get_option('flysystem_offload_sftp_timeout', 10),
|
||||||
|
'file_public' => self::normalizePermissionFromDb(
|
||||||
|
get_option('flysystem_offload_sftp_file_public', '0644')
|
||||||
|
),
|
||||||
|
'file_private' => self::normalizePermissionFromDb(
|
||||||
|
get_option('flysystem_offload_sftp_file_private', '0600')
|
||||||
|
),
|
||||||
|
'dir_public' => self::normalizePermissionFromDb(
|
||||||
|
get_option('flysystem_offload_sftp_dir_public', '0755')
|
||||||
|
),
|
||||||
|
'dir_private' => self::normalizePermissionFromDb(
|
||||||
|
get_option('flysystem_offload_sftp_dir_private', '0700')
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carga configuración de Google Cloud Storage
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private static function loadGcsConfig(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'project_id' => get_option('flysystem_offload_gcs_project_id', ''),
|
||||||
|
'key_file' => get_option('flysystem_offload_gcs_key_file', ''),
|
||||||
|
'bucket' => get_option('flysystem_offload_gcs_bucket', ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carga configuración de Azure Blob Storage
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private static function loadAzureConfig(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'account_name' => get_option('flysystem_offload_azure_account_name', ''),
|
||||||
|
'account_key' => get_option('flysystem_offload_azure_account_key', ''),
|
||||||
|
'container' => get_option('flysystem_offload_azure_container', ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carga configuración de Dropbox
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private static function loadDropboxConfig(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'access_token' => get_option('flysystem_offload_dropbox_access_token', ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carga configuración de Google Drive
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private static function loadGoogleDriveConfig(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'client_id' => get_option('flysystem_offload_gdrive_client_id', ''),
|
||||||
|
'client_secret' => get_option('flysystem_offload_gdrive_client_secret', ''),
|
||||||
|
'refresh_token' => get_option('flysystem_offload_gdrive_refresh_token', ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carga configuración de OneDrive
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private static function loadOneDriveConfig(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'client_id' => get_option('flysystem_offload_onedrive_client_id', ''),
|
||||||
|
'client_secret' => get_option('flysystem_offload_onedrive_client_secret', ''),
|
||||||
|
'refresh_token' => get_option('flysystem_offload_onedrive_refresh_token', ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normaliza un permiso desde la base de datos
|
||||||
|
* Mantiene como string para que el adaptador lo convierta correctamente
|
||||||
|
*
|
||||||
|
* @param mixed $permission Permiso desde la BD
|
||||||
|
* @return string|int Permiso normalizado
|
||||||
|
*/
|
||||||
|
private static function normalizePermissionFromDb($permission)
|
||||||
|
{
|
||||||
|
if (is_int($permission)) {
|
||||||
|
return $permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($permission)) {
|
||||||
|
$permission = trim($permission);
|
||||||
|
|
||||||
|
// Si ya tiene el formato correcto, retornar
|
||||||
|
if (preg_match('/^0[0-7]{3}$/', $permission)) {
|
||||||
|
return $permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si es solo dígitos sin el 0 inicial, añadirlo
|
||||||
|
if (preg_match('/^[0-7]{3}$/', $permission)) {
|
||||||
|
return '0' . $permission;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
// Valor por defecto
|
||||||
|
return '0644';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normaliza la configuración completa
|
||||||
|
*
|
||||||
|
* @param array $config Configuración a normalizar
|
||||||
|
* @return array Configuración normalizada
|
||||||
|
*/
|
||||||
|
private static function normalize(array $config): array
|
||||||
|
{
|
||||||
|
// Eliminar valores vacíos excepto 0 y false
|
||||||
|
$config = array_filter($config, function ($value) {
|
||||||
|
return $value !== '' && $value !== null && $value !== [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Normalizar provider
|
||||||
|
if (isset($config['provider'])) {
|
||||||
|
$config['provider'] = strtolower(trim($config['provider']));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalizar prefix (eliminar barras al inicio y final)
|
||||||
|
if (isset($config['prefix'])) {
|
||||||
|
$config['prefix'] = trim($config['prefix'], '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log('[Flysystem Offload] Final normalized config: ' . print_r($config, true));
|
||||||
|
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida que la configuración sea válida
|
||||||
|
*
|
||||||
|
* @param array $config Configuración a validar
|
||||||
|
* @return bool
|
||||||
|
* @throws InvalidArgumentException Si la configuración es inválida
|
||||||
|
*/
|
||||||
|
public static function validate(array $config): bool
|
||||||
|
{
|
||||||
|
if (empty($config['provider'])) {
|
||||||
|
throw new InvalidArgumentException('Provider is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$provider = $config['provider'];
|
||||||
|
|
||||||
|
error_log('[Flysystem Offload] Validating config for provider: ' . $provider);
|
||||||
|
|
||||||
|
// Validar configuración específica del proveedor
|
||||||
|
switch ($provider) {
|
||||||
|
case 's3':
|
||||||
|
self::validateS3Config($config);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'webdav':
|
||||||
|
self::validateWebdavConfig($config);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'sftp':
|
||||||
|
self::validateSftpConfig($config);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log('[Flysystem Offload] Config validation passed');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida configuración de S3
|
||||||
|
*
|
||||||
|
* @param array $config
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
private static function validateS3Config(array $config): void
|
||||||
|
{
|
||||||
|
$required = ['key', 'secret', 'region', 'bucket'];
|
||||||
|
|
||||||
|
foreach ($required as $key) {
|
||||||
|
if (empty($config[$key])) {
|
||||||
|
throw new InvalidArgumentException("S3 {$key} is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida configuración de WebDAV
|
||||||
|
*
|
||||||
|
* @param array $config
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
private static function validateWebdavConfig(array $config): void
|
||||||
|
{
|
||||||
|
if (empty($config['base_uri'])) {
|
||||||
|
throw new InvalidArgumentException('WebDAV base_uri is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filter_var($config['base_uri'], FILTER_VALIDATE_URL)) {
|
||||||
|
throw new InvalidArgumentException('WebDAV base_uri must be a valid URL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida configuración de SFTP
|
||||||
|
*
|
||||||
|
* @param array $config
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
private static function validateSftpConfig(array $config): void
|
||||||
|
{
|
||||||
|
if (empty($config['host'])) {
|
||||||
|
throw new InvalidArgumentException('SFTP host is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($config['username'])) {
|
||||||
|
throw new InvalidArgumentException('SFTP username is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($config['password']) && empty($config['private_key'])) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'SFTP password or private_key is required'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,39 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
|
||||||
|
declare(strict_types=1);
|
||||||
namespace FlysystemOffload\Filesystem;
|
|
||||||
|
namespace FlysystemOffload\Filesystem;
|
||||||
use League\Flysystem\FilesystemAdapter;
|
|
||||||
|
use League\Flysystem\FilesystemAdapter;
|
||||||
interface AdapterInterface {
|
|
||||||
public function createAdapter(array $config): FilesystemAdapter;
|
/**
|
||||||
}
|
* Interfaz para adaptadores de Flysystem Offload
|
||||||
|
*
|
||||||
|
* Todos los adaptadores deben implementar esta interfaz para garantizar
|
||||||
|
* consistencia en la creación y configuración.
|
||||||
|
*/
|
||||||
|
interface AdapterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Crea y configura el adaptador de Flysystem
|
||||||
|
*
|
||||||
|
* @param array $config Configuración del adaptador
|
||||||
|
* @return FilesystemAdapter Instancia del adaptador configurado
|
||||||
|
* @throws \InvalidArgumentException Si la configuración es inválida
|
||||||
|
*/
|
||||||
|
public function createAdapter(array $config): FilesystemAdapter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene las claves de configuración requeridas
|
||||||
|
*
|
||||||
|
* @return array Lista de claves requeridas
|
||||||
|
*/
|
||||||
|
public function getRequiredConfigKeys(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene las claves de configuración opcionales
|
||||||
|
*
|
||||||
|
* @return array Lista de claves opcionales
|
||||||
|
*/
|
||||||
|
public function getOptionalConfigKeys(): array;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,158 +4,230 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace FlysystemOffload\Filesystem\Adapters;
|
namespace FlysystemOffload\Filesystem\Adapters;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
|
||||||
use League\Flysystem\FilesystemAdapter;
|
use League\Flysystem\FilesystemAdapter;
|
||||||
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
|
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
|
||||||
use League\Flysystem\Visibility;
|
use League\Flysystem\Visibility;
|
||||||
use League\Flysystem\WebDAV\WebDAVAdapter as LeagueWebDAVAdapter;
|
use League\Flysystem\WebDAV\WebDAVAdapter as FlysystemWebDAVAdapter;
|
||||||
use Sabre\DAV\Client;
|
use Sabre\DAV\Client;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
final class WebdavAdapter
|
/**
|
||||||
|
* Adaptador WebDAV para Flysystem Offload
|
||||||
|
*
|
||||||
|
* Proporciona integración con servidores WebDAV usando Sabre/DAV
|
||||||
|
*/
|
||||||
|
class WebdavAdapter implements AdapterInterface
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Crea y configura el adaptador WebDAV
|
||||||
|
*
|
||||||
|
* @param array $config Configuración del adaptador
|
||||||
|
* @return FilesystemAdapter
|
||||||
|
* @throws InvalidArgumentException Si la configuración es inválida
|
||||||
|
*/
|
||||||
public function createAdapter(array $config): FilesystemAdapter
|
public function createAdapter(array $config): FilesystemAdapter
|
||||||
{
|
{
|
||||||
if (! isset($config['webdav']) || ! is_array($config['webdav'])) {
|
$this->validateConfig($config);
|
||||||
throw new InvalidArgumentException('La configuración de WebDAV no está definida.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$webdav = $config['webdav'];
|
// Normalizar permisos de string a integer octal
|
||||||
|
$filePublic = $this->normalizePermission($config['file_public'] ?? '0644');
|
||||||
$endpoint = $this->requiredString($webdav, 'endpoint', 'webdav.endpoint');
|
$filePrivate = $this->normalizePermission($config['file_private'] ?? '0600');
|
||||||
$credentials = $webdav['credentials'] ?? [];
|
$dirPublic = $this->normalizePermission($config['dir_public'] ?? '0755');
|
||||||
|
$dirPrivate = $this->normalizePermission($config['dir_private'] ?? '0700');
|
||||||
$username = $this->requiredString($credentials, 'username', 'webdav.credentials.username');
|
|
||||||
$password = $this->requiredString($credentials, 'password', 'webdav.credentials.password');
|
|
||||||
|
|
||||||
|
// Configurar cliente WebDAV
|
||||||
$clientConfig = [
|
$clientConfig = [
|
||||||
'baseUri' => $this->normaliseEndpoint($endpoint),
|
'baseUri' => rtrim($config['base_uri'], '/') . '/',
|
||||||
'userName' => $username,
|
|
||||||
'password' => $password,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$authType = $credentials['auth_type'] ?? null;
|
// Añadir autenticación si está configurada
|
||||||
if (is_string($authType) && $authType !== '') {
|
if (!empty($config['username'])) {
|
||||||
$clientConfig['authType'] = $authType;
|
$clientConfig['userName'] = $config['username'];
|
||||||
|
$clientConfig['password'] = $config['password'] ?? '';
|
||||||
|
$clientConfig['authType'] = $this->normalizeAuthType($config['auth_type'] ?? 'basic');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! empty($webdav['default_headers']) && is_array($webdav['default_headers'])) {
|
// Configuración adicional opcional
|
||||||
$clientConfig['headers'] = $this->normaliseHeaders($webdav['default_headers']);
|
if (isset($config['encoding'])) {
|
||||||
}
|
$clientConfig['encoding'] = $config['encoding'];
|
||||||
|
|
||||||
if (! empty($webdav['timeout'])) {
|
|
||||||
$clientConfig['timeout'] = (int) $webdav['timeout'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! empty($webdav['curl_options']) && is_array($webdav['curl_options'])) {
|
|
||||||
$clientConfig['curl.options'] = $webdav['curl_options'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$client = new Client($clientConfig);
|
$client = new Client($clientConfig);
|
||||||
|
|
||||||
$prefix = '';
|
// Crear convertidor de visibilidad con permisos normalizados
|
||||||
if (! empty($webdav['prefix'])) {
|
$visibility = new PortableVisibilityConverter(
|
||||||
$prefix = trim((string) $webdav['prefix'], '/');
|
$filePublic,
|
||||||
}
|
$filePrivate,
|
||||||
|
$dirPublic,
|
||||||
$defaultVisibility = $webdav['default_visibility'] ?? ($config['visibility'] ?? Visibility::PRIVATE);
|
$dirPrivate,
|
||||||
$visibility = $this->normaliseVisibility($defaultVisibility);
|
Visibility::PRIVATE // Visibilidad por defecto
|
||||||
|
|
||||||
$permissions = $this->normalisePermissions($webdav['permissions'] ?? []);
|
|
||||||
|
|
||||||
$directoryDefault = $visibility === Visibility::PUBLIC ? 0755 : 0700;
|
|
||||||
|
|
||||||
$visibilityConverter = PortableVisibilityConverter::fromArray(
|
|
||||||
$permissions,
|
|
||||||
$directoryDefault
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return new LeagueWebDAVAdapter($client, $prefix, $visibilityConverter);
|
// Crear y retornar el adaptador
|
||||||
|
return new FlysystemWebDAVAdapter(
|
||||||
|
$client,
|
||||||
|
$config['prefix'] ?? '',
|
||||||
|
$visibility
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function requiredString(array $config, string $key, ?string $path = null): string
|
/**
|
||||||
|
* Convierte permisos de string octal a integer
|
||||||
|
*
|
||||||
|
* @param mixed $permission Permiso en formato string u octal
|
||||||
|
* @return int Permiso como integer octal
|
||||||
|
* @throws InvalidArgumentException Si el formato es inválido
|
||||||
|
*/
|
||||||
|
private function normalizePermission($permission): int
|
||||||
{
|
{
|
||||||
$value = $config[$key] ?? null;
|
// Si ya es integer, retornar directamente
|
||||||
|
if (is_int($permission)) {
|
||||||
if (! is_string($value) || trim($value) === '') {
|
return $permission;
|
||||||
$path ??= $key;
|
|
||||||
|
|
||||||
throw new InvalidArgumentException(sprintf(
|
|
||||||
'El valor "%s" es obligatorio en la configuración de WebDAV.',
|
|
||||||
$path
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $value;
|
// Si es string, convertir de octal a decimal
|
||||||
}
|
if (is_string($permission)) {
|
||||||
|
// Limpiar espacios
|
||||||
private function normaliseEndpoint(string $endpoint): string
|
$permission = trim($permission);
|
||||||
{
|
|
||||||
return rtrim(trim($endpoint), '/') . '/';
|
// Si comienza con '0', es octal
|
||||||
}
|
if (str_starts_with($permission, '0')) {
|
||||||
|
$result = octdec($permission);
|
||||||
private function normaliseHeaders(array $headers): array
|
if ($result === false || $result === 0 && $permission !== '0' && $permission !== '0000') {
|
||||||
{
|
throw new InvalidArgumentException(
|
||||||
$normalised = [];
|
"Invalid octal permission format: {$permission}"
|
||||||
|
);
|
||||||
foreach ($headers as $header => $value) {
|
}
|
||||||
if (! is_string($header) || $header === '') {
|
return $result;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$normalised[$header] = is_array($value) ? implode(',', $value) : (string) $value;
|
// Si es decimal como '644', añadir el 0 y convertir
|
||||||
|
if (ctype_digit($permission)) {
|
||||||
|
return octdec('0' . $permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"Permission must be an octal string (e.g., '0644') or integer"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $normalised;
|
throw new InvalidArgumentException(
|
||||||
|
'Permission must be an integer or string, ' . gettype($permission) . ' given'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normaliseVisibility(string $visibility): string
|
/**
|
||||||
|
* Normaliza el tipo de autenticación
|
||||||
|
*
|
||||||
|
* @param string|int $authType Tipo de autenticación
|
||||||
|
* @return int Constante de autenticación de Sabre\DAV\Client
|
||||||
|
*/
|
||||||
|
private function normalizeAuthType($authType): int
|
||||||
{
|
{
|
||||||
return strtolower($visibility) === Visibility::PUBLIC
|
if (is_int($authType)) {
|
||||||
? Visibility::PUBLIC
|
return $authType;
|
||||||
: Visibility::PRIVATE;
|
}
|
||||||
|
|
||||||
|
$authType = strtolower(trim($authType));
|
||||||
|
|
||||||
|
return match ($authType) {
|
||||||
|
'basic' => Client::AUTH_BASIC,
|
||||||
|
'digest' => Client::AUTH_DIGEST,
|
||||||
|
'ntlm' => Client::AUTH_NTLM,
|
||||||
|
default => Client::AUTH_BASIC,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalisePermissions(array $permissions): array
|
/**
|
||||||
|
* Valida la configuración del adaptador
|
||||||
|
*
|
||||||
|
* @param array $config Configuración a validar
|
||||||
|
* @throws InvalidArgumentException Si falta configuración requerida
|
||||||
|
*/
|
||||||
|
private function validateConfig(array $config): void
|
||||||
{
|
{
|
||||||
$defaults = [
|
// Validar base_uri requerido
|
||||||
'file' => [
|
if (empty($config['base_uri'])) {
|
||||||
Visibility::PUBLIC => 0644,
|
throw new InvalidArgumentException('WebDAV base_uri is required');
|
||||||
Visibility::PRIVATE => 0600,
|
}
|
||||||
],
|
|
||||||
'dir' => [
|
// Validar formato de URL
|
||||||
Visibility::PUBLIC => 0755,
|
if (!filter_var($config['base_uri'], FILTER_VALIDATE_URL)) {
|
||||||
Visibility::PRIVATE => 0700,
|
throw new InvalidArgumentException(
|
||||||
],
|
'Invalid WebDAV base_uri format. Must be a valid URL (e.g., https://webdav.example.com/remote.php/dav/files/username/)'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar que sea HTTP o HTTPS
|
||||||
|
$scheme = parse_url($config['base_uri'], PHP_URL_SCHEME);
|
||||||
|
if (!in_array($scheme, ['http', 'https'], true)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'WebDAV base_uri must use http:// or https:// scheme'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si hay username, debe haber password
|
||||||
|
if (!empty($config['username']) && !isset($config['password'])) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'WebDAV password is required when username is provided'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene las claves de configuración requeridas
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getRequiredConfigKeys(): array
|
||||||
|
{
|
||||||
|
return ['base_uri'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene las claves de configuración opcionales
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getOptionalConfigKeys(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'username',
|
||||||
|
'password',
|
||||||
|
'auth_type',
|
||||||
|
'prefix',
|
||||||
|
'file_public',
|
||||||
|
'file_private',
|
||||||
|
'dir_public',
|
||||||
|
'dir_private',
|
||||||
|
'encoding',
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
foreach (['file', 'dir'] as $type) {
|
/**
|
||||||
if (! isset($permissions[$type]) || ! is_array($permissions[$type])) {
|
* Obtiene la descripción del adaptador
|
||||||
continue;
|
*
|
||||||
}
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'WebDAV storage adapter for Nextcloud, ownCloud, and other WebDAV-compatible servers';
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($permissions[$type] as $visibility => $mode) {
|
/**
|
||||||
$visibility = strtolower((string) $visibility);
|
* Obtiene valores por defecto para la configuración
|
||||||
|
*
|
||||||
if ($visibility !== Visibility::PUBLIC && $visibility !== Visibility::PRIVATE) {
|
* @return array
|
||||||
continue;
|
*/
|
||||||
}
|
public function getDefaultConfig(): array
|
||||||
|
{
|
||||||
if (is_string($mode)) {
|
return [
|
||||||
$mode = trim($mode);
|
'auth_type' => 'basic',
|
||||||
|
'prefix' => '',
|
||||||
if ($mode === '') {
|
'file_public' => '0644',
|
||||||
continue;
|
'file_private' => '0600',
|
||||||
}
|
'dir_public' => '0755',
|
||||||
|
'dir_private' => '0700',
|
||||||
$mode = octdec($mode);
|
];
|
||||||
} elseif (! is_int($mode)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$defaults[$type][$visibility] = $mode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $defaults;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,31 +4,232 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace FlysystemOffload\Filesystem;
|
namespace FlysystemOffload\Filesystem;
|
||||||
|
|
||||||
|
use FlysystemOffload\Filesystem\Adapters\AdapterInterface;
|
||||||
use FlysystemOffload\Filesystem\Adapters\S3Adapter;
|
use FlysystemOffload\Filesystem\Adapters\S3Adapter;
|
||||||
use FlysystemOffload\Filesystem\Adapters\WebdavAdapter;
|
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\Filesystem;
|
||||||
use League\Flysystem\FilesystemOperator;
|
use League\Flysystem\FilesystemAdapter;
|
||||||
use League\Flysystem\Visibility;
|
use InvalidArgumentException;
|
||||||
|
|
||||||
final class FilesystemFactory
|
/**
|
||||||
|
* Factory para crear instancias de Filesystem con diferentes adaptadores
|
||||||
|
*/
|
||||||
|
class FilesystemFactory
|
||||||
{
|
{
|
||||||
public function make(array $config): FilesystemOperator
|
/**
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* @param array $config Configuración del filesystem
|
||||||
|
* @return Filesystem
|
||||||
|
* @throws InvalidArgumentException Si el proveedor no es válido
|
||||||
|
*/
|
||||||
|
public static function create(array $config): Filesystem
|
||||||
{
|
{
|
||||||
$driver = $config['driver'] ?? 's3';
|
$provider = $config['provider'] ?? '';
|
||||||
|
|
||||||
$adapter = match ($driver) {
|
if (empty($provider)) {
|
||||||
's3' => (new S3Adapter())->createAdapter($config),
|
throw new InvalidArgumentException('Provider is required in configuration');
|
||||||
'webdav' => (new WebdavAdapter())->createAdapter($config),
|
}
|
||||||
default => throw new \InvalidArgumentException(
|
|
||||||
sprintf('Driver de Flysystem no soportado: "%s".', $driver)
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
$filesystemConfig = [
|
$adapter = self::createAdapter($provider, $config);
|
||||||
'visibility' => $config['visibility'] ?? Visibility::PUBLIC,
|
|
||||||
'directory_visibility' => $config['visibility'] ?? Visibility::PUBLIC,
|
// Aplicar prefijo global si está configurado
|
||||||
|
if (!empty($config['prefix'])) {
|
||||||
|
$adapter = new PrefixedAdapter($adapter, $config['prefix']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Filesystem($adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea un adaptador específico basado en el proveedor
|
||||||
|
*
|
||||||
|
* @param string $provider Nombre del proveedor
|
||||||
|
* @param array $config Configuración del adaptador
|
||||||
|
* @return FilesystemAdapter
|
||||||
|
* @throws InvalidArgumentException Si el proveedor no es soportado
|
||||||
|
*/
|
||||||
|
private static function createAdapter(string $provider, array $config): FilesystemAdapter
|
||||||
|
{
|
||||||
|
$provider = strtolower(trim($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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normaliza la configuración según el proveedor
|
||||||
|
*
|
||||||
|
* @param string $provider Nombre del proveedor
|
||||||
|
* @param array $config Configuración original
|
||||||
|
* @return array Configuración normalizada
|
||||||
|
*/
|
||||||
|
private static function normalizeConfig(string $provider, array $config): array
|
||||||
|
{
|
||||||
|
$normalized = $config;
|
||||||
|
|
||||||
|
// Normalizar permisos para WebDAV y SFTP
|
||||||
|
if (in_array($provider, ['webdav', 'sftp'], true)) {
|
||||||
|
$normalized = self::normalizePermissions($normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalizar configuración específica de S3
|
||||||
|
if ($provider === 's3') {
|
||||||
|
$normalized = self::normalizeS3Config($normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normaliza permisos de archivos y directorios
|
||||||
|
*
|
||||||
|
* @param array $config Configuración original
|
||||||
|
* @return array Configuración con permisos normalizados
|
||||||
|
*/
|
||||||
|
private static function normalizePermissions(array $config): array
|
||||||
|
{
|
||||||
|
$permissionKeys = ['file_public', 'file_private', 'dir_public', 'dir_private'];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
: '',
|
||||||
];
|
];
|
||||||
|
|
||||||
return new Filesystem($adapter, $filesystemConfig);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
272
src/Plugin.php
272
src/Plugin.php
|
|
@ -1,165 +1,159 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace FlysystemOffload;
|
namespace FlysystemOffload;
|
||||||
|
|
||||||
use FlysystemOffload\Admin\HealthCheck;
|
|
||||||
use FlysystemOffload\Config\ConfigLoader;
|
use FlysystemOffload\Config\ConfigLoader;
|
||||||
use FlysystemOffload\Filesystem\FilesystemFactory;
|
use FlysystemOffload\Filesystem\FilesystemFactory;
|
||||||
use FlysystemOffload\Media\MediaHooks;
|
use FlysystemOffload\Media\MediaHooks;
|
||||||
use FlysystemOffload\Settings\SettingsPage;
|
use FlysystemOffload\Settings\SettingsPage;
|
||||||
use FlysystemOffload\StreamWrapper\FlysystemStreamWrapper;
|
use FlysystemOffload\StreamWrapper\FlysystemStreamWrapper;
|
||||||
use League\Flysystem\FilesystemOperator;
|
use League\Flysystem\Filesystem;
|
||||||
use League\Flysystem\Visibility;
|
use Throwable;
|
||||||
|
|
||||||
final class Plugin {
|
/**
|
||||||
private static bool $bootstrapped = false;
|
* Clase principal del plugin Flysystem Offload
|
||||||
private static ?Plugin $instance = null;
|
*/
|
||||||
|
class Plugin
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Instancia del Filesystem
|
||||||
|
*/
|
||||||
|
private static ?Filesystem $filesystem = null;
|
||||||
|
|
||||||
private array $config;
|
/**
|
||||||
private FilesystemOperator $filesystem;
|
* Configuración del plugin
|
||||||
private MediaHooks $mediaHooks;
|
*/
|
||||||
private ?SettingsPage $settingsPage;
|
private static array $config = [];
|
||||||
private HealthCheck $healthCheck;
|
|
||||||
|
|
||||||
private function __construct(
|
/**
|
||||||
array $config,
|
* Indica si el plugin está inicializado
|
||||||
FilesystemOperator $filesystem,
|
*/
|
||||||
MediaHooks $mediaHooks,
|
private static bool $initialized = false;
|
||||||
?SettingsPage $settingsPage,
|
|
||||||
HealthCheck $healthCheck
|
|
||||||
) {
|
|
||||||
$this->config = $config;
|
|
||||||
$this->filesystem = $filesystem;
|
|
||||||
$this->mediaHooks = $mediaHooks;
|
|
||||||
$this->settingsPage = $settingsPage;
|
|
||||||
$this->healthCheck = $healthCheck;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function bootstrap(): void {
|
/**
|
||||||
if (self::$bootstrapped) {
|
* Bootstrap del plugin
|
||||||
|
*
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
public static function bootstrap(): void
|
||||||
|
{
|
||||||
|
if (self::$initialized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$configDirectory = \defined('FLYSYSTEM_OFFLOAD_CONFIG_PATH')
|
try {
|
||||||
? FLYSYSTEM_OFFLOAD_CONFIG_PATH
|
// Cargar configuración
|
||||||
: \dirname(__DIR__) . '/config';
|
self::$config = ConfigLoader::load();
|
||||||
|
|
||||||
$configLoader = new ConfigLoader($configDirectory);
|
// Validar que haya un proveedor configurado
|
||||||
$config = self::normaliseConfig($configLoader->load());
|
if (empty(self::$config['provider'])) {
|
||||||
|
error_log('[Flysystem Offload] No provider configured. Please configure the plugin in Settings > Flysystem Offload');
|
||||||
|
self::registerAdminNotice('No storage provider configured. Please configure the plugin.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$filesystemFactory = new FilesystemFactory();
|
// Validar configuración
|
||||||
$filesystem = $filesystemFactory->make($config);
|
ConfigLoader::validate(self::$config);
|
||||||
|
|
||||||
FlysystemStreamWrapper::register(
|
// Crear filesystem
|
||||||
$filesystem,
|
self::$filesystem = FilesystemFactory::create(self::$config);
|
||||||
$config['stream']['protocol'],
|
|
||||||
$config['stream']['root_prefix'],
|
|
||||||
$config['visibility']
|
|
||||||
);
|
|
||||||
|
|
||||||
$mediaHooks = new MediaHooks($filesystem, $config);
|
// Registrar stream wrapper
|
||||||
$mediaHooks->register();
|
FlysystemStreamWrapper::register(self::$filesystem);
|
||||||
|
|
||||||
$settingsPage = null;
|
// Registrar hooks de medios
|
||||||
if (! empty($config['admin']['enabled']) && \is_admin()) {
|
MediaHooks::register(self::$config);
|
||||||
$settingsPage = new SettingsPage($filesystem, $config);
|
|
||||||
$settingsPage->register();
|
// Registrar página de ajustes
|
||||||
|
if (is_admin()) {
|
||||||
|
SettingsPage::register();
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$initialized = true;
|
||||||
|
|
||||||
|
error_log('[Flysystem Offload] Plugin initialized successfully with provider: ' . self::$config['provider']);
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('[Flysystem Offload] Initialization error: ' . $e->getMessage());
|
||||||
|
error_log('[Flysystem Offload] Stack trace: ' . $e->getTraceAsString());
|
||||||
|
|
||||||
|
self::registerAdminNotice(
|
||||||
|
'Failed to initialize: ' . $e->getMessage()
|
||||||
|
);
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene la instancia del Filesystem
|
||||||
|
*
|
||||||
|
* @return Filesystem|null
|
||||||
|
*/
|
||||||
|
public static function getFilesystem(): ?Filesystem
|
||||||
|
{
|
||||||
|
return self::$filesystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene la configuración del plugin
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getConfig(): array
|
||||||
|
{
|
||||||
|
return self::$config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si el plugin está inicializado
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isInitialized(): bool
|
||||||
|
{
|
||||||
|
return self::$initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registra un aviso de administración
|
||||||
|
*
|
||||||
|
* @param string $message Mensaje a mostrar
|
||||||
|
* @param string $type Tipo de aviso (error, warning, info, success)
|
||||||
|
*/
|
||||||
|
private static function registerAdminNotice(string $message, string $type = 'error'): void
|
||||||
|
{
|
||||||
|
add_action('admin_notices', static function () use ($message, $type): void {
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf(
|
||||||
|
'<div class="notice notice-%s"><p><strong>Flysystem Offload:</strong> %s</p></div>',
|
||||||
|
esc_attr($type),
|
||||||
|
esc_html($message)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconstruye el filesystem (útil después de cambiar configuración)
|
||||||
|
*
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
|
public static function rebuild(): void
|
||||||
|
{
|
||||||
|
self::$initialized = false;
|
||||||
|
self::$filesystem = null;
|
||||||
|
self::$config = [];
|
||||||
|
|
||||||
|
// Desregistrar stream wrapper si existe
|
||||||
|
if (in_array('fly', stream_get_wrappers(), true)) {
|
||||||
|
stream_wrapper_unregister('fly');
|
||||||
}
|
}
|
||||||
|
|
||||||
$healthCheck = new HealthCheck($filesystem, $config);
|
self::bootstrap();
|
||||||
$healthCheck->register();
|
|
||||||
|
|
||||||
self::$instance = new self($config, $filesystem, $mediaHooks, $settingsPage, $healthCheck);
|
|
||||||
self::$bootstrapped = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function instance(): ?self {
|
|
||||||
return self::$instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function config(): array {
|
|
||||||
return $this->config;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function filesystem(): FilesystemOperator {
|
|
||||||
return $this->filesystem;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function normaliseConfig(array $config): array {
|
|
||||||
$defaults = [
|
|
||||||
'driver' => 's3',
|
|
||||||
'visibility' => Visibility::PUBLIC,
|
|
||||||
'cache_ttl' => 900,
|
|
||||||
'stream' => [
|
|
||||||
'protocol' => 'flysystem',
|
|
||||||
'root_prefix' => '',
|
|
||||||
'host' => 'uploads',
|
|
||||||
],
|
|
||||||
'uploads' => [
|
|
||||||
'base_url' => '',
|
|
||||||
'delete_remote' => true,
|
|
||||||
'prefer_local_for_missing' => false,
|
|
||||||
'cache_control' => 'public, max-age=31536000, immutable',
|
|
||||||
'expires' => null,
|
|
||||||
'expires_ttl' => 31536000,
|
|
||||||
],
|
|
||||||
'admin' => [
|
|
||||||
'enabled' => false,
|
|
||||||
],
|
|
||||||
's3' => [
|
|
||||||
'acl_public' => 'public-read',
|
|
||||||
'acl_private' => 'private',
|
|
||||||
'default_options' => [],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
$config = array_replace_recursive($defaults, $config);
|
|
||||||
|
|
||||||
$config['visibility'] = self::normaliseVisibility((string) ($config['visibility'] ?? Visibility::PUBLIC));
|
|
||||||
$config['stream']['protocol'] = self::sanitizeProtocol((string) $config['stream']['protocol']);
|
|
||||||
$config['stream']['root_prefix'] = self::normalizePathSegment((string) $config['stream']['root_prefix']);
|
|
||||||
$config['stream']['host'] = self::normalizePathSegment((string) $config['stream']['host']) ?: 'uploads';
|
|
||||||
|
|
||||||
if (empty($config['uploads']['base_url'])) {
|
|
||||||
$config['uploads']['base_url'] = rtrim(content_url('uploads'), '/');
|
|
||||||
} else {
|
|
||||||
$config['uploads']['base_url'] = rtrim((string) $config['uploads']['base_url'], '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
$config['uploads']['delete_remote'] = (bool) $config['uploads']['delete_remote'];
|
|
||||||
$config['uploads']['prefer_local_for_missing'] = (bool) $config['uploads']['prefer_local_for_missing'];
|
|
||||||
$config['uploads']['cache_control'] = trim((string) $config['uploads']['cache_control']);
|
|
||||||
$config['uploads']['expires'] = $config['uploads']['expires']
|
|
||||||
? trim((string) $config['uploads']['expires'])
|
|
||||||
: null;
|
|
||||||
$config['uploads']['expires_ttl'] = max(0, (int) ($config['uploads']['expires_ttl'] ?? 0));
|
|
||||||
|
|
||||||
$config['s3']['acl_public'] = (string) ($config['s3']['acl_public'] ?? 'public-read');
|
|
||||||
$config['s3']['acl_private'] = (string) ($config['s3']['acl_private'] ?? 'private');
|
|
||||||
$config['s3']['default_options'] = is_array($config['s3']['default_options'])
|
|
||||||
? $config['s3']['default_options']
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return $config;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function normaliseVisibility(string $visibility): string {
|
|
||||||
$visibility = \strtolower($visibility);
|
|
||||||
|
|
||||||
return $visibility === Visibility::PRIVATE
|
|
||||||
? Visibility::PRIVATE
|
|
||||||
: Visibility::PUBLIC;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function sanitizeProtocol(string $protocol): string {
|
|
||||||
$protocol = \preg_replace('/[^A-Za-z0-9_\-]/', '', $protocol) ?? 'flysystem';
|
|
||||||
$protocol = \strtolower($protocol);
|
|
||||||
|
|
||||||
return $protocol !== '' ? $protocol : 'flysystem';
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function normalizePathSegment(string $segment): string {
|
|
||||||
return \trim($segment, " \t\n\r\0\x0B/");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue