2.0.1
This commit is contained in:
parent
3a316ce2cf
commit
f0f1ee8c00
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace FlysystemOffload\Admin;
|
||||||
|
|
||||||
|
use FlysystemOffload\Plugin;
|
||||||
|
use FlysystemOffload\Helpers\PathHelper;
|
||||||
|
use League\Flysystem\PortableVisibility;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class HealthCheck
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $args
|
||||||
|
* @param array<string, mixed> $assocArgs
|
||||||
|
*/
|
||||||
|
public static function run(array $args, array $assocArgs): void
|
||||||
|
{
|
||||||
|
unset($args, $assocArgs);
|
||||||
|
|
||||||
|
if (! class_exists('\WP_CLI')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$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.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$filesystem->delete($testKey);
|
||||||
|
|
||||||
|
foreach ($report as $line) {
|
||||||
|
\WP_CLI::line($line);
|
||||||
|
}
|
||||||
|
|
||||||
|
\WP_CLI::success('Health-check completado correctamente.');
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
\WP_CLI::error('Health-check falló: ' . $exception->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
133
src/Plugin.php
133
src/Plugin.php
|
|
@ -1,21 +1,27 @@
|
||||||
<?php
|
<?php
|
||||||
|
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\Helpers\PathHelper;
|
use FlysystemOffload\Helpers\PathHelper;
|
||||||
use FlysystemOffload\Media\MediaHooks;
|
use FlysystemOffload\Media\MediaHooks;
|
||||||
use FlysystemOffload\StreamWrapper\FlysystemStreamWrapper;
|
use FlysystemOffload\StreamWrapper\FlysystemStreamWrapper;
|
||||||
use League\Flysystem\FilesystemOperator;
|
use League\Flysystem\FilesystemOperator;
|
||||||
|
use League\Flysystem\PortableVisibility;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
class Plugin
|
final class Plugin
|
||||||
{
|
{
|
||||||
private static $instance;
|
private static ?self $instance = null;
|
||||||
private static string $pluginFile;
|
private static string $pluginFile;
|
||||||
|
|
||||||
private ?FilesystemOperator $filesystem = null;
|
private ?FilesystemOperator $filesystem = null;
|
||||||
private bool $streamRegistered = false;
|
private bool $streamRegistered = false;
|
||||||
private array $config = [];
|
private array $config = [];
|
||||||
|
|
||||||
private ConfigLoader $configLoader;
|
private ConfigLoader $configLoader;
|
||||||
private MediaHooks $mediaHooks;
|
private MediaHooks $mediaHooks;
|
||||||
|
|
||||||
|
|
@ -31,7 +37,7 @@ class Plugin
|
||||||
register_activation_hook($pluginFile, [self::class, 'activate']);
|
register_activation_hook($pluginFile, [self::class, 'activate']);
|
||||||
register_deactivation_hook($pluginFile, [self::class, 'deactivate']);
|
register_deactivation_hook($pluginFile, [self::class, 'deactivate']);
|
||||||
|
|
||||||
add_action('plugins_loaded', static function () {
|
add_action('plugins_loaded', static function (): void {
|
||||||
self::instance()->init();
|
self::instance()->init();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -65,22 +71,21 @@ class Plugin
|
||||||
public function init(): void
|
public function init(): void
|
||||||
{
|
{
|
||||||
$this->configLoader = new ConfigLoader(self::$pluginFile);
|
$this->configLoader = new ConfigLoader(self::$pluginFile);
|
||||||
|
|
||||||
$this->reloadConfig();
|
$this->reloadConfig();
|
||||||
|
|
||||||
add_filter('upload_dir', [$this, 'filterUploadDir'], 20);
|
add_filter('upload_dir', [$this, 'filterUploadDir'], 20);
|
||||||
add_filter('wp_get_attachment_url', [$this, 'filterAttachmentUrl'], 20, 2);
|
add_filter('wp_get_attachment_url', [$this, 'filterAttachmentUrl'], 20, 2);
|
||||||
add_filter('wp_get_attachment_metadata', [$this, 'filterAttachmentMetadata'], 20);
|
add_filter('wp_get_attachment_metadata', [$this, 'filterAttachmentMetadata'], 20);
|
||||||
add_filter('wp_get_original_image_path', [$this, 'filterOriginalImagePath'], 20);
|
add_filter('wp_get_original_image_path', [$this, 'filterOriginalImagePath'], 20);
|
||||||
add_filter('wp_delete_file', [$this, 'handleDeleteFile'], 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('delete_attachment', [$this, 'handleDeleteAttachment'], 20);
|
||||||
add_action('switch_blog', [$this, 'handleSwitchBlog']);
|
add_action('switch_blog', [$this, 'handleSwitchBlog']);
|
||||||
|
|
||||||
add_action('flysystem_offload_reload_config', [$this, 'reloadConfig']);
|
add_action('flysystem_offload_reload_config', [$this, 'reloadConfig']);
|
||||||
|
|
||||||
if (defined('WP_CLI') && WP_CLI) {
|
if (defined('WP_CLI') && WP_CLI && class_exists(HealthCheck::class)) {
|
||||||
\WP_CLI::add_command('flysystem-offload health-check', [Admin\HealthCheck::class, 'run']);
|
\WP_CLI::add_command('flysystem-offload health-check', [HealthCheck::class, 'run']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,8 +93,8 @@ class Plugin
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->config = $this->configLoader->load();
|
$this->config = $this->configLoader->load();
|
||||||
} catch (\Throwable $e) {
|
} catch (Throwable $exception) {
|
||||||
error_log('[Flysystem Offload] Error cargando configuración: ' . $e->getMessage());
|
error_log('[Flysystem Offload] Error cargando configuración: ' . $exception->getMessage());
|
||||||
$this->config = $this->configLoader->defaults();
|
$this->config = $this->configLoader->defaults();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,11 +112,14 @@ class Plugin
|
||||||
$this->reloadConfig();
|
$this->reloadConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Throwable
|
||||||
|
*/
|
||||||
public function getFilesystem(): FilesystemOperator
|
public function getFilesystem(): FilesystemOperator
|
||||||
{
|
{
|
||||||
if (! $this->filesystem) {
|
if ($this->filesystem === null) {
|
||||||
$factory = new FilesystemFactory($this->config);
|
$factory = new FilesystemFactory($this->config);
|
||||||
$result = $factory->make();
|
$result = $factory->make();
|
||||||
|
|
||||||
if (is_wp_error($result)) {
|
if (is_wp_error($result)) {
|
||||||
throw new \RuntimeException($result->get_error_message());
|
throw new \RuntimeException($result->get_error_message());
|
||||||
|
|
@ -123,43 +131,15 @@ class Plugin
|
||||||
return $this->filesystem;
|
return $this->filesystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function registerStreamWrapper(): void
|
|
||||||
{
|
|
||||||
if ($this->streamRegistered) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$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
|
public function filterUploadDir(array $dirs): array
|
||||||
{
|
{
|
||||||
$remoteBase = $this->getRemoteUrlBase();
|
$remoteBase = $this->getRemoteUrlBase();
|
||||||
$prefix = PathHelper::normalizePrefix($this->config['base_prefix'] ?? '');
|
$prefix = PathHelper::normalizePrefix($this->config['base_prefix'] ?? '');
|
||||||
$subdir = $dirs['subdir'] ?? '';
|
|
||||||
|
|
||||||
$dirs['path'] = "fly://{$prefix}{$subdir}";
|
$subdir = $dirs['subdir'] ?? '';
|
||||||
|
$dirs['path'] = "fly://{$prefix}" . ltrim($subdir, '/');
|
||||||
$dirs['basedir'] = "fly://{$prefix}";
|
$dirs['basedir'] = "fly://{$prefix}";
|
||||||
$dirs['url'] = trailingslashit($remoteBase) . ltrim($subdir, '/');
|
$dirs['url'] = trailingslashit($remoteBase) . ltrim($subdir, '/');
|
||||||
$dirs['baseurl'] = $remoteBase;
|
$dirs['baseurl'] = $remoteBase;
|
||||||
|
|
||||||
return $dirs;
|
return $dirs;
|
||||||
|
|
@ -167,7 +147,10 @@ class Plugin
|
||||||
|
|
||||||
public function filterAttachmentUrl(string $url, int $postId): string
|
public function filterAttachmentUrl(string $url, int $postId): string
|
||||||
{
|
{
|
||||||
$localBase = trailingslashit(wp_get_upload_dir()['baseurl']);
|
unset($postId);
|
||||||
|
|
||||||
|
$uploadDir = wp_get_upload_dir();
|
||||||
|
$localBase = trailingslashit($uploadDir['baseurl'] ?? '');
|
||||||
$remoteBase = trailingslashit($this->getRemoteUrlBase());
|
$remoteBase = trailingslashit($this->getRemoteUrlBase());
|
||||||
|
|
||||||
return str_replace($localBase, $remoteBase, $url);
|
return str_replace($localBase, $remoteBase, $url);
|
||||||
|
|
@ -182,9 +165,10 @@ class Plugin
|
||||||
if (! empty($metadata['sizes'])) {
|
if (! empty($metadata['sizes'])) {
|
||||||
foreach ($metadata['sizes'] as &$size) {
|
foreach ($metadata['sizes'] as &$size) {
|
||||||
if (! empty($size['file'])) {
|
if (! empty($size['file'])) {
|
||||||
$size['file'] = ltrim($size['file'], '/');
|
$size['file'] = ltrim(PathHelper::stripProtocol($size['file']), '/');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unset($size);
|
unset($size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,14 +180,25 @@ class Plugin
|
||||||
return PathHelper::ensureFlyProtocol($path);
|
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
|
public function handleDeleteFile(string $file): string|false
|
||||||
{
|
{
|
||||||
$flyPath = PathHelper::stripProtocol($file);
|
$flyPath = PathHelper::stripProtocol($file);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->getFilesystem()->delete($flyPath);
|
$this->getFilesystem()->delete($flyPath);
|
||||||
} catch (\Throwable $e) {
|
} catch (Throwable $exception) {
|
||||||
error_log('[Flysystem Offload] Error al borrar archivo: ' . $flyPath . ' - ' . $e->getMessage());
|
error_log('[Flysystem Offload] Error al borrar archivo: ' . $flyPath . ' - ' . $exception->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -216,17 +211,51 @@ class Plugin
|
||||||
foreach ($files as $relativePath) {
|
foreach ($files as $relativePath) {
|
||||||
try {
|
try {
|
||||||
$this->getFilesystem()->delete($relativePath);
|
$this->getFilesystem()->delete($relativePath);
|
||||||
} catch (\Throwable $e) {
|
} catch (Throwable $exception) {
|
||||||
error_log('[Flysystem Offload] Error al borrar attachment: ' . $relativePath . ' - ' . $e->getMessage());
|
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;
|
||||||
|
|
||||||
|
FlysystemStreamWrapper::register(
|
||||||
|
$filesystem,
|
||||||
|
'fly',
|
||||||
|
PathHelper::normalizePrefix($this->config['base_prefix'] ?? ''),
|
||||||
|
['visibility' => $visibility]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->streamRegistered = true;
|
||||||
|
} 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
|
private function getRemoteUrlBase(): string
|
||||||
{
|
{
|
||||||
$adapterKey = $this->config['adapter'] ?? 'local';
|
$adapterKey = $this->config['adapter'] ?? 'local';
|
||||||
$settings = $this->config['adapters'][$adapterKey] ?? [];
|
$settings = $this->config['adapters'][$adapterKey] ?? [];
|
||||||
|
|
||||||
return (new FilesystemFactory($this->config))->resolvePublicBaseUrl($adapterKey, $settings);
|
return (new FilesystemFactory($this->config))
|
||||||
|
->resolvePublicBaseUrl($adapterKey, $settings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,15 @@ namespace FlysystemOffload\StreamWrapper;
|
||||||
|
|
||||||
use League\Flysystem\FilesystemException;
|
use League\Flysystem\FilesystemException;
|
||||||
use League\Flysystem\FilesystemOperator;
|
use League\Flysystem\FilesystemOperator;
|
||||||
|
use League\Flysystem\PortableVisibility;
|
||||||
use League\Flysystem\StorageAttributes;
|
use League\Flysystem\StorageAttributes;
|
||||||
use League\Flysystem\UnableToDeleteFile;
|
use League\Flysystem\UnableToDeleteFile;
|
||||||
use League\Flysystem\UnableToReadFile;
|
use League\Flysystem\UnableToReadFile;
|
||||||
use League\Flysystem\UnableToWriteFile;
|
use League\Flysystem\UnableToWriteFile;
|
||||||
|
use RuntimeException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
class FlysystemStreamWrapper
|
final class FlysystemStreamWrapper
|
||||||
{
|
{
|
||||||
/** @var array<string, FilesystemOperator> */
|
/** @var array<string, FilesystemOperator> */
|
||||||
private static array $filesystems = [];
|
private static array $filesystems = [];
|
||||||
|
|
@ -18,6 +21,9 @@ class FlysystemStreamWrapper
|
||||||
/** @var array<string, string> */
|
/** @var array<string, string> */
|
||||||
private static array $prefixes = [];
|
private static array $prefixes = [];
|
||||||
|
|
||||||
|
/** @var array<string, array<string, mixed>> */
|
||||||
|
private static array $writeOptions = [];
|
||||||
|
|
||||||
/** @var resource|null */
|
/** @var resource|null */
|
||||||
private $stream = null;
|
private $stream = null;
|
||||||
|
|
||||||
|
|
@ -31,41 +37,54 @@ class FlysystemStreamWrapper
|
||||||
/** @var resource|array|string|null */
|
/** @var resource|array|string|null */
|
||||||
public $context = null;
|
public $context = null;
|
||||||
|
|
||||||
public static function register(FilesystemOperator $filesystem, string $protocol, string $prefix = ''): void
|
public static function register(
|
||||||
{
|
FilesystemOperator $filesystem,
|
||||||
|
string $protocol,
|
||||||
|
string $prefix = '',
|
||||||
|
array $writeOptions = []
|
||||||
|
): void {
|
||||||
if (in_array($protocol, stream_get_wrappers(), true)) {
|
if (in_array($protocol, stream_get_wrappers(), true)) {
|
||||||
stream_wrapper_unregister($protocol);
|
stream_wrapper_unregister($protocol);
|
||||||
}
|
}
|
||||||
|
|
||||||
self::$filesystems[$protocol] = $filesystem;
|
self::$filesystems[$protocol] = $filesystem;
|
||||||
self::$prefixes[$protocol] = trim($prefix, '/');
|
self::$prefixes[$protocol] = trim($prefix, '/');
|
||||||
|
self::$writeOptions[$protocol] = $writeOptions + [
|
||||||
|
'visibility' => PortableVisibility::PUBLIC,
|
||||||
|
];
|
||||||
|
|
||||||
stream_wrapper_register($protocol, static::class, STREAM_IS_URL);
|
stream_wrapper_register($protocol, static::class, STREAM_IS_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool
|
public function stream_open(string $path, string $mode, int $options, ?string &$openedPath): bool
|
||||||
{
|
{
|
||||||
$protocol = $this->extractProtocol($path);
|
unset($openedPath);
|
||||||
$this->protocol = $protocol;
|
$this->protocol = $this->extractProtocol($path);
|
||||||
$this->path = $path;
|
$this->path = $path;
|
||||||
$this->mode = $mode;
|
$this->mode = $mode;
|
||||||
$this->flyPath = $this->resolveFlyPath($path);
|
$this->flyPath = $this->resolveFlyPath($path);
|
||||||
|
|
||||||
$filesystem = $this->filesystem($protocol);
|
|
||||||
|
|
||||||
|
$filesystem = $this->filesystem($this->protocol);
|
||||||
$binary = str_contains($mode, 'b') ? 'b' : '';
|
$binary = str_contains($mode, 'b') ? 'b' : '';
|
||||||
|
|
||||||
if (strpbrk($mode, 'waxc')) {
|
if (strpbrk($mode, 'waxc') !== false) {
|
||||||
$this->stream = fopen('php://temp', 'w+' . $binary);
|
$this->stream = fopen('php://temp', 'w+' . $binary);
|
||||||
|
|
||||||
|
if ($this->stream === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (str_contains($mode, 'a') && $filesystem->fileExists($this->flyPath)) {
|
if (str_contains($mode, 'a') && $filesystem->fileExists($this->flyPath)) {
|
||||||
try {
|
try {
|
||||||
$remote = $filesystem->readStream($this->flyPath);
|
$remote = $filesystem->readStream($this->flyPath);
|
||||||
|
|
||||||
if (is_resource($remote)) {
|
if (is_resource($remote)) {
|
||||||
stream_copy_to_stream($remote, $this->stream);
|
stream_copy_to_stream($remote, $this->stream);
|
||||||
fclose($remote);
|
fclose($remote);
|
||||||
}
|
}
|
||||||
} catch (UnableToReadFile $e) {
|
} catch (UnableToReadFile $exception) {
|
||||||
|
error_log('[Flysystem Offload] Unable to open stream for append: ' . $exception->getMessage());
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -77,37 +96,54 @@ class FlysystemStreamWrapper
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$remote = $filesystem->readStream($this->flyPath);
|
$remote = $filesystem->readStream($this->flyPath);
|
||||||
|
|
||||||
if (! is_resource($remote)) {
|
if (! is_resource($remote)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$local = fopen('php://temp', 'w+' . $binary);
|
$local = fopen('php://temp', 'w+' . $binary);
|
||||||
stream_copy_to_stream($remote, $local);
|
|
||||||
|
|
||||||
|
if ($local === false) {
|
||||||
|
fclose($remote);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream_copy_to_stream($remote, $local);
|
||||||
fclose($remote);
|
fclose($remote);
|
||||||
rewind($local);
|
rewind($local);
|
||||||
|
|
||||||
$this->stream = $local;
|
$this->stream = $local;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (UnableToReadFile $e) {
|
} catch (UnableToReadFile $exception) {
|
||||||
|
error_log('[Flysystem Offload] Unable to open stream: ' . $exception->getMessage());
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function stream_read(int $count): string|false
|
public function stream_read(int $count): string|false
|
||||||
{
|
{
|
||||||
|
if (! is_resource($this->stream)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return fread($this->stream, $count);
|
return fread($this->stream, $count);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function stream_write(string $data): int|false
|
public function stream_write(string $data): int|false
|
||||||
{
|
{
|
||||||
|
if (! is_resource($this->stream)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return fwrite($this->stream, $data);
|
return fwrite($this->stream, $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function stream_flush(): bool
|
public function stream_flush(): bool
|
||||||
{
|
{
|
||||||
if (! strpbrk($this->mode, 'waxc')) {
|
if (! is_resource($this->stream) || strpbrk($this->mode, 'waxc') === false) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,19 +151,25 @@ class FlysystemStreamWrapper
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$meta = stream_get_meta_data($this->stream);
|
$meta = stream_get_meta_data($this->stream);
|
||||||
if ($meta['seekable'] ?? false) {
|
$seekable = (bool) ($meta['seekable'] ?? false);
|
||||||
|
|
||||||
|
if ($seekable) {
|
||||||
rewind($this->stream);
|
rewind($this->stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
$filesystem->writeStream($this->flyPath, $this->stream);
|
$filesystem->writeStream(
|
||||||
|
$this->flyPath,
|
||||||
|
$this->stream,
|
||||||
|
$this->writeOptionsForProtocol($this->protocol)
|
||||||
|
);
|
||||||
|
|
||||||
if ($meta['seekable'] ?? false) {
|
if ($seekable) {
|
||||||
rewind($this->stream);
|
rewind($this->stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (UnableToWriteFile $e) {
|
} catch (UnableToWriteFile|Throwable $exception) {
|
||||||
error_log('[Flysystem Offload] Unable to flush stream: ' . $e->getMessage());
|
error_log('[Flysystem Offload] Unable to flush stream: ' . $exception->getMessage());
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -142,11 +184,18 @@ class FlysystemStreamWrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->stream = null;
|
$this->stream = null;
|
||||||
|
$this->path = '';
|
||||||
|
$this->mode = '';
|
||||||
|
$this->flyPath = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function stream_tell(): int|false
|
public function stream_tell(): int|false
|
||||||
{
|
{
|
||||||
return is_resource($this->stream) ? ftell($this->stream) : false;
|
if (! is_resource($this->stream)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ftell($this->stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function stream_seek(int $offset, int $whence = SEEK_SET): bool
|
public function stream_seek(int $offset, int $whence = SEEK_SET): bool
|
||||||
|
|
@ -160,27 +209,31 @@ class FlysystemStreamWrapper
|
||||||
|
|
||||||
public function stream_eof(): bool
|
public function stream_eof(): bool
|
||||||
{
|
{
|
||||||
return is_resource($this->stream) ? feof($this->stream) : true;
|
if (! is_resource($this->stream)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return feof($this->stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function stream_metadata(string $path, int $option, mixed $value): bool
|
public function stream_metadata(string $path, int $option, mixed $value): bool
|
||||||
{
|
{
|
||||||
// WordPress suele invocar chmod/chown/chgrp incluso sobre streams remotos.
|
unset($path, $option, $value);
|
||||||
// Los tratamos como no-ops y devolvemos true para evitar warnings.
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function stream_cast(int $cast_as)
|
public function stream_cast(int $castAs)
|
||||||
{
|
{
|
||||||
// Permite que funciones como getimagesize() o getID3 obtengan el recurso subyacente.
|
|
||||||
if (! is_resource($this->stream)) {
|
if (! is_resource($this->stream)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aseguramos que el puntero esté al inicio cuando se castea como stream.
|
if (in_array($castAs, [STREAM_CAST_FOR_SELECT, STREAM_CAST_AS_STREAM], true)) {
|
||||||
if (in_array($cast_as, [STREAM_CAST_FOR_SELECT, STREAM_CAST_AS_STREAM], true)) {
|
|
||||||
$meta = stream_get_meta_data($this->stream);
|
$meta = stream_get_meta_data($this->stream);
|
||||||
if ($meta['seekable'] ?? false) {
|
$seekable = (bool) ($meta['seekable'] ?? false);
|
||||||
|
|
||||||
|
if ($seekable) {
|
||||||
rewind($this->stream);
|
rewind($this->stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,23 +245,35 @@ class FlysystemStreamWrapper
|
||||||
|
|
||||||
public function stream_set_option(int $option, int $arg1, int $arg2): bool
|
public function stream_set_option(int $option, int $arg1, int $arg2): bool
|
||||||
{
|
{
|
||||||
// No se requieren operaciones especiales; devolvemos false para indicar que no se manejó.
|
if (! is_resource($this->stream)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($option === STREAM_OPTION_READ_TIMEOUT) {
|
||||||
|
return stream_set_timeout($this->stream, $arg1, $arg2);
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function stream_stat(): array|false
|
public function stream_stat(): array|false
|
||||||
{
|
{
|
||||||
return $this->url_stat($this->path, 0);
|
if (is_resource($this->stream)) {
|
||||||
|
return fstat($this->stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->url_stat($this->path !== '' ? $this->path : $this->protocol . '://', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function url_stat(string $path, int $flags): array|false
|
public function url_stat(string $path, int $flags): array|false
|
||||||
{
|
{
|
||||||
$protocol = $this->extractProtocol($path);
|
$protocol = $this->extractProtocol($path);
|
||||||
$filesystem = $this->filesystem($protocol);
|
$filesystem = $this->filesystem($protocol);
|
||||||
$flyPath = $this->resolveFlyPath($path);
|
$flyPath = $this->resolveFlyPath($path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$exists = $filesystem->fileExists($flyPath) || $filesystem->directoryExists($flyPath);
|
$isDirectory = $filesystem->directoryExists($flyPath);
|
||||||
|
$exists = $isDirectory || $filesystem->fileExists($flyPath);
|
||||||
|
|
||||||
if (! $exists) {
|
if (! $exists) {
|
||||||
if ($flags & STREAM_URL_STAT_QUIET) {
|
if ($flags & STREAM_URL_STAT_QUIET) {
|
||||||
|
|
@ -220,17 +285,18 @@ class FlysystemStreamWrapper
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$isDir = $filesystem->directoryExists($flyPath);
|
$size = $isDirectory ? 0 : $filesystem->fileSize($flyPath);
|
||||||
$size = $isDir ? 0 : $filesystem->fileSize($flyPath);
|
|
||||||
$mtime = $filesystem->lastModified($flyPath);
|
$mtime = $filesystem->lastModified($flyPath);
|
||||||
|
|
||||||
|
$mode = $isDirectory ? 0040777 : 0100777;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
0 => 0,
|
0 => 0,
|
||||||
'dev' => 0,
|
'dev' => 0,
|
||||||
1 => 0,
|
1 => 0,
|
||||||
'ino' => 0,
|
'ino' => 0,
|
||||||
2 => $isDir ? 0040777 : 0100777,
|
2 => $mode,
|
||||||
'mode' => $isDir ? 0040777 : 0100777,
|
'mode' => $mode,
|
||||||
3 => 0,
|
3 => 0,
|
||||||
'nlink' => 0,
|
'nlink' => 0,
|
||||||
4 => 0,
|
4 => 0,
|
||||||
|
|
@ -252,9 +318,9 @@ class FlysystemStreamWrapper
|
||||||
12 => -1,
|
12 => -1,
|
||||||
'blocks' => -1,
|
'blocks' => -1,
|
||||||
];
|
];
|
||||||
} catch (FilesystemException $e) {
|
} catch (FilesystemException $exception) {
|
||||||
if (! ($flags & STREAM_URL_STAT_QUIET)) {
|
if (! ($flags & STREAM_URL_STAT_QUIET)) {
|
||||||
trigger_error($e->getMessage(), E_USER_WARNING);
|
trigger_error($exception->getMessage(), E_USER_WARNING);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -263,45 +329,55 @@ class FlysystemStreamWrapper
|
||||||
|
|
||||||
public function unlink(string $path): bool
|
public function unlink(string $path): bool
|
||||||
{
|
{
|
||||||
$protocol = $this->extractProtocol($path);
|
$protocol = $this->extractProtocol($path);
|
||||||
$filesystem = $this->filesystem($protocol);
|
$filesystem = $this->filesystem($protocol);
|
||||||
$flyPath = $this->resolveFlyPath($path);
|
$flyPath = $this->resolveFlyPath($path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$filesystem->delete($flyPath);
|
$filesystem->delete($flyPath);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (UnableToDeleteFile $e) {
|
} catch (UnableToDeleteFile $exception) {
|
||||||
|
error_log('[Flysystem Offload] Unable to delete file: ' . $exception->getMessage());
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mkdir(string $path, int $mode, int $options): bool
|
public function mkdir(string $path, int $mode, int $options): bool
|
||||||
{
|
{
|
||||||
$protocol = $this->extractProtocol($path);
|
unset($mode, $options);
|
||||||
|
|
||||||
|
$protocol = $this->extractProtocol($path);
|
||||||
$filesystem = $this->filesystem($protocol);
|
$filesystem = $this->filesystem($protocol);
|
||||||
$flyPath = $this->resolveFlyPath($path);
|
$flyPath = $this->resolveFlyPath($path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$filesystem->createDirectory($flyPath);
|
$filesystem->createDirectory($flyPath);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (FilesystemException $e) {
|
} catch (FilesystemException $exception) {
|
||||||
|
error_log('[Flysystem Offload] Unable to create directory: ' . $exception->getMessage());
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rmdir(string $path, int $options): bool
|
public function rmdir(string $path, int $options): bool
|
||||||
{
|
{
|
||||||
$protocol = $this->extractProtocol($path);
|
unset($options);
|
||||||
|
|
||||||
|
$protocol = $this->extractProtocol($path);
|
||||||
$filesystem = $this->filesystem($protocol);
|
$filesystem = $this->filesystem($protocol);
|
||||||
$flyPath = $this->resolveFlyPath($path);
|
$flyPath = $this->resolveFlyPath($path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$filesystem->deleteDirectory($flyPath);
|
$filesystem->deleteDirectory($flyPath);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (FilesystemException $e) {
|
} catch (FilesystemException $exception) {
|
||||||
|
error_log('[Flysystem Offload] Unable to delete directory: ' . $exception->getMessage());
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -316,34 +392,40 @@ class FlysystemStreamWrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
$filesystem = $this->filesystem($oldProtocol);
|
$filesystem = $this->filesystem($oldProtocol);
|
||||||
$from = $this->resolveFlyPath($oldPath);
|
$from = $this->resolveFlyPath($oldPath);
|
||||||
$to = $this->resolveFlyPath($newPath);
|
$to = $this->resolveFlyPath($newPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$filesystem->move($from, $to);
|
$filesystem->move($from, $to);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (FilesystemException $e) {
|
} catch (FilesystemException $exception) {
|
||||||
|
error_log('[Flysystem Offload] Unable to move file: ' . $exception->getMessage());
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function dir_opendir(string $path, int $options): bool
|
public function dir_opendir(string $path, int $options): bool
|
||||||
{
|
{
|
||||||
$protocol = $this->extractProtocol($path);
|
unset($options);
|
||||||
$this->protocol = $protocol;
|
|
||||||
$this->flyPath = $this->resolveFlyPath($path);
|
$this->protocol = $this->extractProtocol($path);
|
||||||
$filesystem = $this->filesystem($protocol);
|
$this->flyPath = $this->resolveFlyPath($path);
|
||||||
$this->dirEntries = ['.', '..'];
|
|
||||||
|
$filesystem = $this->filesystem($this->protocol);
|
||||||
|
|
||||||
|
$entries = ['.', '..'];
|
||||||
|
|
||||||
foreach ($filesystem->listContents($this->flyPath, false) as $item) {
|
foreach ($filesystem->listContents($this->flyPath, false) as $item) {
|
||||||
if ($item instanceof StorageAttributes) {
|
if ($item instanceof StorageAttributes) {
|
||||||
$this->dirEntries[] = basename($item->path());
|
$entries[] = basename($item->path());
|
||||||
} elseif (is_array($item) && isset($item['path'])) {
|
} elseif (is_array($item) && isset($item['path'])) {
|
||||||
$this->dirEntries[] = basename($item['path']);
|
$entries[] = basename((string) $item['path']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->dirEntries = $entries;
|
||||||
$this->dirPosition = 0;
|
$this->dirPosition = 0;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -367,7 +449,7 @@ class FlysystemStreamWrapper
|
||||||
|
|
||||||
public function dir_closedir(): bool
|
public function dir_closedir(): bool
|
||||||
{
|
{
|
||||||
$this->dirEntries = [];
|
$this->dirEntries = [];
|
||||||
$this->dirPosition = 0;
|
$this->dirPosition = 0;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -376,7 +458,7 @@ class FlysystemStreamWrapper
|
||||||
private function filesystem(string $protocol): FilesystemOperator
|
private function filesystem(string $protocol): FilesystemOperator
|
||||||
{
|
{
|
||||||
if (! isset(self::$filesystems[$protocol])) {
|
if (! isset(self::$filesystems[$protocol])) {
|
||||||
throw new \RuntimeException('No filesystem registered for protocol: ' . $protocol);
|
throw new RuntimeException('No filesystem registered for protocol: ' . $protocol);
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::$filesystems[$protocol];
|
return self::$filesystems[$protocol];
|
||||||
|
|
@ -385,14 +467,39 @@ class FlysystemStreamWrapper
|
||||||
private function resolveFlyPath(string $path): string
|
private function resolveFlyPath(string $path): string
|
||||||
{
|
{
|
||||||
$raw = preg_replace('#^[^:]+://#', '', $path) ?? '';
|
$raw = preg_replace('#^[^:]+://#', '', $path) ?? '';
|
||||||
|
$relative = ltrim($raw, '/');
|
||||||
|
|
||||||
return ltrim($raw, '/');
|
$prefix = self::$prefixes[$this->protocol] ?? '';
|
||||||
|
|
||||||
|
if ($prefix !== '') {
|
||||||
|
$prefixWithSlash = $prefix . '/';
|
||||||
|
|
||||||
|
if (str_starts_with($relative, $prefixWithSlash)) {
|
||||||
|
$relative = substr($relative, strlen($prefixWithSlash));
|
||||||
|
} elseif ($relative === $prefix) {
|
||||||
|
$relative = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function extractProtocol(string $path): string
|
private function extractProtocol(string $path): string
|
||||||
{
|
{
|
||||||
$pos = strpos($path, '://');
|
$pos = strpos($path, '://');
|
||||||
|
|
||||||
return $pos === false ? $this->protocol : substr($path, 0, $pos);
|
if ($pos === false) {
|
||||||
|
return $this->protocol ?: 'fly';
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($path, 0, $pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function writeOptionsForProtocol(string $protocol): array
|
||||||
|
{
|
||||||
|
return self::$writeOptions[$protocol] ?? ['visibility' => PortableVisibility::PUBLIC];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue