2.0.0
This commit is contained in:
parent
6bf225fbfe
commit
bf16330bc7
|
|
@ -1,5 +1,5 @@
|
|||
vendor/
|
||||
node_modules/
|
||||
/vendor/
|
||||
/node_modules/
|
||||
.idea/
|
||||
*.log
|
||||
*.txt
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
(function ($) {
|
||||
const settings = window.flysystemOffloadSettings || {};
|
||||
const s3Label = settings.labels ? settings.labels.s3 : 'Amazon S3 / Compatible';
|
||||
|
||||
function ensureS3Wrapper() {
|
||||
const $heading = $('h2').filter(function () {
|
||||
return $(this).text().trim() === s3Label;
|
||||
});
|
||||
|
||||
if ($heading.length && !$heading.parent().hasClass('flysystem-offload-adapter-section')) {
|
||||
const $table = $heading.next('table');
|
||||
if ($table.length) {
|
||||
const $wrapper = $('<div>', {
|
||||
class: 'flysystem-offload-adapter-section',
|
||||
'data-adapter': 's3'
|
||||
});
|
||||
|
||||
$heading.add($table).wrapAll($wrapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSections() {
|
||||
const adapter = $('#flysystem-offload-adapter').val();
|
||||
|
||||
$('.flysystem-offload-adapter-section').hide();
|
||||
$('.flysystem-offload-field').closest('tr').hide();
|
||||
|
||||
if (adapter === 's3') {
|
||||
$('.flysystem-offload-adapter-section[data-adapter="s3"]').show();
|
||||
$('.flysystem-offload-field--s3').closest('tr').show();
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
ensureS3Wrapper();
|
||||
toggleSections();
|
||||
$('#flysystem-offload-adapter').on('change', function () {
|
||||
toggleSections();
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
||||
|
|
@ -1,277 +0,0 @@
|
|||
<?php
|
||||
namespace FlysystemOffload\Admin;
|
||||
|
||||
class SettingsPage
|
||||
{
|
||||
private string $pluginFile;
|
||||
|
||||
public function __construct(string $pluginFile)
|
||||
{
|
||||
$this->pluginFile = $pluginFile;
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
add_action('admin_menu', [$this, 'registerPage']);
|
||||
add_action('admin_init', [$this, 'registerSettings']);
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
|
||||
}
|
||||
|
||||
public function registerPage(): void
|
||||
{
|
||||
add_options_page(
|
||||
__('Flysystem Offload', 'flysystem-offload'),
|
||||
__('Flysystem Offload', 'flysystem-offload'),
|
||||
'manage_options',
|
||||
'flysystem-offload',
|
||||
[$this, 'render']
|
||||
);
|
||||
}
|
||||
|
||||
public function registerSettings(): void
|
||||
{
|
||||
register_setting(
|
||||
'flysystem_offload',
|
||||
'flysystem_offload_settings',
|
||||
[
|
||||
'sanitize_callback' => [$this, 'sanitizeSettings'],
|
||||
]
|
||||
);
|
||||
|
||||
add_settings_section(
|
||||
'flysystem_offload_general',
|
||||
__('Configuración general', 'flysystem-offload'),
|
||||
'__return_false',
|
||||
'flysystem-offload'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'adapter',
|
||||
__('Adaptador activo', 'flysystem-offload'),
|
||||
[$this, 'renderAdapterField'],
|
||||
'flysystem-offload',
|
||||
'flysystem_offload_general'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'base_prefix',
|
||||
__('Prefijo de almacenamiento', 'flysystem-offload'),
|
||||
[$this, 'renderBasePrefixField'],
|
||||
'flysystem-offload',
|
||||
'flysystem_offload_general'
|
||||
);
|
||||
|
||||
add_settings_section(
|
||||
'flysystem_offload_s3',
|
||||
__('Amazon S3 / Compatible', 'flysystem-offload'),
|
||||
function () {
|
||||
echo '<p>' . esc_html__('Proporciona credenciales de un bucket S3 o compatible (MinIO, DigitalOcean Spaces, etc.).', 'flysystem-offload') . '</p>';
|
||||
},
|
||||
'flysystem-offload'
|
||||
);
|
||||
|
||||
$this->registerS3Fields();
|
||||
}
|
||||
|
||||
private function registerS3Fields(): void
|
||||
{
|
||||
$fields = [
|
||||
[
|
||||
'id' => 's3_access_key',
|
||||
'label' => __('Access Key', 'flysystem-offload'),
|
||||
'args' => ['key' => 'access_key', 'type' => 'text'],
|
||||
],
|
||||
[
|
||||
'id' => 's3_secret_key',
|
||||
'label' => __('Secret Key', 'flysystem-offload'),
|
||||
'args' => ['key' => 'secret_key', 'type' => 'password'],
|
||||
],
|
||||
[
|
||||
'id' => 's3_region',
|
||||
'label' => __('Región', 'flysystem-offload'),
|
||||
'args' => ['key' => 'region', 'type' => 'text', 'placeholder' => 'us-east-1'],
|
||||
],
|
||||
[
|
||||
'id' => 's3_bucket',
|
||||
'label' => __('Bucket', 'flysystem-offload'),
|
||||
'args' => ['key' => 'bucket', 'type' => 'text'],
|
||||
],
|
||||
[
|
||||
'id' => 's3_prefix',
|
||||
'label' => __('Prefijo (opcional)', 'flysystem-offload'),
|
||||
'args' => ['key' => 'prefix', 'type' => 'text', 'placeholder' => 'uploads'],
|
||||
],
|
||||
[
|
||||
'id' => 's3_endpoint',
|
||||
'label' => __('Endpoint personalizado', 'flysystem-offload'),
|
||||
'args' => ['key' => 'endpoint', 'type' => 'text', 'placeholder' => 'https://nyc3.digitaloceanspaces.com'],
|
||||
],
|
||||
[
|
||||
'id' => 's3_cdn_url',
|
||||
'label' => __('URL CDN (opcional)', 'flysystem-offload'),
|
||||
'args' => ['key' => 'cdn_url', 'type' => 'text', 'placeholder' => 'https://cdn.midominio.com'],
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$args = array_merge(
|
||||
$field['args'],
|
||||
['class' => 'flysystem-offload-field flysystem-offload-field--s3']
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
$field['id'],
|
||||
$field['label'],
|
||||
[$this, 'renderS3Field'],
|
||||
'flysystem-offload',
|
||||
'flysystem_offload_s3',
|
||||
$args
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function enqueueAssets(): void
|
||||
{
|
||||
$screen = get_current_screen();
|
||||
|
||||
if (! $screen || $screen->id !== 'settings_page_flysystem-offload') {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_script(
|
||||
'flysystem-offload-settings',
|
||||
plugins_url('assets/admin-settings.js', $this->pluginFile),
|
||||
['jquery'],
|
||||
'0.1.1',
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script(
|
||||
'flysystem-offload-settings',
|
||||
'flysystemOffloadSettings',
|
||||
[
|
||||
'labels' => [
|
||||
's3' => __('Amazon S3 / Compatible', 'flysystem-offload'),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function sanitizeSettings(array $input): array
|
||||
{
|
||||
$current = get_option('flysystem_offload_settings', []);
|
||||
$adapter = sanitize_key($input['adapter'] ?? $current['adapter'] ?? 'local');
|
||||
$basePrefix = trim($input['base_prefix'] ?? '');
|
||||
|
||||
$s3 = $current['adapters']['s3'] ?? [];
|
||||
$inputS3 = $input['adapters']['s3'] ?? [];
|
||||
|
||||
$secretRaw = $inputS3['secret_key'] ?? '';
|
||||
$secret = $secretRaw === '' ? ($s3['secret_key'] ?? '') : $secretRaw;
|
||||
|
||||
$sanitizedS3 = [
|
||||
'access_key' => sanitize_text_field($inputS3['access_key'] ?? $s3['access_key'] ?? ''),
|
||||
'secret_key' => sanitize_text_field($secret),
|
||||
'region' => sanitize_text_field($inputS3['region'] ?? $s3['region'] ?? ''),
|
||||
'bucket' => sanitize_text_field($inputS3['bucket'] ?? $s3['bucket'] ?? ''),
|
||||
'prefix' => trim($inputS3['prefix'] ?? $s3['prefix'] ?? ''),
|
||||
'endpoint' => esc_url_raw($inputS3['endpoint'] ?? $s3['endpoint'] ?? ''),
|
||||
'cdn_url' => esc_url_raw($inputS3['cdn_url'] ?? $s3['cdn_url'] ?? ''),
|
||||
];
|
||||
|
||||
$current['adapter'] = $adapter;
|
||||
$current['base_prefix'] = $basePrefix;
|
||||
$current['adapters']['s3'] = $sanitizedS3;
|
||||
|
||||
return $current;
|
||||
}
|
||||
|
||||
public function render(): void
|
||||
{
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e('Flysystem Offload', 'flysystem-offload'); ?></h1>
|
||||
<form method="post" action="options.php">
|
||||
<?php
|
||||
settings_fields('flysystem_offload');
|
||||
do_settings_sections('flysystem-offload');
|
||||
submit_button(__('Guardar cambios', 'flysystem-offload'));
|
||||
?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function renderAdapterField(): void
|
||||
{
|
||||
$settings = get_option('flysystem_offload_settings', []);
|
||||
$adapter = $settings['adapter'] ?? 'local';
|
||||
|
||||
$options = [
|
||||
'local' => __('Local (fallback)', 'flysystem-offload'),
|
||||
's3' => __('Amazon S3 / Compatible', 'flysystem-offload'),
|
||||
'sftp' => 'SFTP',
|
||||
'webdav' => 'WebDAV',
|
||||
'gcs' => 'Google Cloud Storage',
|
||||
'azure' => 'Azure Blob Storage',
|
||||
'googledrive'=> 'Google Drive (beta)',
|
||||
'onedrive' => 'OneDrive (beta)',
|
||||
'dropbox' => 'Dropbox (beta)',
|
||||
];
|
||||
?>
|
||||
<select name="flysystem_offload_settings[adapter]" id="flysystem-offload-adapter">
|
||||
<?php foreach ($options as $value => $label) : ?>
|
||||
<option value="<?php echo esc_attr($value); ?>" <?php selected($adapter, $value); ?>>
|
||||
<?php echo esc_html($label); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function renderBasePrefixField(): void
|
||||
{
|
||||
$settings = get_option('flysystem_offload_settings', []);
|
||||
$prefix = $settings['base_prefix'] ?? '';
|
||||
?>
|
||||
<input
|
||||
type="text"
|
||||
name="flysystem_offload_settings[base_prefix]"
|
||||
value="<?php echo esc_attr($prefix); ?>"
|
||||
placeholder="media/"
|
||||
class="regular-text"
|
||||
/>
|
||||
<p class="description">
|
||||
<?php esc_html_e('Opcional. Se antepone a todas las rutas remotas (ej. "wordpress/uploads").', 'flysystem-offload'); ?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function renderS3Field(array $args): void
|
||||
{
|
||||
$settings = get_option('flysystem_offload_settings', []);
|
||||
$s3 = $settings['adapters']['s3'] ?? [];
|
||||
$key = $args['key'];
|
||||
$type = $args['type'] ?? 'text';
|
||||
$placeholder = $args['placeholder'] ?? '';
|
||||
$value = $s3[$key] ?? '';
|
||||
|
||||
if ($type === 'password') {
|
||||
$value = '';
|
||||
}
|
||||
?>
|
||||
<input
|
||||
type="<?php echo esc_attr($type); ?>"
|
||||
name="flysystem_offload_settings[adapters][s3][<?php echo esc_attr($key); ?>]"
|
||||
value="<?php echo esc_attr($value); ?>"
|
||||
class="regular-text"
|
||||
placeholder="<?php echo esc_attr($placeholder); ?>"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<?php if ('secret_key' === $key) : ?>
|
||||
<p class="description">
|
||||
<?php esc_html_e('La clave no se mostrará después de guardarla.', 'flysystem-offload'); ?>
|
||||
</p>
|
||||
<?php endif;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
namespace FlysystemOffload\Config;
|
||||
|
||||
class ConfigLoader
|
||||
{
|
||||
private string $pluginDir;
|
||||
|
||||
public function __construct(string $pluginFile)
|
||||
{
|
||||
$this->pluginDir = dirname($pluginFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve la configuración efectiva (defaults + overrides).
|
||||
*
|
||||
* @throws \RuntimeException cuando un archivo config no retorna array.
|
||||
*/
|
||||
public function load(): array
|
||||
{
|
||||
$defaults = $this->defaults();
|
||||
$files = $this->discoverConfigFiles();
|
||||
|
||||
$config = [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
$data = require $file;
|
||||
|
||||
if (! is_array($data)) {
|
||||
throw new \RuntimeException(
|
||||
sprintf('[Flysystem Offload] El archivo de configuración "%s" debe retornar un array.', $file)
|
||||
);
|
||||
}
|
||||
|
||||
$config = array_replace_recursive($config, $data);
|
||||
}
|
||||
|
||||
if (empty($config)) {
|
||||
$config = $defaults;
|
||||
} else {
|
||||
$config = array_replace_recursive($defaults, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permite ajustar/ensanchar la configuración en tiempo de ejecución.
|
||||
* Ideal para multisite o integraciones externas.
|
||||
*
|
||||
* @param array $config
|
||||
* @param array $files Lista de archivos usados (en orden de carga).
|
||||
*/
|
||||
return apply_filters('flysystem_offload_config', $config, $files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defaults que garantizan compatibilidad si falta un archivo.
|
||||
*/
|
||||
public function defaults(): array
|
||||
{
|
||||
return [
|
||||
'adapter' => 'local',
|
||||
'base_prefix' => '',
|
||||
'adapters' => [
|
||||
's3' => [
|
||||
'access_key' => '',
|
||||
'secret_key' => '',
|
||||
'region' => '',
|
||||
'bucket' => '',
|
||||
'prefix' => '',
|
||||
'endpoint' => '',
|
||||
'cdn_url' => '',
|
||||
],
|
||||
'sftp' => [
|
||||
'host' => '',
|
||||
'port' => 22,
|
||||
'username' => '',
|
||||
'password' => '',
|
||||
'root' => '/uploads',
|
||||
],
|
||||
'gcs' => [
|
||||
'project_id' => '',
|
||||
'bucket' => '',
|
||||
'key_file_path' => '',
|
||||
],
|
||||
'azure' => [
|
||||
'account_name' => '',
|
||||
'account_key' => '',
|
||||
'container' => '',
|
||||
'prefix' => '',
|
||||
],
|
||||
'webdav' => [
|
||||
'base_uri' => '',
|
||||
'username' => '',
|
||||
'password' => '',
|
||||
'path_prefix'=> '',
|
||||
],
|
||||
'googledrive' => [],
|
||||
'onedrive' => [],
|
||||
'dropbox' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Descubre archivos de configuración en orden de prioridad.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function discoverConfigFiles(): array
|
||||
{
|
||||
$candidates = [];
|
||||
|
||||
if (defined('FLYSYSTEM_OFFLOAD_CONFIG')) {
|
||||
$candidates[] = FLYSYSTEM_OFFLOAD_CONFIG;
|
||||
}
|
||||
|
||||
// Opción por defecto en wp-content/.
|
||||
$candidates[] = WP_CONTENT_DIR . '/flysystem-offload.php';
|
||||
|
||||
// Alias alternativo frecuente.
|
||||
$candidates[] = WP_CONTENT_DIR . '/flysystem-offload-config.php';
|
||||
|
||||
// Fallback incluido dentro del plugin (para entornos sin personalización inicial).
|
||||
$candidates[] = $this->pluginDir . '/config/flysystem-offload.php';
|
||||
|
||||
$unique = array_unique(array_filter(
|
||||
$candidates,
|
||||
static fn (string $path) => is_readable($path)
|
||||
));
|
||||
|
||||
return array_values($unique);
|
||||
}
|
||||
}
|
||||
|
|
@ -49,17 +49,32 @@ class S3Adapter implements AdapterInterface
|
|||
|
||||
public function publicBaseUrl(array $settings): string
|
||||
{
|
||||
if (!empty($settings['cdn_url'])) {
|
||||
if (! empty($settings['cdn_url'])) {
|
||||
return rtrim($settings['cdn_url'], '/');
|
||||
}
|
||||
|
||||
$bucket = $settings['bucket'];
|
||||
$region = $settings['region'];
|
||||
$bucket = $settings['bucket'] ?? '';
|
||||
$prefix = isset($settings['prefix']) ? trim($settings['prefix'], '/') : '';
|
||||
$prefix = $prefix === '' ? '' : '/' . $prefix;
|
||||
|
||||
if ($region === 'us-east-1') {
|
||||
return "https://{$bucket}.s3.amazonaws.com";
|
||||
if (! empty($settings['endpoint'])) {
|
||||
$endpoint = trim($settings['endpoint']);
|
||||
if (! preg_match('#^https?://#i', $endpoint)) {
|
||||
$endpoint = 'https://' . $endpoint;
|
||||
}
|
||||
|
||||
$endpoint = rtrim($endpoint, '/');
|
||||
|
||||
// Cuando se usa endpoint propio forzamos path-style (+ bucket en la ruta)
|
||||
return $endpoint . '/' . $bucket . $prefix;
|
||||
}
|
||||
|
||||
return "https://{$bucket}.s3.{$region}.amazonaws.com";
|
||||
$region = $settings['region'] ?? 'us-east-1';
|
||||
|
||||
if ($region === 'us-east-1') {
|
||||
return "https://{$bucket}.s3.amazonaws.com{$prefix}";
|
||||
}
|
||||
|
||||
return "https://{$bucket}.s3.{$region}.amazonaws.com{$prefix}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FlysystemOffload\Media;
|
||||
|
||||
use WP_Error;
|
||||
use WP_Image_Editor_Imagick;
|
||||
|
||||
class ImageEditorImagick extends WP_Image_Editor_Imagick
|
||||
{
|
||||
protected ?string $remoteFilename = null;
|
||||
protected array $tempFiles = [];
|
||||
|
||||
public function load()
|
||||
{
|
||||
if ($this->image instanceof \Imagick) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (empty($this->file)) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_missing_file',
|
||||
__('Archivo no definido.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
if (! $this->isFlysystemPath($this->file)) {
|
||||
return parent::load();
|
||||
}
|
||||
|
||||
$localPath = $this->mirrorToLocal($this->file);
|
||||
|
||||
if (is_wp_error($localPath)) {
|
||||
return $localPath;
|
||||
}
|
||||
|
||||
$this->remoteFilename = $this->file;
|
||||
$this->file = $localPath;
|
||||
|
||||
$result = parent::load();
|
||||
|
||||
$this->file = $this->remoteFilename;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function _save($image, $filename = null, $mime_type = null)
|
||||
{
|
||||
[$filename, $extension, $mime_type] = $this->get_output_format($filename, $mime_type);
|
||||
|
||||
if (! $filename) {
|
||||
$filename = $this->generate_filename(null, null, $extension);
|
||||
}
|
||||
|
||||
$isRemote = $this->isFlysystemPath($filename);
|
||||
$tempTarget = $isRemote ? $this->createTempFile($filename) : false;
|
||||
|
||||
$result = parent::_save($image, $tempTarget ?: $filename, $mime_type);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
if ($tempTarget) {
|
||||
@unlink($tempTarget);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($tempTarget) {
|
||||
$copy = copy($result['path'], $filename);
|
||||
|
||||
@unlink($result['path']);
|
||||
@unlink($tempTarget);
|
||||
|
||||
if (! $copy) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_copy_failed',
|
||||
__('No se pudo copiar la imagen procesada al almacenamiento remoto.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
$result['path'] = $filename;
|
||||
$result['file'] = wp_basename($filename);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
foreach ($this->tempFiles as $temp) {
|
||||
@unlink($temp);
|
||||
}
|
||||
|
||||
parent::__destruct();
|
||||
}
|
||||
|
||||
protected function mirrorToLocal(string $remotePath)
|
||||
{
|
||||
$tempFile = $this->createTempFile($remotePath);
|
||||
|
||||
if (! $tempFile) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_temp_missing',
|
||||
__('No se pudo crear un archivo temporal para la imagen remota.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
if (! copy($remotePath, $tempFile)) {
|
||||
@unlink($tempFile);
|
||||
|
||||
return new WP_Error(
|
||||
'flysystem_offload_remote_copy_failed',
|
||||
__('No se pudo copiar la imagen desde el almacenamiento remoto.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
$this->tempFiles[] = $tempFile;
|
||||
|
||||
return $tempFile;
|
||||
}
|
||||
|
||||
protected function createTempFile(string $context)
|
||||
{
|
||||
if (! function_exists('wp_tempnam')) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
}
|
||||
|
||||
$tempFile = wp_tempnam(wp_basename($context));
|
||||
|
||||
return $tempFile ?: false;
|
||||
}
|
||||
|
||||
protected function isFlysystemPath(string $path): bool
|
||||
{
|
||||
return strpos($path, 'fly://') === 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,287 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FlysystemOffload\Media;
|
||||
|
||||
use League\Flysystem\FilesystemException;
|
||||
use League\Flysystem\FilesystemOperator;
|
||||
use League\Flysystem\StorageAttributes;
|
||||
use WP_Error;
|
||||
|
||||
class MediaHooks
|
||||
{
|
||||
private ?FilesystemOperator $filesystem = null;
|
||||
private bool $registered = false;
|
||||
private bool $metadataMirrorInProgress = false;
|
||||
|
||||
public function setFilesystem(?FilesystemOperator $filesystem): void
|
||||
{
|
||||
$this->filesystem = $filesystem;
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
if ($this->registered) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_filter('wp_image_editors', [$this, 'filterImageEditors'], 5);
|
||||
add_filter('pre_move_uploaded_file', [$this, 'handlePreMoveUploadedFile'], 10, 3);
|
||||
add_filter('wp_read_image_metadata', [$this, 'filterReadImageMetadata'], 10, 2);
|
||||
add_filter('wp_generate_attachment_metadata', [$this, 'filterGenerateAttachmentMetadata'], 10, 2);
|
||||
add_filter('pre_wp_unique_filename_file_list', [$this, 'filterUniqueFilenameFileList'], 10, 3);
|
||||
add_action('delete_attachment', [$this, 'handleDeleteAttachment'], 10);
|
||||
|
||||
$this->registered = true;
|
||||
}
|
||||
|
||||
public function unregister(): void
|
||||
{
|
||||
if (! $this->registered) {
|
||||
return;
|
||||
}
|
||||
|
||||
remove_filter('wp_image_editors', [$this, 'filterImageEditors'], 5);
|
||||
remove_filter('pre_move_uploaded_file', [$this, 'handlePreMoveUploadedFile'], 10);
|
||||
remove_filter('wp_read_image_metadata', [$this, 'filterReadImageMetadata'], 10);
|
||||
remove_filter('wp_generate_attachment_metadata', [$this, 'filterGenerateAttachmentMetadata'], 10);
|
||||
remove_filter('pre_wp_unique_filename_file_list', [$this, 'filterUniqueFilenameFileList'], 10);
|
||||
remove_action('delete_attachment', [$this, 'handleDeleteAttachment']);
|
||||
|
||||
$this->registered = false;
|
||||
}
|
||||
|
||||
public function filterImageEditors(array $editors): array
|
||||
{
|
||||
$imagickIndex = array_search(\WP_Image_Editor_Imagick::class, $editors, true);
|
||||
|
||||
if ($imagickIndex !== false) {
|
||||
unset($editors[$imagickIndex]);
|
||||
}
|
||||
|
||||
array_unshift($editors, ImageEditorImagick::class);
|
||||
|
||||
return array_values(array_unique($editors));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sobreescribe el movimiento final del archivo para subirlo a fly:// mediante Flysystem.
|
||||
*/
|
||||
public function handlePreMoveUploadedFile($override, array $file, string $destination)
|
||||
{
|
||||
if ($override !== null) {
|
||||
return $override;
|
||||
}
|
||||
|
||||
if (! $this->isFlyPath($destination)) {
|
||||
return $override;
|
||||
}
|
||||
|
||||
if (! $this->filesystem) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_missing_filesystem',
|
||||
__('No se pudo acceder al filesystem remoto.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
$relativePath = $this->relativeFlyPath($destination);
|
||||
|
||||
if ($relativePath === null) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_invalid_destination',
|
||||
__('Ruta de destino inválida para el stream fly://.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
$directory = dirname($relativePath);
|
||||
|
||||
if ($directory !== '' && $directory !== '.') {
|
||||
try {
|
||||
$this->filesystem->createDirectory($directory);
|
||||
} catch (FilesystemException $e) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_directory_error',
|
||||
sprintf(
|
||||
__('No se pudo crear el directorio remoto "%s": %s', 'flysystem-offload'),
|
||||
esc_html($directory),
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$resource = @fopen($file['tmp_name'], 'rb');
|
||||
|
||||
if (! $resource) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_tmp_read_fail',
|
||||
__('No se pudo leer el archivo temporal subido.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->filesystem->writeStream($relativePath, $resource);
|
||||
} catch (FilesystemException $e) {
|
||||
if (is_resource($resource)) {
|
||||
fclose($resource);
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'flysystem_offload_write_fail',
|
||||
sprintf(
|
||||
__('No se pudo guardar el archivo en el almacenamiento remoto: %s', 'flysystem-offload'),
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (is_resource($resource)) {
|
||||
fclose($resource);
|
||||
}
|
||||
|
||||
@unlink($file['tmp_name']);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function filterReadImageMetadata($metadata, string $file)
|
||||
{
|
||||
if ($this->metadataMirrorInProgress || ! $this->isFlyPath($file)) {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
$this->metadataMirrorInProgress = true;
|
||||
|
||||
$temp = $this->mirrorToLocal($file);
|
||||
|
||||
if (! is_wp_error($temp)) {
|
||||
$metadata = wp_read_image_metadata($temp);
|
||||
@unlink($temp);
|
||||
}
|
||||
|
||||
$this->metadataMirrorInProgress = false;
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
public function filterGenerateAttachmentMetadata(array $metadata, int $attachmentId): array
|
||||
{
|
||||
if (isset($metadata['filesize'])) {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
$file = get_attached_file($attachmentId);
|
||||
|
||||
if ($file && file_exists($file)) {
|
||||
$metadata['filesize'] = filesize($file);
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
public function filterUniqueFilenameFileList($files, string $dir, string $filename)
|
||||
{
|
||||
if (! $this->isFlyPath($dir) || ! $this->filesystem) {
|
||||
return $files;
|
||||
}
|
||||
|
||||
$relativeDir = $this->relativeFlyPath($dir);
|
||||
|
||||
if ($relativeDir === null) {
|
||||
return $files;
|
||||
}
|
||||
|
||||
$existing = [];
|
||||
|
||||
foreach ($this->filesystem->listContents($relativeDir, false) as $item) {
|
||||
/** @var StorageAttributes $item */
|
||||
if ($item->isDir()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing[] = basename($item->path());
|
||||
}
|
||||
|
||||
return $existing;
|
||||
}
|
||||
|
||||
public function handleDeleteAttachment(int $attachmentId): void
|
||||
{
|
||||
$file = get_attached_file($attachmentId);
|
||||
|
||||
if (! $file || ! $this->isFlyPath($file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$meta = wp_get_attachment_metadata($attachmentId);
|
||||
|
||||
if (! empty($meta['sizes'])) {
|
||||
foreach ($meta['sizes'] as $sizeInfo) {
|
||||
if (empty($sizeInfo['file'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
wp_delete_file(str_replace(basename($file), $sizeInfo['file'], $file));
|
||||
}
|
||||
}
|
||||
|
||||
$original = get_post_meta($attachmentId, 'original_image', true);
|
||||
if ($original) {
|
||||
wp_delete_file(str_replace(basename($file), $original, $file));
|
||||
}
|
||||
|
||||
$backup = get_post_meta($attachmentId, '_wp_attachment_backup_sizes', true);
|
||||
if (is_array($backup)) {
|
||||
foreach ($backup as $sizeInfo) {
|
||||
if (empty($sizeInfo['file'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
wp_delete_file(str_replace(basename($file), $sizeInfo['file'], $file));
|
||||
}
|
||||
}
|
||||
|
||||
wp_delete_file($file);
|
||||
}
|
||||
|
||||
protected function mirrorToLocal(string $remotePath)
|
||||
{
|
||||
if (! function_exists('wp_tempnam')) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
}
|
||||
|
||||
$temp = wp_tempnam(wp_basename($remotePath));
|
||||
|
||||
if (! $temp) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_temp_fail',
|
||||
__('No se pudo crear un archivo temporal para la lectura de metadatos.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
if (! @copy($remotePath, $temp)) {
|
||||
@unlink($temp);
|
||||
|
||||
return new WP_Error(
|
||||
'flysystem_offload_remote_copy_fail',
|
||||
__('No se pudo copiar la imagen desde el almacenamiento remoto.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
return $temp;
|
||||
}
|
||||
|
||||
protected function isFlyPath(string $path): bool
|
||||
{
|
||||
return strpos($path, 'fly://') === 0;
|
||||
}
|
||||
|
||||
protected function relativeFlyPath(string $path): ?string
|
||||
{
|
||||
if (! $this->isFlyPath($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ltrim(substr($path, 6), '/');
|
||||
}
|
||||
}
|
||||
162
src/Plugin.php
162
src/Plugin.php
|
|
@ -1,20 +1,28 @@
|
|||
<?php
|
||||
namespace FlysystemOffload;
|
||||
|
||||
use FlysystemOffload\Admin\SettingsPage;
|
||||
use FlysystemOffload\Config\ConfigLoader;
|
||||
use FlysystemOffload\Filesystem\FilesystemFactory;
|
||||
use FlysystemOffload\Helpers\PathHelper;
|
||||
use FlysystemOffload\Media\MediaHooks;
|
||||
use FlysystemOffload\StreamWrapper\FlysystemStreamWrapper;
|
||||
use League\Flysystem\FilesystemOperator;
|
||||
use WP_Error;
|
||||
|
||||
class Plugin
|
||||
{
|
||||
private static $instance;
|
||||
private static string $pluginFile;
|
||||
|
||||
private ?FilesystemOperator $filesystem = null;
|
||||
private bool $streamRegistered = false;
|
||||
private array $settings = [];
|
||||
private array $config = [];
|
||||
private ConfigLoader $configLoader;
|
||||
private MediaHooks $mediaHooks;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->mediaHooks = new MediaHooks();
|
||||
}
|
||||
|
||||
public static function bootstrap(string $pluginFile): void
|
||||
{
|
||||
|
|
@ -35,53 +43,20 @@ class Plugin
|
|||
|
||||
public static function activate(): void
|
||||
{
|
||||
$defaults = [
|
||||
'adapter' => 'local',
|
||||
'base_prefix' => '',
|
||||
'adapters' => [
|
||||
's3' => [
|
||||
'access_key' => '',
|
||||
'secret_key' => '',
|
||||
'region' => '',
|
||||
'bucket' => '',
|
||||
'endpoint' => '',
|
||||
'cdn_url' => ''
|
||||
],
|
||||
'sftp' => [
|
||||
'host' => '',
|
||||
'port' => 22,
|
||||
'username' => '',
|
||||
'password' => '',
|
||||
'root' => '/uploads'
|
||||
],
|
||||
'gcs' => [
|
||||
'project_id' => '',
|
||||
'bucket' => '',
|
||||
'key_file_path' => ''
|
||||
],
|
||||
'azure' => [
|
||||
'account_name' => '',
|
||||
'account_key' => '',
|
||||
'container' => '',
|
||||
'prefix' => ''
|
||||
],
|
||||
'webdav' => [
|
||||
'base_uri' => '',
|
||||
'username' => '',
|
||||
'password' => '',
|
||||
'path_prefix' => ''
|
||||
],
|
||||
'googledrive' => [],
|
||||
'onedrive' => [],
|
||||
'dropbox' => []
|
||||
]
|
||||
];
|
||||
wp_mkdir_p(WP_CONTENT_DIR . '/flysystem-uploads');
|
||||
|
||||
add_option('flysystem_offload_settings', $defaults);
|
||||
if (! defined('FLYSYSTEM_OFFLOAD_CONFIG') && ! file_exists(WP_CONTENT_DIR . '/flysystem-offload.php')) {
|
||||
error_log('[Flysystem Offload] No se encontró un archivo de configuración. Copia config/flysystem-offload.example.php a wp-content/flysystem-offload.php y ajústalo.');
|
||||
}
|
||||
}
|
||||
|
||||
public static function deactivate(): void
|
||||
{
|
||||
if ($instance = self::$instance) {
|
||||
$instance->mediaHooks->unregister();
|
||||
$instance->mediaHooks->setFilesystem(null);
|
||||
}
|
||||
|
||||
if (in_array('fly', stream_get_wrappers(), true)) {
|
||||
stream_wrapper_unregister('fly');
|
||||
}
|
||||
|
|
@ -89,50 +64,54 @@ class Plugin
|
|||
|
||||
public function init(): void
|
||||
{
|
||||
$this->settings = get_option('flysystem_offload_settings', []);
|
||||
$this->configLoader = new ConfigLoader(self::$pluginFile);
|
||||
|
||||
add_action(
|
||||
'update_option_flysystem_offload_settings',
|
||||
function ($oldValue, $newValue) {
|
||||
$this->settings = $newValue;
|
||||
$this->filesystem = null;
|
||||
$this->streamRegistered = false;
|
||||
$this->registerStreamWrapper();
|
||||
},
|
||||
10,
|
||||
2
|
||||
);
|
||||
|
||||
$this->registerStreamWrapper();
|
||||
$this->reloadConfig();
|
||||
|
||||
add_filter('upload_dir', [$this, 'filterUploadDir'], 20);
|
||||
add_filter('wp_get_attachment_url', [$this, 'filterAttachmentUrl'], 20, 2);
|
||||
add_filter('wp_get_attachment_metadata', [$this, 'filterAttachmentMetadata'], 20);
|
||||
add_filter('wp_get_original_image_path', [$this, 'filterOriginalImagePath'], 20);
|
||||
add_filter('wp_delete_file', [$this, 'handleDeleteFile'], 20);
|
||||
|
||||
add_action('delete_attachment', [$this, 'handleDeleteAttachment'], 20);
|
||||
add_action('switch_blog', [$this, 'handleSwitchBlog']);
|
||||
|
||||
add_action('switch_blog', function () {
|
||||
$this->filesystem = null;
|
||||
$this->streamRegistered = false;
|
||||
$this->settings = get_option('flysystem_offload_settings', []);
|
||||
$this->registerStreamWrapper();
|
||||
});
|
||||
|
||||
if (is_admin()) {
|
||||
(new SettingsPage(self::$pluginFile))->boot();
|
||||
}
|
||||
add_action('flysystem_offload_reload_config', [$this, 'reloadConfig']);
|
||||
|
||||
if (defined('WP_CLI') && WP_CLI) {
|
||||
\WP_CLI::add_command('flysystem-offload health-check', [Admin\HealthCheck::class, 'run']);
|
||||
}
|
||||
}
|
||||
|
||||
public function reloadConfig(): void
|
||||
{
|
||||
try {
|
||||
$this->config = $this->configLoader->load();
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[Flysystem Offload] Error cargando configuración: ' . $e->getMessage());
|
||||
$this->config = $this->configLoader->defaults();
|
||||
}
|
||||
|
||||
$this->filesystem = null;
|
||||
$this->streamRegistered = false;
|
||||
|
||||
$this->mediaHooks->unregister();
|
||||
$this->mediaHooks->setFilesystem(null);
|
||||
|
||||
$this->registerStreamWrapper();
|
||||
}
|
||||
|
||||
public function handleSwitchBlog(): void
|
||||
{
|
||||
$this->reloadConfig();
|
||||
}
|
||||
|
||||
public function getFilesystem(): FilesystemOperator
|
||||
{
|
||||
if (!$this->filesystem) {
|
||||
$factory = new FilesystemFactory($this->settings);
|
||||
$result = $factory->make();
|
||||
if (! $this->filesystem) {
|
||||
$factory = new FilesystemFactory($this->config);
|
||||
$result = $factory->make();
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
throw new \RuntimeException($result->get_error_message());
|
||||
|
|
@ -151,22 +130,36 @@ class Plugin
|
|||
}
|
||||
|
||||
try {
|
||||
FlysystemStreamWrapper::register($this->getFilesystem(), 'fly', PathHelper::normalizePrefix($this->settings['base_prefix'] ?? ''));
|
||||
$filesystem = $this->getFilesystem();
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[Flysystem Offload] No se pudo obtener el filesystem: ' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
FlysystemStreamWrapper::register(
|
||||
$filesystem,
|
||||
'fly',
|
||||
PathHelper::normalizePrefix($this->config['base_prefix'] ?? '')
|
||||
);
|
||||
$this->streamRegistered = true;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[Flysystem Offload] No se pudo registrar el stream wrapper: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$this->mediaHooks->setFilesystem($filesystem);
|
||||
$this->mediaHooks->register();
|
||||
}
|
||||
|
||||
public function filterUploadDir(array $dirs): array
|
||||
{
|
||||
$remoteBase = $this->getRemoteUrlBase();
|
||||
$prefix = PathHelper::normalizePrefix($this->settings['base_prefix'] ?? '');
|
||||
$subdir = $dirs['subdir'] ?? '';
|
||||
$prefix = PathHelper::normalizePrefix($this->config['base_prefix'] ?? '');
|
||||
$subdir = $dirs['subdir'] ?? '';
|
||||
|
||||
$dirs['path'] = "fly://{$prefix}{$subdir}";
|
||||
$dirs['path'] = "fly://{$prefix}{$subdir}";
|
||||
$dirs['basedir'] = "fly://{$prefix}";
|
||||
$dirs['url'] = trailingslashit($remoteBase) . ltrim($subdir, '/');
|
||||
$dirs['url'] = trailingslashit($remoteBase) . ltrim($subdir, '/');
|
||||
$dirs['baseurl'] = $remoteBase;
|
||||
|
||||
return $dirs;
|
||||
|
|
@ -174,7 +167,7 @@ class Plugin
|
|||
|
||||
public function filterAttachmentUrl(string $url, int $postId): string
|
||||
{
|
||||
$localBase = trailingslashit(wp_get_upload_dir()['baseurl']);
|
||||
$localBase = trailingslashit(wp_get_upload_dir()['baseurl']);
|
||||
$remoteBase = trailingslashit($this->getRemoteUrlBase());
|
||||
|
||||
return str_replace($localBase, $remoteBase, $url);
|
||||
|
|
@ -182,16 +175,17 @@ class Plugin
|
|||
|
||||
public function filterAttachmentMetadata(array $metadata): array
|
||||
{
|
||||
if (!empty($metadata['file'])) {
|
||||
if (! empty($metadata['file'])) {
|
||||
$metadata['file'] = PathHelper::stripProtocol($metadata['file']);
|
||||
}
|
||||
|
||||
if (!empty($metadata['sizes'])) {
|
||||
if (! empty($metadata['sizes'])) {
|
||||
foreach ($metadata['sizes'] as &$size) {
|
||||
if (!empty($size['file'])) {
|
||||
if (! empty($size['file'])) {
|
||||
$size['file'] = ltrim($size['file'], '/');
|
||||
}
|
||||
}
|
||||
unset($size);
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
|
|
@ -230,9 +224,9 @@ class Plugin
|
|||
|
||||
private function getRemoteUrlBase(): string
|
||||
{
|
||||
$adapterKey = $this->settings['adapter'] ?? 'local';
|
||||
$config = $this->settings['adapters'][$adapterKey] ?? [];
|
||||
$adapterKey = $this->config['adapter'] ?? 'local';
|
||||
$settings = $this->config['adapters'][$adapterKey] ?? [];
|
||||
|
||||
return (new FilesystemFactory($this->settings))->resolvePublicBaseUrl($adapterKey, $config);
|
||||
return (new FilesystemFactory($this->config))->resolvePublicBaseUrl($adapterKey, $settings);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue