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());
|
||||
}
|
||||
}
|
||||
}
|
||||
125
src/Plugin.php
125
src/Plugin.php
|
|
@ -1,21 +1,27 @@
|
|||
<?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\StreamWrapper\FlysystemStreamWrapper;
|
||||
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 ?FilesystemOperator $filesystem = null;
|
||||
private bool $streamRegistered = false;
|
||||
private array $config = [];
|
||||
|
||||
private ConfigLoader $configLoader;
|
||||
private MediaHooks $mediaHooks;
|
||||
|
||||
|
|
@ -31,7 +37,7 @@ class Plugin
|
|||
register_activation_hook($pluginFile, [self::class, 'activate']);
|
||||
register_deactivation_hook($pluginFile, [self::class, 'deactivate']);
|
||||
|
||||
add_action('plugins_loaded', static function () {
|
||||
add_action('plugins_loaded', static function (): void {
|
||||
self::instance()->init();
|
||||
});
|
||||
}
|
||||
|
|
@ -65,22 +71,21 @@ class Plugin
|
|||
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('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('switch_blog', [$this, 'handleSwitchBlog']);
|
||||
|
||||
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']);
|
||||
if (defined('WP_CLI') && WP_CLI && class_exists(HealthCheck::class)) {
|
||||
\WP_CLI::add_command('flysystem-offload health-check', [HealthCheck::class, 'run']);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,8 +93,8 @@ class Plugin
|
|||
{
|
||||
try {
|
||||
$this->config = $this->configLoader->load();
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[Flysystem Offload] Error cargando configuración: ' . $e->getMessage());
|
||||
} catch (Throwable $exception) {
|
||||
error_log('[Flysystem Offload] Error cargando configuración: ' . $exception->getMessage());
|
||||
$this->config = $this->configLoader->defaults();
|
||||
}
|
||||
|
||||
|
|
@ -107,9 +112,12 @@ class Plugin
|
|||
$this->reloadConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function getFilesystem(): FilesystemOperator
|
||||
{
|
||||
if (! $this->filesystem) {
|
||||
if ($this->filesystem === null) {
|
||||
$factory = new FilesystemFactory($this->config);
|
||||
$result = $factory->make();
|
||||
|
||||
|
|
@ -123,41 +131,13 @@ class Plugin
|
|||
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
|
||||
{
|
||||
$remoteBase = $this->getRemoteUrlBase();
|
||||
$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['url'] = trailingslashit($remoteBase) . ltrim($subdir, '/');
|
||||
$dirs['baseurl'] = $remoteBase;
|
||||
|
|
@ -167,7 +147,10 @@ class Plugin
|
|||
|
||||
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());
|
||||
|
||||
return str_replace($localBase, $remoteBase, $url);
|
||||
|
|
@ -182,9 +165,10 @@ class Plugin
|
|||
if (! empty($metadata['sizes'])) {
|
||||
foreach ($metadata['sizes'] as &$size) {
|
||||
if (! empty($size['file'])) {
|
||||
$size['file'] = ltrim($size['file'], '/');
|
||||
$size['file'] = ltrim(PathHelper::stripProtocol($size['file']), '/');
|
||||
}
|
||||
}
|
||||
|
||||
unset($size);
|
||||
}
|
||||
|
||||
|
|
@ -196,14 +180,25 @@ class Plugin
|
|||
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 $e) {
|
||||
error_log('[Flysystem Offload] Error al borrar archivo: ' . $flyPath . ' - ' . $e->getMessage());
|
||||
} catch (Throwable $exception) {
|
||||
error_log('[Flysystem Offload] Error al borrar archivo: ' . $flyPath . ' - ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
@ -216,17 +211,51 @@ class Plugin
|
|||
foreach ($files as $relativePath) {
|
||||
try {
|
||||
$this->getFilesystem()->delete($relativePath);
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[Flysystem Offload] Error al borrar attachment: ' . $relativePath . ' - ' . $e->getMessage());
|
||||
} 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;
|
||||
|
||||
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
|
||||
{
|
||||
$adapterKey = $this->config['adapter'] ?? 'local';
|
||||
$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\FilesystemOperator;
|
||||
use League\Flysystem\PortableVisibility;
|
||||
use League\Flysystem\StorageAttributes;
|
||||
use League\Flysystem\UnableToDeleteFile;
|
||||
use League\Flysystem\UnableToReadFile;
|
||||
use League\Flysystem\UnableToWriteFile;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class FlysystemStreamWrapper
|
||||
final class FlysystemStreamWrapper
|
||||
{
|
||||
/** @var array<string, FilesystemOperator> */
|
||||
private static array $filesystems = [];
|
||||
|
|
@ -18,6 +21,9 @@ class FlysystemStreamWrapper
|
|||
/** @var array<string, string> */
|
||||
private static array $prefixes = [];
|
||||
|
||||
/** @var array<string, array<string, mixed>> */
|
||||
private static array $writeOptions = [];
|
||||
|
||||
/** @var resource|null */
|
||||
private $stream = null;
|
||||
|
||||
|
|
@ -31,41 +37,54 @@ class FlysystemStreamWrapper
|
|||
/** @var resource|array|string|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)) {
|
||||
stream_wrapper_unregister($protocol);
|
||||
}
|
||||
|
||||
self::$filesystems[$protocol] = $filesystem;
|
||||
self::$prefixes[$protocol] = trim($prefix, '/');
|
||||
self::$writeOptions[$protocol] = $writeOptions + [
|
||||
'visibility' => PortableVisibility::PUBLIC,
|
||||
];
|
||||
|
||||
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);
|
||||
$this->protocol = $protocol;
|
||||
unset($openedPath);
|
||||
$this->protocol = $this->extractProtocol($path);
|
||||
$this->path = $path;
|
||||
$this->mode = $mode;
|
||||
$this->flyPath = $this->resolveFlyPath($path);
|
||||
|
||||
$filesystem = $this->filesystem($protocol);
|
||||
|
||||
$filesystem = $this->filesystem($this->protocol);
|
||||
$binary = str_contains($mode, 'b') ? 'b' : '';
|
||||
|
||||
if (strpbrk($mode, 'waxc')) {
|
||||
if (strpbrk($mode, 'waxc') !== false) {
|
||||
$this->stream = fopen('php://temp', 'w+' . $binary);
|
||||
|
||||
if ($this->stream === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (str_contains($mode, 'a') && $filesystem->fileExists($this->flyPath)) {
|
||||
try {
|
||||
$remote = $filesystem->readStream($this->flyPath);
|
||||
|
||||
if (is_resource($remote)) {
|
||||
stream_copy_to_stream($remote, $this->stream);
|
||||
fclose($remote);
|
||||
}
|
||||
} catch (UnableToReadFile $e) {
|
||||
} catch (UnableToReadFile $exception) {
|
||||
error_log('[Flysystem Offload] Unable to open stream for append: ' . $exception->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -77,37 +96,54 @@ class FlysystemStreamWrapper
|
|||
|
||||
try {
|
||||
$remote = $filesystem->readStream($this->flyPath);
|
||||
|
||||
if (! is_resource($remote)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$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);
|
||||
rewind($local);
|
||||
|
||||
$this->stream = $local;
|
||||
|
||||
return true;
|
||||
} catch (UnableToReadFile $e) {
|
||||
} catch (UnableToReadFile $exception) {
|
||||
error_log('[Flysystem Offload] Unable to open stream: ' . $exception->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function stream_read(int $count): string|false
|
||||
{
|
||||
if (! is_resource($this->stream)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return fread($this->stream, $count);
|
||||
}
|
||||
|
||||
public function stream_write(string $data): int|false
|
||||
{
|
||||
if (! is_resource($this->stream)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return fwrite($this->stream, $data);
|
||||
}
|
||||
|
||||
public function stream_flush(): bool
|
||||
{
|
||||
if (! strpbrk($this->mode, 'waxc')) {
|
||||
if (! is_resource($this->stream) || strpbrk($this->mode, 'waxc') === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -115,19 +151,25 @@ class FlysystemStreamWrapper
|
|||
|
||||
try {
|
||||
$meta = stream_get_meta_data($this->stream);
|
||||
if ($meta['seekable'] ?? false) {
|
||||
$seekable = (bool) ($meta['seekable'] ?? false);
|
||||
|
||||
if ($seekable) {
|
||||
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);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (UnableToWriteFile $e) {
|
||||
error_log('[Flysystem Offload] Unable to flush stream: ' . $e->getMessage());
|
||||
} catch (UnableToWriteFile|Throwable $exception) {
|
||||
error_log('[Flysystem Offload] Unable to flush stream: ' . $exception->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -142,11 +184,18 @@ class FlysystemStreamWrapper
|
|||
}
|
||||
|
||||
$this->stream = null;
|
||||
$this->path = '';
|
||||
$this->mode = '';
|
||||
$this->flyPath = '';
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -160,27 +209,31 @@ class FlysystemStreamWrapper
|
|||
|
||||
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
|
||||
{
|
||||
// WordPress suele invocar chmod/chown/chgrp incluso sobre streams remotos.
|
||||
// Los tratamos como no-ops y devolvemos true para evitar warnings.
|
||||
unset($path, $option, $value);
|
||||
|
||||
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)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Aseguramos que el puntero esté al inicio cuando se castea como stream.
|
||||
if (in_array($cast_as, [STREAM_CAST_FOR_SELECT, STREAM_CAST_AS_STREAM], true)) {
|
||||
if (in_array($castAs, [STREAM_CAST_FOR_SELECT, STREAM_CAST_AS_STREAM], true)) {
|
||||
$meta = stream_get_meta_data($this->stream);
|
||||
if ($meta['seekable'] ?? false) {
|
||||
$seekable = (bool) ($meta['seekable'] ?? false);
|
||||
|
||||
if ($seekable) {
|
||||
rewind($this->stream);
|
||||
}
|
||||
|
||||
|
|
@ -192,13 +245,24 @@ class FlysystemStreamWrapper
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -208,7 +272,8 @@ class FlysystemStreamWrapper
|
|||
$flyPath = $this->resolveFlyPath($path);
|
||||
|
||||
try {
|
||||
$exists = $filesystem->fileExists($flyPath) || $filesystem->directoryExists($flyPath);
|
||||
$isDirectory = $filesystem->directoryExists($flyPath);
|
||||
$exists = $isDirectory || $filesystem->fileExists($flyPath);
|
||||
|
||||
if (! $exists) {
|
||||
if ($flags & STREAM_URL_STAT_QUIET) {
|
||||
|
|
@ -220,17 +285,18 @@ class FlysystemStreamWrapper
|
|||
return false;
|
||||
}
|
||||
|
||||
$isDir = $filesystem->directoryExists($flyPath);
|
||||
$size = $isDir ? 0 : $filesystem->fileSize($flyPath);
|
||||
$size = $isDirectory ? 0 : $filesystem->fileSize($flyPath);
|
||||
$mtime = $filesystem->lastModified($flyPath);
|
||||
|
||||
$mode = $isDirectory ? 0040777 : 0100777;
|
||||
|
||||
return [
|
||||
0 => 0,
|
||||
'dev' => 0,
|
||||
1 => 0,
|
||||
'ino' => 0,
|
||||
2 => $isDir ? 0040777 : 0100777,
|
||||
'mode' => $isDir ? 0040777 : 0100777,
|
||||
2 => $mode,
|
||||
'mode' => $mode,
|
||||
3 => 0,
|
||||
'nlink' => 0,
|
||||
4 => 0,
|
||||
|
|
@ -252,9 +318,9 @@ class FlysystemStreamWrapper
|
|||
12 => -1,
|
||||
'blocks' => -1,
|
||||
];
|
||||
} catch (FilesystemException $e) {
|
||||
} catch (FilesystemException $exception) {
|
||||
if (! ($flags & STREAM_URL_STAT_QUIET)) {
|
||||
trigger_error($e->getMessage(), E_USER_WARNING);
|
||||
trigger_error($exception->getMessage(), E_USER_WARNING);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
@ -271,13 +337,17 @@ class FlysystemStreamWrapper
|
|||
$filesystem->delete($flyPath);
|
||||
|
||||
return true;
|
||||
} catch (UnableToDeleteFile $e) {
|
||||
} catch (UnableToDeleteFile $exception) {
|
||||
error_log('[Flysystem Offload] Unable to delete file: ' . $exception->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function mkdir(string $path, int $mode, int $options): bool
|
||||
{
|
||||
unset($mode, $options);
|
||||
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$filesystem = $this->filesystem($protocol);
|
||||
$flyPath = $this->resolveFlyPath($path);
|
||||
|
|
@ -286,13 +356,17 @@ class FlysystemStreamWrapper
|
|||
$filesystem->createDirectory($flyPath);
|
||||
|
||||
return true;
|
||||
} catch (FilesystemException $e) {
|
||||
} catch (FilesystemException $exception) {
|
||||
error_log('[Flysystem Offload] Unable to create directory: ' . $exception->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function rmdir(string $path, int $options): bool
|
||||
{
|
||||
unset($options);
|
||||
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$filesystem = $this->filesystem($protocol);
|
||||
$flyPath = $this->resolveFlyPath($path);
|
||||
|
|
@ -301,7 +375,9 @@ class FlysystemStreamWrapper
|
|||
$filesystem->deleteDirectory($flyPath);
|
||||
|
||||
return true;
|
||||
} catch (FilesystemException $e) {
|
||||
} catch (FilesystemException $exception) {
|
||||
error_log('[Flysystem Offload] Unable to delete directory: ' . $exception->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -323,27 +399,33 @@ class FlysystemStreamWrapper
|
|||
$filesystem->move($from, $to);
|
||||
|
||||
return true;
|
||||
} catch (FilesystemException $e) {
|
||||
} catch (FilesystemException $exception) {
|
||||
error_log('[Flysystem Offload] Unable to move file: ' . $exception->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function dir_opendir(string $path, int $options): bool
|
||||
{
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$this->protocol = $protocol;
|
||||
unset($options);
|
||||
|
||||
$this->protocol = $this->extractProtocol($path);
|
||||
$this->flyPath = $this->resolveFlyPath($path);
|
||||
$filesystem = $this->filesystem($protocol);
|
||||
$this->dirEntries = ['.', '..'];
|
||||
|
||||
$filesystem = $this->filesystem($this->protocol);
|
||||
|
||||
$entries = ['.', '..'];
|
||||
|
||||
foreach ($filesystem->listContents($this->flyPath, false) as $item) {
|
||||
if ($item instanceof StorageAttributes) {
|
||||
$this->dirEntries[] = basename($item->path());
|
||||
$entries[] = basename($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;
|
||||
|
||||
return true;
|
||||
|
|
@ -376,7 +458,7 @@ class FlysystemStreamWrapper
|
|||
private function filesystem(string $protocol): FilesystemOperator
|
||||
{
|
||||
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];
|
||||
|
|
@ -385,14 +467,39 @@ class FlysystemStreamWrapper
|
|||
private function resolveFlyPath(string $path): string
|
||||
{
|
||||
$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
|
||||
{
|
||||
$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