### Flysystem Offload Flysystem Offload es un plugin minimalista de WordPress que sustituye el almacenamiento local por un backend remoto usando [Flysystem v3](https://flysystem.thephpleague.com/v3/docs/). Inspirado por el estilo ligero de “S3 Uploads”, se concentra en proporcionar una integración transparente, sin panel de administración, y con compatibilidad total con el flujo estándar de medios de WordPress. ### Características - Registro temprano del stream wrapper para redirigir `wp-content/uploads` a almacenamiento remoto. - Generación automática de URLs públicas coherentes con el prefijo de bucket. - Cabeceras `Cache-Control` y `Expires` aplicadas en las subidas. - Limpieza remota de adjuntos al eliminarlos desde WordPress. - Configuración declarativa vía `config/flysystem-offload.php`. ### Requisitos - WordPress 6.5 o superior. - PHP 8.2 o superior (probado en PHP 8.4). - Extensiones PHP: `json`, `curl`, `mbstring`. - Credenciales válidas para un servicio S3 compatible. - Composer para instalar dependencias (AWS SDK). ### Instalación 1. Clona el repositorio en `wp-content/plugins/flysystem-offload`. 2. Ejecuta `composer install` dentro del plugin si el SDK no está en el proyecto. 3. Copia `config/flysystem-offload.example.php` a `config/flysystem-offload.php`. 4. Ajusta las claves del archivo de configuración. 5. Activa el plugin desde el dashboard de WordPress o vía `wp plugin activate flysystem-offload`. ### Configuración básica (`config/flysystem-offload.php`) ```php 's3', 'visibility' => 'public', 'stream' => [ 'protocol' => 'flysystem', 'root_prefix' => '', 'host' => 'uploads', ], 'uploads' => [ 'base_url' => getenv('FLYSYSTEM_OFFLOAD_BASE_URL') ?: 'https://offload.brasdrive.com.br', 'delete_remote' => true, 'prefer_local_for_missing' => false, 'cache_control' => 'public, max-age=31536000, immutable', 'expires_ttl' => 31536000, 'expires' => null, ], 'admin' => [ 'enabled' => false, ], 's3' => [ 'key' => getenv('AWS_ACCESS_KEY_ID') ?: null, 'secret' => getenv('AWS_SECRET_ACCESS_KEY') ?: null, 'session_token' => getenv('AWS_SESSION_TOKEN') ?: null, 'region' => getenv('AWS_DEFAULT_REGION') ?: 'us-east-1', 'bucket' => getenv('FLYSYSTEM_OFFLOAD_BUCKET') ?: 'your-bucket-name', 'prefix' => getenv('FLYSYSTEM_OFFLOAD_PREFIX') ?: 'wordpress', 'endpoint' => getenv('FLYSYSTEM_OFFLOAD_ENDPOINT') ?: null, 'use_path_style_endpoint' => (bool) (getenv('AWS_USE_PATH_STYLE_ENDPOINT') ?: false), 'version' => 'latest', 'options' => [], 'default_options' => [], 'acl_public' => 'public-read', 'acl_private' => 'private', ], ]; ``` ### Normalización de configuración `Plugin::normaliseConfig()` establece valores por defecto, garantiza que `uploads.base_url` termine sin `/`, y fusiona `s3.prefix`, `stream.root_prefix` y `stream.host` para producir el prefijo remoto final. ### Funcionamiento del stream wrapper - El filtro `upload_dir` devuelve rutas como `flysystem://uploads/2025/01`. - `MediaHooks` reescribe automáticamente `wp_get_attachment_url` para apuntar a la ruta remota (`base_url + prefijo + archivo`). - El prefijo remoto resultante combina `s3.prefix`, `stream.root_prefix` y `stream.host` para reflejar exactamente la clave almacenada en el bucket (ej.: `wordpress/uploads/2025/01/file.jpg`). ### Cabeceras de caché El adaptador S3 añade `Cache-Control` y `Expires` a través de las opciones por defecto de Flysystem: ```php $defaultOptions['CacheControl'] = 'public, max-age=31536000, immutable'; $defaultOptions['Expires'] = gmdate('D, d M Y H:i:s \G\M\T', time() + 31536000); ``` Puede personalizarse mediante el filtro `flysystem_offload_s3_default_write_options`. ### Escenario bucket público (probado con IDrive e2) 1. Marcar bucket como público en la consola de IDrive e2. 2. Aplicar policy simple (la UI sólo acepta una versión sin `Condition`). ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "AllowPublicReadUploads", "Effect": "Allow", "Principal": { "AWS": "*" }, "Action": [ "s3:GetObject" ], "Resource": [ "arn:aws:s3:::flysystem-offload/wordpress/uploads/*" ] } ] } ``` 3. Configurar CORS si el panel o el frontend cargan las imágenes desde otro origen. ```json [ { "AllowedOrigins": [ "https://offload.brasdrive.com.br" ], "AllowedMethods": [ "GET", "HEAD" ], "AllowedHeaders": [ "*" ], "ExposeHeaders": [ "ETag", "Last-Modified", "x-amz-request-id", "x-amz-id-2" ], "AllowCredentials": false, "MaxAgeSeconds": 86400 } ] ``` ### Escenario recomendado: bucket privado + Cloudflare 1. **Bucket privado** - Mantenerlo cerrado; permitir `s3:GetObject` sólo a IPs de Cloudflare. - Aplicar policy vía CLI (`aws s3api put-bucket-policy`). Ejemplo con `Condition` por IP (mantén la lista actualizada con la API `GET /client/v4/ips`): ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "AllowCloudflareAccess", "Effect": "Allow", "Principal": { "AWS": "*" }, "Action": [ "s3:GetObject" ], "Resource": [ "arn:aws:s3:::flysystem-offload/wordpress/uploads/*" ], "Condition": { "IpAddress": { "aws:SourceIp": [ "173.245.48.0/20", "103.21.244.0/22" // ... ] } } } ] } ``` 2. **Cloudflare** - CNAME `offload.brasdrive.com.br` → endpoint IDrive (ej.`u8k6.va11.idrivee2-9.com`). - Proxy naranja activado; SSL universal. - Reglas WAF o Rate Limiting opcionales. 3. **WordPress** - `uploads.base_url = 'https://offload.brasdrive.com.br'`. - Mantener `prefer_local_for_missing = false`. 4. **Automatización** - Script/cron que consulte la API de Cloudflare, regenere la policy con los nuevos rangos IP y la aplique (controlando con `etag`). 5. **Caché y purgas** - Cloudflare respeta las cabeceras `Cache-Control` y `Expires`. - Purgar desde la consola de Cloudflare cuando sea necesario (por URL o caché completa). ### Compatibilidad de backends Actualmente Flysystem Offload proporciona el adaptador **S3**. El código está preparado para admitir otros backends soportados por Flysystem (WebDAV, SFTP, etc.), pero aún deben agregarse adaptadores específicos (`WebdavAdapter`, `SftpAdapter`, etc.) y sus respectivas configuraciones. Esto forma parte del plan inmediato de ampliación: se crearán clases adicionales en `src/Filesystem/Adapters/`, se extenderá la factoría de adaptadores y se actualizará la documentación para cada backend. ### Notas sobre IDrive e2 - Para habilitar un bucket público reciente, es necesario contactar soporte. - El editor web de políticas no soporta `Condition`; aplicar policies complejas via CLI/API. - CORS sí puede configurarse desde la UI mientras se respete el formato simple. - Usa el endpoint regional del bucket como `endpoint` en configuración y CLI. ### Solución de problemas - URLs sin prefijo: actualizar a la versión reciente de `MediaHooks.php`. - Confirmar `s3.prefix`, `stream.root_prefix`, `stream.host`. - Falta de `Cache-Control`/`Expires`: revisar `config` y `s3.default_options`. - Error `_.filter` en la UI: bug del formulario; usar CLI. - Archivos inaccesibles con bucket privado: verificar policy, rangos Cloudflare, CNAME activo. ### Contribuciones Se aceptan PRs. Mantén el enfoque minimalista, sin panel de administración, y prioriza compatibilidad con el flujo nativo de WordPress. Roadmap cercano: añadir adaptadores para otros drivers Flysystem. ### Licencia MIT. Revisa el archivo `LICENSE`.