2.0.3
This commit is contained in:
parent
d4087f5299
commit
65c7a7365d
|
|
@ -10,6 +10,7 @@
|
|||
"league/flysystem-azure-blob-storage": "^3.24",
|
||||
"league/flysystem-google-cloud-storage": "^3.24",
|
||||
"league/flysystem-webdav": "^3.24",
|
||||
"league/flysystem-path-prefixing": "^3.24",
|
||||
"aws/aws-sdk-php": "^3.330",
|
||||
"google/cloud-storage": "^1.33",
|
||||
"microsoft/azure-storage-blob": "^1.5",
|
||||
|
|
|
|||
|
|
@ -4,61 +4,70 @@ declare(strict_types=1);
|
|||
|
||||
namespace FlysystemOffload\Admin;
|
||||
|
||||
use FlysystemOffload\Helpers\PathHelper;
|
||||
use FlysystemOffload\Plugin;
|
||||
use League\Flysystem\PortableVisibility;
|
||||
use Throwable;
|
||||
use FlysystemOffload\Filesystem\FilesystemFactory;
|
||||
use League\Flysystem\FilesystemException;
|
||||
use League\Flysystem\Visibility;
|
||||
use WP_CLI;
|
||||
use WP_CLI_Command;
|
||||
use WP_Error;
|
||||
|
||||
final class HealthCheck
|
||||
class HealthCheck extends WP_CLI_Command
|
||||
{
|
||||
private FilesystemFactory $factory;
|
||||
|
||||
public function __construct(FilesystemFactory $factory)
|
||||
{
|
||||
$this->factory = $factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $args
|
||||
* @param array<string, mixed> $assocArgs
|
||||
* Ejecuta un chequeo básico de conectividad e integración.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* wp flysystem-offload health-check
|
||||
*/
|
||||
public static function run(array $args, array $assocArgs): void
|
||||
public function __invoke(): void
|
||||
{
|
||||
unset($args, $assocArgs);
|
||||
$settings = get_option('flysystem_offload_settings', []);
|
||||
|
||||
if (! is_array($settings)) {
|
||||
WP_CLI::error('No se encontraron ajustes del plugin.');
|
||||
|
||||
if (! class_exists('\WP_CLI')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->run($settings);
|
||||
|
||||
if ($result instanceof WP_Error) {
|
||||
WP_CLI::error($result->get_error_message());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
WP_CLI::success('Chequeo completado correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $settings
|
||||
*/
|
||||
public function run(array $settings)
|
||||
{
|
||||
try {
|
||||
$plugin = Plugin::instance();
|
||||
$filesystem = $plugin->getFilesystem();
|
||||
|
||||
if (! in_array('fly', stream_get_wrappers(), true)) {
|
||||
\WP_CLI::error('El stream wrapper "fly://" no está registrado.');
|
||||
$filesystem = $this->factory->build($settings);
|
||||
} catch (FilesystemException|\Throwable $exception) {
|
||||
return new WP_Error('flysystem_offload_health_check_failed', $exception->getMessage());
|
||||
}
|
||||
|
||||
$report = [];
|
||||
|
||||
$report[] = 'Adapter: ' . get_class($filesystem);
|
||||
|
||||
$uploadDir = wp_get_upload_dir();
|
||||
$report[] = 'Base URL remoto: ' . ($uploadDir['baseurl'] ?? '(desconocido)');
|
||||
$report[] = 'Directorio remoto: ' . ($uploadDir['basedir'] ?? '(desconocido)');
|
||||
|
||||
$testKey = trim(PathHelper::stripProtocol(($uploadDir['path'] ?? '') . '/flysystem-offload-health-check-' . uniqid('', true)), '/');
|
||||
|
||||
$filesystem->write($testKey, 'ok', ['visibility' => PortableVisibility::PUBLIC]);
|
||||
$content = $filesystem->read($testKey);
|
||||
|
||||
if ($content !== 'ok') {
|
||||
\WP_CLI::warning('El contenido leído no coincide con lo escrito.');
|
||||
} else {
|
||||
$report[] = 'Lectura/escritura remota verificada.';
|
||||
}
|
||||
$testKey = sprintf('health-check/%s.txt', wp_generate_uuid4());
|
||||
|
||||
try {
|
||||
$filesystem->write($testKey, 'ok', ['visibility' => Visibility::PUBLIC]);
|
||||
$filesystem->delete($testKey);
|
||||
|
||||
foreach ($report as $line) {
|
||||
\WP_CLI::line($line);
|
||||
} catch (\Throwable $exception) {
|
||||
return new WP_Error('flysystem_offload_health_check_failed', $exception->getMessage());
|
||||
}
|
||||
|
||||
\WP_CLI::success('Health-check completado correctamente.');
|
||||
} catch (Throwable $exception) {
|
||||
\WP_CLI::error('Health-check falló: ' . $exception->getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,240 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FlysystemOffload\Filesystem\Adapters;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Generator;
|
||||
use League\Flysystem\CalculateChecksumFromStream;
|
||||
use League\Flysystem\ChecksumProvider;
|
||||
use League\Flysystem\Config;
|
||||
use League\Flysystem\FileAttributes;
|
||||
use League\Flysystem\FilesystemAdapter;
|
||||
use League\Flysystem\PathPrefixer;
|
||||
use League\Flysystem\UnableToCheckDirectoryExistence;
|
||||
use League\Flysystem\UnableToCheckFileExistence;
|
||||
use League\Flysystem\UnableToCopyFile;
|
||||
use League\Flysystem\UnableToCreateDirectory;
|
||||
use League\Flysystem\UnableToDeleteDirectory;
|
||||
use League\Flysystem\UnableToDeleteFile;
|
||||
use League\Flysystem\UnableToGeneratePublicUrl;
|
||||
use League\Flysystem\UnableToGenerateTemporaryUrl;
|
||||
use League\Flysystem\UnableToMoveFile;
|
||||
use League\Flysystem\UnableToReadFile;
|
||||
use League\Flysystem\UnableToRetrieveMetadata;
|
||||
use League\Flysystem\UnableToSetVisibility;
|
||||
use League\Flysystem\UnableToWriteFile;
|
||||
use League\Flysystem\UrlGeneration\PublicUrlGenerator;
|
||||
use League\Flysystem\UrlGeneration\TemporaryUrlGenerator;
|
||||
use Throwable;
|
||||
|
||||
class PrefixedAdapter implements FilesystemAdapter, PublicUrlGenerator, ChecksumProvider, TemporaryUrlGenerator
|
||||
{
|
||||
use CalculateChecksumFromStream;
|
||||
|
||||
/** @var PathPrefixer */
|
||||
private $prefix;
|
||||
|
||||
/** @var FilesystemAdapter */
|
||||
private $adapter;
|
||||
|
||||
public function __construct(FilesystemAdapter $adapter, string $prefix)
|
||||
{
|
||||
if ($prefix === '') {
|
||||
throw new \InvalidArgumentException('The prefix must not be empty.');
|
||||
}
|
||||
|
||||
$this->adapter = $adapter;
|
||||
$this->prefix = new PathPrefixer($prefix);
|
||||
}
|
||||
|
||||
public function read(string $location): string
|
||||
{
|
||||
try {
|
||||
return $this->adapter->read($this->prefix->prefixPath($location));
|
||||
} catch (Throwable $previous) {
|
||||
throw UnableToReadFile::fromLocation($location, $previous->getMessage(), $previous);
|
||||
}
|
||||
}
|
||||
|
||||
public function readStream(string $location)
|
||||
{
|
||||
try {
|
||||
return $this->adapter->readStream($this->prefix->prefixPath($location));
|
||||
} catch (Throwable $previous) {
|
||||
throw UnableToReadFile::fromLocation($location, $previous->getMessage(), $previous);
|
||||
}
|
||||
}
|
||||
|
||||
public function listContents(string $location, bool $deep): Generator
|
||||
{
|
||||
foreach ($this->adapter->listContents($this->prefix->prefixPath($location), $deep) as $attributes) {
|
||||
yield $attributes->withPath($this->prefix->stripPrefix($attributes->path()));
|
||||
}
|
||||
}
|
||||
|
||||
public function fileExists(string $location): bool
|
||||
{
|
||||
try {
|
||||
return $this->adapter->fileExists($this->prefix->prefixPath($location));
|
||||
} catch (Throwable $previous) {
|
||||
throw UnableToCheckFileExistence::forLocation($location, $previous);
|
||||
}
|
||||
}
|
||||
|
||||
public function directoryExists(string $location): bool
|
||||
{
|
||||
try {
|
||||
return $this->adapter->directoryExists($this->prefix->prefixPath($location));
|
||||
} catch (Throwable $previous) {
|
||||
throw UnableToCheckDirectoryExistence::forLocation($location, $previous);
|
||||
}
|
||||
}
|
||||
|
||||
public function lastModified(string $path): FileAttributes
|
||||
{
|
||||
try {
|
||||
return $this->adapter->lastModified($this->prefix->prefixPath($path));
|
||||
} catch (Throwable $previous) {
|
||||
throw UnableToRetrieveMetadata::lastModified($path, $previous->getMessage(), $previous);
|
||||
}
|
||||
}
|
||||
|
||||
public function fileSize(string $path): FileAttributes
|
||||
{
|
||||
try {
|
||||
return $this->adapter->fileSize($this->prefix->prefixPath($path));
|
||||
} catch (Throwable $previous) {
|
||||
throw UnableToRetrieveMetadata::fileSize($path, $previous->getMessage(), $previous);
|
||||
}
|
||||
}
|
||||
|
||||
public function mimeType(string $path): FileAttributes
|
||||
{
|
||||
try {
|
||||
return $this->adapter->mimeType($this->prefix->prefixPath($path));
|
||||
} catch (Throwable $previous) {
|
||||
throw UnableToRetrieveMetadata::mimeType($path, $previous->getMessage(), $previous);
|
||||
}
|
||||
}
|
||||
|
||||
public function visibility(string $path): FileAttributes
|
||||
{
|
||||
try {
|
||||
return $this->adapter->visibility($this->prefix->prefixPath($path));
|
||||
} catch (Throwable $previous) {
|
||||
throw UnableToRetrieveMetadata::visibility($path, $previous->getMessage(), $previous);
|
||||
}
|
||||
}
|
||||
|
||||
public function write(string $location, string $contents, Config $config): void
|
||||
{
|
||||
try {
|
||||
$this->adapter->write($this->prefix->prefixPath($location), $contents, $config);
|
||||
} catch (Throwable $previous) {
|
||||
throw UnableToWriteFile::atLocation($location, $previous->getMessage(), $previous);
|
||||
}
|
||||
}
|
||||
|
||||
public function writeStream(string $location, $contents, Config $config): void
|
||||
{
|
||||
try {
|
||||
$this->adapter->writeStream($this->prefix->prefixPath($location), $contents, $config);
|
||||
} catch (Throwable $previous) {
|
||||
throw UnableToWriteFile::atLocation($location, $previous->getMessage(), $previous);
|
||||
}
|
||||
}
|
||||
|
||||
public function setVisibility(string $path, string $visibility): void
|
||||
{
|
||||
try {
|
||||
$this->adapter->setVisibility($this->prefix->prefixPath($path), $visibility);
|
||||
} catch (Throwable $previous) {
|
||||
throw UnableToSetVisibility::atLocation($path, $previous->getMessage(), $previous);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(string $location): void
|
||||
{
|
||||
try {
|
||||
$this->adapter->delete($this->prefix->prefixPath($location));
|
||||
} catch (Throwable $previous) {
|
||||
throw UnableToDeleteFile::atLocation($location, $previous->getMessage(), $previous);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteDirectory(string $location): void
|
||||
{
|
||||
try {
|
||||
$this->adapter->deleteDirectory($this->prefix->prefixPath($location));
|
||||
} catch (Throwable $previous) {
|
||||
throw UnableToDeleteDirectory::atLocation($location, $previous->getMessage(), $previous);
|
||||
}
|
||||
}
|
||||
|
||||
public function createDirectory(string $location, Config $config): void
|
||||
{
|
||||
try {
|
||||
$this->adapter->createDirectory($this->prefix->prefixPath($location), $config);
|
||||
} catch (Throwable $previous) {
|
||||
throw UnableToCreateDirectory::atLocation($location, $previous->getMessage(), $previous);
|
||||
}
|
||||
}
|
||||
|
||||
public function move(string $source, string $destination, Config $config): void
|
||||
{
|
||||
try {
|
||||
$this->adapter->move(
|
||||
$this->prefix->prefixPath($source),
|
||||
$this->prefix->prefixPath($destination),
|
||||
$config
|
||||
);
|
||||
} catch (Throwable $previous) {
|
||||
throw UnableToMoveFile::fromLocationTo($source, $destination, $previous);
|
||||
}
|
||||
}
|
||||
|
||||
public function copy(string $source, string $destination, Config $config): void
|
||||
{
|
||||
try {
|
||||
$this->adapter->copy(
|
||||
$this->prefix->prefixPath($source),
|
||||
$this->prefix->prefixPath($destination),
|
||||
$config
|
||||
);
|
||||
} catch (Throwable $previous) {
|
||||
throw UnableToCopyFile::fromLocationTo($source, $destination, $previous);
|
||||
}
|
||||
}
|
||||
|
||||
public function publicUrl(string $path, Config $config): string
|
||||
{
|
||||
if (! $this->adapter instanceof PublicUrlGenerator) {
|
||||
throw UnableToGeneratePublicUrl::noGeneratorConfigured($path);
|
||||
}
|
||||
|
||||
return $this->adapter->publicUrl($this->prefix->prefixPath($path), $config);
|
||||
}
|
||||
|
||||
public function checksum(string $path, Config $config): string
|
||||
{
|
||||
if ($this->adapter instanceof ChecksumProvider) {
|
||||
return $this->adapter->checksum($this->prefix->prefixPath($path), $config);
|
||||
}
|
||||
|
||||
return $this->calculateChecksumFromStream($path, $config);
|
||||
}
|
||||
|
||||
public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string
|
||||
{
|
||||
if (! $this->adapter instanceof TemporaryUrlGenerator) {
|
||||
throw UnableToGenerateTemporaryUrl::noGeneratorConfigured($path);
|
||||
}
|
||||
|
||||
return $this->adapter->temporaryUrl(
|
||||
$this->prefix->prefixPath($path),
|
||||
$expiresAt,
|
||||
$config
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -68,11 +68,14 @@ class S3Adapter implements AdapterInterface
|
|||
mimeTypeDetector: $this->mimeTypeDetector
|
||||
);
|
||||
|
||||
if (! empty($settings['prefix'])) {
|
||||
$adapter = new PathPrefixedAdapter(
|
||||
$adapter,
|
||||
trim($settings['prefix'], '/')
|
||||
);
|
||||
$prefix = trim((string) ($settings['prefix'] ?? ''), '/');
|
||||
|
||||
if ($prefix !== '') {
|
||||
if (class_exists(PathPrefixedAdapter::class)) {
|
||||
$adapter = new PathPrefixedAdapter($adapter, $prefix);
|
||||
} else {
|
||||
$adapter = new PrefixedAdapter($adapter, $prefix);
|
||||
}
|
||||
}
|
||||
|
||||
return $adapter;
|
||||
|
|
@ -92,7 +95,7 @@ class S3Adapter implements AdapterInterface
|
|||
$endpoint = $settings['endpoint'] ?? null;
|
||||
$region = $settings['region'] ?? 'us-east-1';
|
||||
$usePathStyle = (bool) ($settings['use_path_style_endpoint'] ?? false);
|
||||
$prefix = trim($settings['prefix'] ?? '', '/');
|
||||
$prefix = trim((string) ($settings['prefix'] ?? ''), '/');
|
||||
|
||||
$normalizedUrl = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -39,4 +39,17 @@ final class PathHelper
|
|||
|
||||
return self::trimTrailingSlash($path);
|
||||
}
|
||||
|
||||
public static function normalizePrefix(?string $prefix): string
|
||||
{
|
||||
if ($prefix === null || $prefix === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$prefix = self::stripProtocol($prefix);
|
||||
$prefix = self::normalizeDirectory($prefix);
|
||||
$prefix = self::trimSlashes($prefix);
|
||||
|
||||
return $prefix === '' ? '' : self::ensureTrailingSlash($prefix);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,18 @@ if (! defined('ABSPATH')) {
|
|||
exit;
|
||||
}
|
||||
|
||||
if (! class_exists(\WP_Image_Editor_GD::class) && file_exists(ABSPATH . WPINC . '/class-wp-image-editor-gd.php')) {
|
||||
require_once ABSPATH . WPINC . '/class-wp-image-editor-gd.php';
|
||||
if (! class_exists(\WP_Image_Editor::class, false)) {
|
||||
require_once ABSPATH . WPINC . '/class-wp-image-editor.php';
|
||||
}
|
||||
|
||||
if (! class_exists(\WP_Image_Editor_GD::class)) {
|
||||
if (! class_exists(\WP_Image_Editor_GD::class, false)) {
|
||||
$gdEditorFile = ABSPATH . WPINC . '/class-wp-image-editor-gd.php';
|
||||
if (file_exists($gdEditorFile)) {
|
||||
require_once $gdEditorFile;
|
||||
}
|
||||
}
|
||||
|
||||
if (! class_exists(\WP_Image_Editor_GD::class, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,11 +11,19 @@ if (! defined('ABSPATH')) {
|
|||
exit;
|
||||
}
|
||||
|
||||
if (! class_exists(\WP_Image_Editor_Imagick::class) && file_exists(ABSPATH . WPINC . '/class-wp-image-editor-imagick.php')) {
|
||||
require_once ABSPATH . WPINC . '/class-wp-image-editor-imagick.php';
|
||||
if (! class_exists(\WP_Image_Editor::class, false)) {
|
||||
require_once ABSPATH . WPINC . '/class-wp-image-editor.php';
|
||||
}
|
||||
|
||||
if (! class_exists(\WP_Image_Editor_Imagick::class)) {
|
||||
if (! class_exists(\WP_Image_Editor_Imagick::class, false)) {
|
||||
$imagickEditorFile = ABSPATH . WPINC . '/class-wp-image-editor-imagick.php';
|
||||
|
||||
if (file_exists($imagickEditorFile)) {
|
||||
require_once $imagickEditorFile;
|
||||
}
|
||||
}
|
||||
|
||||
if (! class_exists(\WP_Image_Editor_Imagick::class, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
303
src/Plugin.php
303
src/Plugin.php
|
|
@ -1,261 +1,142 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FlysystemOffload;
|
||||
|
||||
use FlysystemOffload\Admin\HealthCheck;
|
||||
use FlysystemOffload\Config\ConfigLoader;
|
||||
use FlysystemOffload\Filesystem\FilesystemFactory;
|
||||
use FlysystemOffload\Helpers\PathHelper;
|
||||
use FlysystemOffload\Media\MediaHooks;
|
||||
use FlysystemOffload\Settings\SettingsPage;
|
||||
use FlysystemOffload\StreamWrapper\FlysystemStreamWrapper;
|
||||
use League\Flysystem\FilesystemOperator;
|
||||
use League\Flysystem\PortableVisibility;
|
||||
use Throwable;
|
||||
use League\Flysystem\Visibility;
|
||||
use RuntimeException;
|
||||
use WP_Error;
|
||||
|
||||
final class Plugin
|
||||
{
|
||||
private static ?self $instance = null;
|
||||
private static string $pluginFile;
|
||||
|
||||
private ?FilesystemOperator $filesystem = null;
|
||||
private bool $streamRegistered = false;
|
||||
private array $config = [];
|
||||
|
||||
private ConfigLoader $configLoader;
|
||||
private FilesystemFactory $filesystemFactory;
|
||||
private MediaHooks $mediaHooks;
|
||||
private SettingsPage $settingsPage;
|
||||
private array $config = [];
|
||||
private bool $isInitialized = false;
|
||||
|
||||
public function __construct()
|
||||
public function __construct(
|
||||
FilesystemFactory $filesystemFactory,
|
||||
MediaHooks $mediaHooks,
|
||||
SettingsPage $settingsPage
|
||||
) {
|
||||
$this->filesystemFactory = $filesystemFactory;
|
||||
$this->mediaHooks = $mediaHooks;
|
||||
$this->settingsPage = $settingsPage;
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
$this->mediaHooks = new MediaHooks();
|
||||
add_action('plugins_loaded', [$this, 'bootstrap'], 5);
|
||||
add_action('init', [$this, 'loadTextDomain']);
|
||||
add_filter('plugin_action_links_' . plugin_basename(FlysystemOffload::PLUGIN_FILE), [$this, 'pluginLinks']);
|
||||
}
|
||||
|
||||
public static function bootstrap(string $pluginFile): void
|
||||
public function loadTextDomain(): void
|
||||
{
|
||||
self::$pluginFile = $pluginFile;
|
||||
|
||||
register_activation_hook($pluginFile, [self::class, 'activate']);
|
||||
register_deactivation_hook($pluginFile, [self::class, 'deactivate']);
|
||||
|
||||
add_action('plugins_loaded', static function (): void {
|
||||
self::instance()->init();
|
||||
});
|
||||
load_plugin_textdomain(
|
||||
'flysystem-offload',
|
||||
false,
|
||||
dirname(plugin_basename(FlysystemOffload::PLUGIN_FILE)) . '/languages'
|
||||
);
|
||||
}
|
||||
|
||||
public static function instance(): self
|
||||
public function bootstrap(): void
|
||||
{
|
||||
return self::$instance ??= new self();
|
||||
if ($this->isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
public static function activate(): void
|
||||
{
|
||||
wp_mkdir_p(WP_CONTENT_DIR . '/flysystem-uploads');
|
||||
$this->config = $this->settingsPage->getSettings();
|
||||
|
||||
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.');
|
||||
if (! is_array($this->config)) {
|
||||
$this->config = [];
|
||||
}
|
||||
|
||||
$this->registerStreamWrapper();
|
||||
$this->mediaHooks->register($this);
|
||||
$this->settingsPage->register($this);
|
||||
|
||||
if (defined('WP_CLI') && WP_CLI) {
|
||||
if (class_exists(HealthCheck::class)) {
|
||||
\WP_CLI::add_command('flysystem-offload health-check', new HealthCheck($this->filesystemFactory));
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
public function init(): void
|
||||
{
|
||||
$this->configLoader = new ConfigLoader(self::$pluginFile);
|
||||
$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('get_attached_file', [$this, 'filterAttachedFile'], 20, 2);
|
||||
|
||||
add_filter('wp_delete_file', [$this, 'handleDeleteFile'], 20);
|
||||
add_action('delete_attachment', [$this, 'handleDeleteAttachment'], 20);
|
||||
add_action('switch_blog', [$this, 'handleSwitchBlog']);
|
||||
add_action('flysystem_offload_reload_config', [$this, 'reloadConfig']);
|
||||
|
||||
if (defined('WP_CLI') && WP_CLI && class_exists(\FlysystemOffload\Admin\HealthCheck::class)) {
|
||||
\WP_CLI::add_command('flysystem-offload health-check', [\FlysystemOffload\Admin\HealthCheck::class, 'run']);
|
||||
}
|
||||
$this->isInitialized = true;
|
||||
}
|
||||
|
||||
public function reloadConfig(): void
|
||||
{
|
||||
try {
|
||||
$this->config = $this->configLoader->load();
|
||||
} catch (Throwable $exception) {
|
||||
error_log('[Flysystem Offload] Error cargando configuración: ' . $exception->getMessage());
|
||||
$this->config = $this->configLoader->defaults();
|
||||
}
|
||||
|
||||
$this->filesystem = null;
|
||||
$this->streamRegistered = false;
|
||||
|
||||
$this->mediaHooks->unregister();
|
||||
$this->mediaHooks->setFilesystem(null);
|
||||
$this->config = $this->settingsPage->getSettings();
|
||||
if (! is_array($this->config)) {
|
||||
$this->config = [];
|
||||
}
|
||||
|
||||
$this->registerStreamWrapper();
|
||||
$this->mediaHooks->register($this);
|
||||
}
|
||||
|
||||
public function handleSwitchBlog(): void
|
||||
public function registerStreamWrapper(): void
|
||||
{
|
||||
$this->reloadConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function getFilesystem(): FilesystemOperator
|
||||
{
|
||||
if ($this->filesystem === null) {
|
||||
$factory = new FilesystemFactory($this->config);
|
||||
$result = $factory->make();
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
throw new \RuntimeException($result->get_error_message());
|
||||
}
|
||||
|
||||
$this->filesystem = $result;
|
||||
}
|
||||
|
||||
return $this->filesystem;
|
||||
}
|
||||
|
||||
public function filterUploadDir(array $dirs): array
|
||||
{
|
||||
$remoteBase = $this->getRemoteUrlBase();
|
||||
$prefix = PathHelper::normalizePrefix($this->config['base_prefix'] ?? '');
|
||||
|
||||
$subdir = $dirs['subdir'] ?? '';
|
||||
$dirs['path'] = "fly://{$prefix}" . ltrim($subdir, '/');
|
||||
$dirs['basedir'] = "fly://{$prefix}";
|
||||
$dirs['url'] = trailingslashit($remoteBase) . ltrim($subdir, '/');
|
||||
$dirs['baseurl'] = $remoteBase;
|
||||
|
||||
return $dirs;
|
||||
}
|
||||
|
||||
public function filterAttachmentUrl(string $url, int $postId): string
|
||||
{
|
||||
unset($postId);
|
||||
|
||||
$uploadDir = wp_get_upload_dir();
|
||||
$localBase = trailingslashit($uploadDir['baseurl'] ?? '');
|
||||
$remoteBase = trailingslashit($this->getRemoteUrlBase());
|
||||
|
||||
return str_replace($localBase, $remoteBase, $url);
|
||||
}
|
||||
|
||||
public function filterAttachmentMetadata(array $metadata): array
|
||||
{
|
||||
if (! empty($metadata['file'])) {
|
||||
$metadata['file'] = PathHelper::stripProtocol($metadata['file']);
|
||||
}
|
||||
|
||||
if (! empty($metadata['sizes'])) {
|
||||
foreach ($metadata['sizes'] as &$size) {
|
||||
if (! empty($size['file'])) {
|
||||
$size['file'] = ltrim(PathHelper::stripProtocol($size['file']), '/');
|
||||
}
|
||||
}
|
||||
|
||||
unset($size);
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
public function filterOriginalImagePath(string $path): string
|
||||
{
|
||||
return PathHelper::ensureFlyProtocol($path);
|
||||
}
|
||||
|
||||
public function filterAttachedFile(string $file, int $attachmentId): string
|
||||
{
|
||||
unset($attachmentId);
|
||||
|
||||
if (PathHelper::isFlyProtocol($file)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return PathHelper::ensureFlyProtocol($file);
|
||||
}
|
||||
|
||||
public function handleDeleteFile(string $file): string|false
|
||||
{
|
||||
$flyPath = PathHelper::stripProtocol($file);
|
||||
|
||||
try {
|
||||
$this->getFilesystem()->delete($flyPath);
|
||||
} catch (Throwable $exception) {
|
||||
error_log('[Flysystem Offload] Error al borrar archivo: ' . $flyPath . ' - ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleDeleteAttachment(int $postId): void
|
||||
{
|
||||
$files = PathHelper::collectFilesFromAttachment($postId);
|
||||
|
||||
foreach ($files as $relativePath) {
|
||||
try {
|
||||
$this->getFilesystem()->delete($relativePath);
|
||||
} catch (Throwable $exception) {
|
||||
error_log('[Flysystem Offload] Error al borrar attachment: ' . $relativePath . ' - ' . $exception->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function registerStreamWrapper(): void
|
||||
{
|
||||
if ($this->streamRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$filesystem = $this->getFilesystem();
|
||||
} catch (Throwable $exception) {
|
||||
error_log('[Flysystem Offload] No se pudo obtener el filesystem: ' . $exception->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$visibility = $this->config['visibility'] ?? PortableVisibility::PUBLIC;
|
||||
$filesystem = $this->filesystemFactory->build($this->config);
|
||||
$protocol = $this->config['protocol'] ?? 'fly';
|
||||
$prefix = $this->config['root_prefix'] ?? '';
|
||||
|
||||
FlysystemStreamWrapper::register(
|
||||
$filesystem,
|
||||
'fly',
|
||||
PathHelper::normalizePrefix($this->config['base_prefix'] ?? ''),
|
||||
['visibility' => $visibility]
|
||||
$protocol,
|
||||
$prefix,
|
||||
[
|
||||
'visibility' => $this->config['visibility'] ?? Visibility::PUBLIC,
|
||||
]
|
||||
);
|
||||
|
||||
$this->streamRegistered = true;
|
||||
} catch (Throwable $exception) {
|
||||
} catch (\Throwable $exception) {
|
||||
error_log('[Flysystem Offload] No se pudo registrar el stream wrapper: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
$this->mediaHooks->setFilesystem($filesystem);
|
||||
$this->mediaHooks->register();
|
||||
}
|
||||
|
||||
private function getRemoteUrlBase(): string
|
||||
public function pluginLinks(array $links): array
|
||||
{
|
||||
$adapterKey = $this->config['adapter'] ?? 'local';
|
||||
$settings = $this->config['adapters'][$adapterKey] ?? [];
|
||||
$settingsUrl = admin_url('options-general.php?page=flysystem-offload');
|
||||
|
||||
return (new FilesystemFactory($this->config))
|
||||
->resolvePublicBaseUrl($adapterKey, $settings);
|
||||
$links[] = '<a href="' . esc_url($settingsUrl) . '">' . esc_html__('Ajustes', 'flysystem-offload') . '</a>';
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
public function getConfig(): array
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
public function getFilesystemFactory(): FilesystemFactory
|
||||
{
|
||||
return $this->filesystemFactory;
|
||||
}
|
||||
|
||||
public function getRemoteUrlBase(): string
|
||||
{
|
||||
$driver = $this->config['driver'] ?? null;
|
||||
|
||||
if (! $driver) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$result = $this->filesystemFactory->resolvePublicBaseUrl($driver, $this->config);
|
||||
|
||||
if ($result instanceof WP_Error) {
|
||||
throw new RuntimeException($result->get_error_message());
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FlysystemOffload\Settings;
|
||||
|
||||
use FlysystemOffload\Plugin;
|
||||
|
||||
class SettingsPage
|
||||
{
|
||||
private const OPTION_KEY = 'flysystem_offload_settings';
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getSettings(): array
|
||||
{
|
||||
$settings = get_option(self::OPTION_KEY, []);
|
||||
|
||||
return is_array($settings) ? $settings : [];
|
||||
}
|
||||
|
||||
public function register(Plugin $plugin): void
|
||||
{
|
||||
add_action('admin_menu', function () {
|
||||
add_options_page(
|
||||
__('Flysystem Offload', 'flysystem-offload'),
|
||||
__('Flysystem Offload', 'flysystem-offload'),
|
||||
'manage_options',
|
||||
'flysystem-offload',
|
||||
[$this, 'renderPage']
|
||||
);
|
||||
});
|
||||
|
||||
add_action('admin_init', [$this, 'registerSettings']);
|
||||
}
|
||||
|
||||
public function renderPage(): void
|
||||
{
|
||||
if (! current_user_can('manage_options')) {
|
||||
wp_die(__('No tienes permisos para acceder a esta página.', 'flysystem-offload'));
|
||||
}
|
||||
|
||||
$settings = $this->getSettings();
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e('Flysystem Offload', 'flysystem-offload'); ?></h1>
|
||||
<p><?php esc_html_e('Configura el almacenamiento remoto para WordPress.', 'flysystem-offload'); ?></p>
|
||||
|
||||
<form action="options.php" method="post">
|
||||
<?php
|
||||
settings_fields('flysystem_offload');
|
||||
do_settings_sections('flysystem-offload');
|
||||
submit_button();
|
||||
?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function registerSettings(): void
|
||||
{
|
||||
register_setting('flysystem_offload', self::OPTION_KEY);
|
||||
|
||||
add_settings_section(
|
||||
'flysystem_offload_general',
|
||||
__('Configuración General', 'flysystem-offload'),
|
||||
function () {
|
||||
echo '<p>' . esc_html__(
|
||||
'Introduce las credenciales del proveedor que deseas utilizar.',
|
||||
'flysystem-offload'
|
||||
) . '</p>';
|
||||
},
|
||||
'flysystem-offload'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'flysystem_offload_driver',
|
||||
__('Driver', 'flysystem-offload'),
|
||||
[$this, 'renderDriverField'],
|
||||
'flysystem-offload',
|
||||
'flysystem_offload_general'
|
||||
);
|
||||
}
|
||||
|
||||
public function renderDriverField(): void
|
||||
{
|
||||
$settings = $this->getSettings();
|
||||
$driver = $settings['driver'] ?? '';
|
||||
?>
|
||||
<select name="<?php echo esc_attr(self::OPTION_KEY . '[driver]'); ?>">
|
||||
<option value=""><?php esc_html_e('Selecciona un driver', 'flysystem-offload'); ?></option>
|
||||
<option value="s3" <?php selected($driver, 's3'); ?>><?php esc_html_e('Amazon S3 / Compatible', 'flysystem-offload'); ?></option>
|
||||
<option value="gcs" <?php selected($driver, 'gcs'); ?>><?php esc_html_e('Google Cloud Storage', 'flysystem-offload'); ?></option>
|
||||
<option value="azure" <?php selected($driver, 'azure'); ?>><?php esc_html_e('Azure Blob Storage', 'flysystem-offload'); ?></option>
|
||||
<option value="sftp" <?php selected($driver, 'sftp'); ?>><?php esc_html_e('SFTP', 'flysystem-offload'); ?></option>
|
||||
<option value="webdav" <?php selected($driver, 'webdav'); ?>><?php esc_html_e('WebDAV', 'flysystem-offload'); ?></option>
|
||||
</select>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
|
@ -6,11 +6,11 @@ namespace FlysystemOffload\StreamWrapper;
|
|||
|
||||
use League\Flysystem\FilesystemException;
|
||||
use League\Flysystem\FilesystemOperator;
|
||||
use League\Flysystem\PortableVisibility;
|
||||
use League\Flysystem\StorageAttributes;
|
||||
use League\Flysystem\UnableToDeleteFile;
|
||||
use League\Flysystem\UnableToReadFile;
|
||||
use League\Flysystem\UnableToWriteFile;
|
||||
use League\Flysystem\Visibility;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ final class FlysystemStreamWrapper
|
|||
self::$filesystems[$protocol] = $filesystem;
|
||||
self::$prefixes[$protocol] = trim($prefix, '/');
|
||||
self::$writeOptions[$protocol] = $writeOptions + [
|
||||
'visibility' => PortableVisibility::PUBLIC,
|
||||
'visibility' => Visibility::PUBLIC,
|
||||
];
|
||||
|
||||
stream_wrapper_register($protocol, static::class, STREAM_IS_URL);
|
||||
|
|
@ -500,6 +500,6 @@ final class FlysystemStreamWrapper
|
|||
*/
|
||||
private function writeOptionsForProtocol(string $protocol): array
|
||||
{
|
||||
return self::$writeOptions[$protocol] ?? ['visibility' => PortableVisibility::PUBLIC];
|
||||
return self::$writeOptions[$protocol] ?? ['visibility' => Visibility::PUBLIC];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue