2.0.0
This commit is contained in:
parent
bf16330bc7
commit
3ef11020d7
|
|
@ -1,6 +1,4 @@
|
|||
/vendor/
|
||||
/node_modules/
|
||||
.idea/
|
||||
*.log
|
||||
*.txt
|
||||
*.lock
|
||||
|
|
|
|||
|
|
@ -49,32 +49,44 @@ class S3Adapter implements AdapterInterface
|
|||
|
||||
public function publicBaseUrl(array $settings): string
|
||||
{
|
||||
if (! empty($settings['cdn_url'])) {
|
||||
return rtrim($settings['cdn_url'], '/');
|
||||
$cdn = $settings['cdn_base_url'] ?? null;
|
||||
if ($cdn) {
|
||||
return rtrim($cdn, '/');
|
||||
}
|
||||
|
||||
$bucket = $settings['bucket'] ?? '';
|
||||
$prefix = isset($settings['prefix']) ? trim($settings['prefix'], '/') : '';
|
||||
$prefix = $prefix === '' ? '' : '/' . $prefix;
|
||||
$bucket = $settings['bucket'] ?? '';
|
||||
$endpoint = $settings['endpoint'] ?? null;
|
||||
$region = $settings['region'] ?? 'us-east-1';
|
||||
$usePathStyle = (bool) ($settings['use_path_style_endpoint'] ?? false);
|
||||
$prefix = trim($settings['prefix'] ?? '', '/');
|
||||
|
||||
if (! empty($settings['endpoint'])) {
|
||||
$endpoint = trim($settings['endpoint']);
|
||||
if (! preg_match('#^https?://#i', $endpoint)) {
|
||||
$endpoint = 'https://' . $endpoint;
|
||||
}
|
||||
$normalizedUrl = null;
|
||||
|
||||
if ($endpoint) {
|
||||
$endpoint = rtrim($endpoint, '/');
|
||||
$parts = parse_url($endpoint);
|
||||
|
||||
// Cuando se usa endpoint propio forzamos path-style (+ bucket en la ruta)
|
||||
return $endpoint . '/' . $bucket . $prefix;
|
||||
if (! $parts || empty($parts['host'])) {
|
||||
$normalizedUrl = sprintf('%s/%s', $endpoint, $bucket);
|
||||
} elseif ($usePathStyle) {
|
||||
$path = isset($parts['path']) ? rtrim($parts['path'], '/') : '';
|
||||
$scheme = $parts['scheme'] ?? 'https';
|
||||
$port = isset($parts['port']) ? ':' . $parts['port'] : '';
|
||||
|
||||
$normalizedUrl = sprintf('%s://%s%s%s/%s', $scheme, $parts['host'], $port, $path, $bucket);
|
||||
} else {
|
||||
$scheme = $parts['scheme'] ?? 'https';
|
||||
$port = isset($parts['port']) ? ':' . $parts['port'] : '';
|
||||
$path = isset($parts['path']) ? rtrim($parts['path'], '/') : '';
|
||||
|
||||
$normalizedUrl = sprintf('%s://%s.%s%s%s', $scheme, $bucket, $parts['host'], $port, $path);
|
||||
}
|
||||
}
|
||||
|
||||
$region = $settings['region'] ?? 'us-east-1';
|
||||
|
||||
if ($region === 'us-east-1') {
|
||||
return "https://{$bucket}.s3.amazonaws.com{$prefix}";
|
||||
if (! $normalizedUrl) {
|
||||
$normalizedUrl = sprintf('https://%s.s3.%s.amazonaws.com', $bucket, $region);
|
||||
}
|
||||
|
||||
return "https://{$bucket}.s3.{$region}.amazonaws.com{$prefix}";
|
||||
return $prefix ? $normalizedUrl . '/' . $prefix : $normalizedUrl;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FlysystemOffload\Filesystem;
|
||||
|
||||
use FlysystemOffload\Filesystem\Adapters\AzureBlobAdapter;
|
||||
|
|
@ -9,8 +11,10 @@ use FlysystemOffload\Filesystem\Adapters\OneDriveAdapter;
|
|||
use FlysystemOffload\Filesystem\Adapters\S3Adapter;
|
||||
use FlysystemOffload\Filesystem\Adapters\SftpAdapter;
|
||||
use FlysystemOffload\Filesystem\Adapters\WebdavAdapter;
|
||||
use FlysystemOffload\Helpers\PathHelper;
|
||||
use League\Flysystem\Filesystem;
|
||||
use League\Flysystem\FilesystemOperator;
|
||||
use League\Flysystem\Local\LocalFilesystemAdapter;
|
||||
use WP_Error;
|
||||
|
||||
class FilesystemFactory
|
||||
|
|
@ -25,21 +29,21 @@ class FilesystemFactory
|
|||
public function make(): FilesystemOperator|WP_Error
|
||||
{
|
||||
$adapterKey = $this->settings['adapter'] ?? 'local';
|
||||
$config = $this->settings['adapters'][$adapterKey] ?? [];
|
||||
$config = $this->settings['adapters'][$adapterKey] ?? [];
|
||||
|
||||
$adapter = $this->resolveAdapter($adapterKey);
|
||||
|
||||
if (is_wp_error($adapter)) {
|
||||
if ($adapter instanceof WP_Error) {
|
||||
return $adapter;
|
||||
}
|
||||
|
||||
$validation = $adapter->validate($config);
|
||||
if (is_wp_error($validation)) {
|
||||
if ($validation instanceof WP_Error) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
$flyAdapter = $adapter->create($config);
|
||||
if (is_wp_error($flyAdapter)) {
|
||||
if ($flyAdapter instanceof WP_Error) {
|
||||
return $flyAdapter;
|
||||
}
|
||||
|
||||
|
|
@ -50,39 +54,46 @@ class FilesystemFactory
|
|||
{
|
||||
$adapter = $this->resolveAdapter($adapterKey);
|
||||
|
||||
if (is_wp_error($adapter)) {
|
||||
if ($adapter instanceof WP_Error) {
|
||||
return content_url('/uploads');
|
||||
}
|
||||
|
||||
return $adapter->publicBaseUrl($settings);
|
||||
$baseUrl = $adapter->publicBaseUrl($settings);
|
||||
|
||||
return untrailingslashit($baseUrl);
|
||||
}
|
||||
|
||||
private function resolveAdapter(string $adapterKey): AdapterInterface|WP_Error
|
||||
{
|
||||
return match ($adapterKey) {
|
||||
's3' => new S3Adapter(),
|
||||
'sftp' => new SftpAdapter(),
|
||||
'gcs' => new GoogleCloudAdapter(),
|
||||
'azure' => new AzureBlobAdapter(),
|
||||
'webdav' => new WebdavAdapter(),
|
||||
's3' => new S3Adapter(),
|
||||
'sftp' => new SftpAdapter(),
|
||||
'gcs' => new GoogleCloudAdapter(),
|
||||
'azure' => new AzureBlobAdapter(),
|
||||
'webdav' => new WebdavAdapter(),
|
||||
'googledrive' => new GoogleDriveAdapter(), // stub (dev)
|
||||
'onedrive' => new OneDriveAdapter(), // stub (dev)
|
||||
'dropbox' => new DropboxAdapter(), // stub (dev)
|
||||
default => new class implements AdapterInterface {
|
||||
'onedrive' => new OneDriveAdapter(), // stub (dev)
|
||||
'dropbox' => new DropboxAdapter(), // stub (dev)
|
||||
default => new class implements AdapterInterface {
|
||||
public function create(array $settings)
|
||||
{
|
||||
return new \League\Flysystem\Local\LocalFilesystemAdapter(WP_CONTENT_DIR . '/flysystem-uploads');
|
||||
$root = WP_CONTENT_DIR . '/flysystem-uploads';
|
||||
|
||||
return new LocalFilesystemAdapter($root);
|
||||
}
|
||||
|
||||
public function publicBaseUrl(array $settings): string
|
||||
{
|
||||
return content_url('/flysystem-uploads');
|
||||
}
|
||||
|
||||
public function validate(array $settings)
|
||||
{
|
||||
wp_mkdir_p(WP_CONTENT_DIR . '/flysystem-uploads');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,135 +3,227 @@ declare(strict_types=1);
|
|||
|
||||
namespace FlysystemOffload\Media;
|
||||
|
||||
use FlysystemOffload\Helpers\PathHelper;
|
||||
use League\Flysystem\FilesystemOperator;
|
||||
use WP_Error;
|
||||
use WP_Image_Editor_Imagick;
|
||||
|
||||
class ImageEditorImagick extends WP_Image_Editor_Imagick
|
||||
if (! defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
if (! class_exists(\WP_Image_Editor::class)) {
|
||||
require_once ABSPATH . WPINC . '/class-wp-image-editor.php';
|
||||
}
|
||||
|
||||
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_Imagick::class)) {
|
||||
// Si la clase base no está disponible (p. ej. Imagick deshabilitado),
|
||||
// salimos silenciosamente para permitir que WordPress siga usando GD.
|
||||
return;
|
||||
}
|
||||
|
||||
class ImageEditorImagick extends \WP_Image_Editor_Imagick
|
||||
{
|
||||
protected ?string $remoteFilename = null;
|
||||
protected array $tempFiles = [];
|
||||
protected static ?FilesystemOperator $filesystem = null;
|
||||
protected ?string $remotePath = null;
|
||||
protected ?string $localPath = null;
|
||||
|
||||
public static function bootWithFilesystem(?FilesystemOperator $filesystem): void
|
||||
{
|
||||
self::$filesystem = $filesystem;
|
||||
}
|
||||
|
||||
public function load()
|
||||
{
|
||||
if ($this->image instanceof \Imagick) {
|
||||
return true;
|
||||
if ($this->isFlyPath($this->file) && self::$filesystem) {
|
||||
$this->remotePath = PathHelper::stripProtocol($this->file);
|
||||
$temp = $this->downloadToTemp($this->remotePath);
|
||||
|
||||
if (is_wp_error($temp)) {
|
||||
return $temp;
|
||||
}
|
||||
|
||||
$this->localPath = $temp;
|
||||
$this->file = $temp;
|
||||
}
|
||||
|
||||
if (empty($this->file)) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_missing_file',
|
||||
__('Archivo no definido.', 'flysystem-offload')
|
||||
);
|
||||
return parent::load();
|
||||
}
|
||||
|
||||
public function save($filename = null, $mime_type = null)
|
||||
{
|
||||
$result = parent::save($filename, $mime_type);
|
||||
|
||||
if (is_wp_error($result) || ! $this->remotePath || ! self::$filesystem || empty($result['path'])) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (! $this->isFlysystemPath($this->file)) {
|
||||
return parent::load();
|
||||
$remote = $this->determineRemotePath($result['path']);
|
||||
$sync = $this->pushToRemote($result['path'], $remote);
|
||||
|
||||
if (is_wp_error($sync)) {
|
||||
return $sync;
|
||||
}
|
||||
|
||||
$localPath = $this->mirrorToLocal($this->file);
|
||||
|
||||
if (is_wp_error($localPath)) {
|
||||
return $localPath;
|
||||
$result['path'] = 'fly://' . $remote;
|
||||
if (isset($result['file'])) {
|
||||
$result['file'] = basename($remote);
|
||||
}
|
||||
|
||||
$this->remoteFilename = $this->file;
|
||||
$this->file = $localPath;
|
||||
|
||||
$result = parent::load();
|
||||
|
||||
$this->file = $this->remoteFilename;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function _save($image, $filename = null, $mime_type = null)
|
||||
public function multi_resize($sizes)
|
||||
{
|
||||
[$filename, $extension, $mime_type] = $this->get_output_format($filename, $mime_type);
|
||||
$results = parent::multi_resize($sizes);
|
||||
|
||||
if (! $filename) {
|
||||
$filename = $this->generate_filename(null, null, $extension);
|
||||
if (! $this->remotePath || ! self::$filesystem) {
|
||||
return $results;
|
||||
}
|
||||
|
||||
$isRemote = $this->isFlysystemPath($filename);
|
||||
$tempTarget = $isRemote ? $this->createTempFile($filename) : false;
|
||||
|
||||
$result = parent::_save($image, $tempTarget ?: $filename, $mime_type);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
if ($tempTarget) {
|
||||
@unlink($tempTarget);
|
||||
foreach ($results as &$result) {
|
||||
if (empty($result['path'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
$remote = $this->determineRemotePath($result['path']);
|
||||
$sync = $this->pushToRemote($result['path'], $remote);
|
||||
|
||||
if ($tempTarget) {
|
||||
$copy = copy($result['path'], $filename);
|
||||
|
||||
@unlink($result['path']);
|
||||
@unlink($tempTarget);
|
||||
|
||||
if (! $copy) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_copy_failed',
|
||||
__('No se pudo copiar la imagen procesada al almacenamiento remoto.', 'flysystem-offload')
|
||||
);
|
||||
if (is_wp_error($sync)) {
|
||||
$result['error'] = $sync->get_error_message();
|
||||
continue;
|
||||
}
|
||||
|
||||
$result['path'] = $filename;
|
||||
$result['file'] = wp_basename($filename);
|
||||
$result['path'] = 'fly://' . $remote;
|
||||
if (isset($result['file'])) {
|
||||
$result['file'] = basename($remote);
|
||||
}
|
||||
}
|
||||
unset($result);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function stream($mime_type = null)
|
||||
{
|
||||
if ($this->remotePath && $this->localPath) {
|
||||
$this->file = $this->localPath;
|
||||
}
|
||||
|
||||
return $result;
|
||||
return parent::stream($mime_type);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
foreach ($this->tempFiles as $temp) {
|
||||
@unlink($temp);
|
||||
if ($this->localPath && file_exists($this->localPath)) {
|
||||
@unlink($this->localPath);
|
||||
}
|
||||
|
||||
parent::__destruct();
|
||||
}
|
||||
|
||||
protected function mirrorToLocal(string $remotePath)
|
||||
protected function pushToRemote(string $localFile, string $remotePath)
|
||||
{
|
||||
$tempFile = $this->createTempFile($remotePath);
|
||||
$stream = @fopen($localFile, 'rb');
|
||||
|
||||
if (! $tempFile) {
|
||||
if (! $stream) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_temp_missing',
|
||||
__('No se pudo crear un archivo temporal para la imagen remota.', 'flysystem-offload')
|
||||
'flysystem_offload_upload_fail',
|
||||
__('No se pudo leer el archivo procesado para subirlo al almacenamiento remoto.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
if (! copy($remotePath, $tempFile)) {
|
||||
@unlink($tempFile);
|
||||
try {
|
||||
self::$filesystem->writeStream($remotePath, $stream);
|
||||
} catch (\Throwable $e) {
|
||||
if (is_resource($stream)) {
|
||||
fclose($stream);
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'flysystem_offload_remote_copy_failed',
|
||||
__('No se pudo copiar la imagen desde el almacenamiento remoto.', 'flysystem-offload')
|
||||
'flysystem_offload_upload_fail',
|
||||
sprintf(
|
||||
__('Fallo al subir %s al almacenamiento remoto: %s', 'flysystem-offload'),
|
||||
$remotePath,
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->tempFiles[] = $tempFile;
|
||||
if (is_resource($stream)) {
|
||||
fclose($stream);
|
||||
}
|
||||
|
||||
return $tempFile;
|
||||
@unlink($localFile);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function createTempFile(string $context)
|
||||
protected function downloadToTemp(string $remotePath)
|
||||
{
|
||||
if (! function_exists('wp_tempnam')) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
}
|
||||
|
||||
$tempFile = wp_tempnam(wp_basename($context));
|
||||
$temp = wp_tempnam(basename($remotePath));
|
||||
|
||||
return $tempFile ?: false;
|
||||
if (! $temp) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_temp_fail',
|
||||
__('No se pudo crear un archivo temporal para procesar la imagen.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$source = self::$filesystem->readStream($remotePath);
|
||||
if (! is_resource($source)) {
|
||||
throw new \RuntimeException('No se pudo abrir el stream remoto.');
|
||||
}
|
||||
|
||||
$target = fopen($temp, 'wb');
|
||||
if (! $target) {
|
||||
throw new \RuntimeException('No se pudo abrir el archivo temporal.');
|
||||
}
|
||||
|
||||
stream_copy_to_stream($source, $target);
|
||||
|
||||
fclose($source);
|
||||
fclose($target);
|
||||
} catch (\Throwable $e) {
|
||||
if (isset($source) && is_resource($source)) {
|
||||
fclose($source);
|
||||
}
|
||||
if (isset($target) && is_resource($target)) {
|
||||
fclose($target);
|
||||
}
|
||||
@unlink($temp);
|
||||
|
||||
return new WP_Error(
|
||||
'flysystem_offload_download_fail',
|
||||
sprintf(
|
||||
__('Fallo al descargar "%s" del almacenamiento remoto: %s', 'flysystem-offload'),
|
||||
$remotePath,
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $temp;
|
||||
}
|
||||
|
||||
protected function isFlysystemPath(string $path): bool
|
||||
protected function determineRemotePath(string $localSavedPath): string
|
||||
{
|
||||
return strpos($path, 'fly://') === 0;
|
||||
$remoteDir = $this->remotePath ? trim(dirname($this->remotePath), './') : '';
|
||||
$basename = basename($localSavedPath);
|
||||
|
||||
return ltrim(($remoteDir ? $remoteDir . '/' : '') . $basename, '/');
|
||||
}
|
||||
|
||||
protected function isFlyPath(string $path): bool
|
||||
{
|
||||
return strncmp($path, 'fly://', 6) === 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ use WP_Error;
|
|||
|
||||
class MediaHooks
|
||||
{
|
||||
private const CUSTOM_IMAGE_EDITOR = 'FlysystemOffload\\Media\\ImageEditorImagick';
|
||||
|
||||
private ?FilesystemOperator $filesystem = null;
|
||||
private bool $registered = false;
|
||||
private bool $metadataMirrorInProgress = false;
|
||||
|
|
@ -17,6 +19,10 @@ class MediaHooks
|
|||
public function setFilesystem(?FilesystemOperator $filesystem): void
|
||||
{
|
||||
$this->filesystem = $filesystem;
|
||||
|
||||
if (class_exists(self::CUSTOM_IMAGE_EDITOR)) {
|
||||
\call_user_func([self::CUSTOM_IMAGE_EDITOR, 'bootWithFilesystem'], $filesystem);
|
||||
}
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
|
|
@ -53,20 +59,21 @@ class MediaHooks
|
|||
|
||||
public function filterImageEditors(array $editors): array
|
||||
{
|
||||
if (! class_exists(self::CUSTOM_IMAGE_EDITOR)) {
|
||||
return $editors;
|
||||
}
|
||||
|
||||
$imagickIndex = array_search(\WP_Image_Editor_Imagick::class, $editors, true);
|
||||
|
||||
if ($imagickIndex !== false) {
|
||||
unset($editors[$imagickIndex]);
|
||||
}
|
||||
|
||||
array_unshift($editors, ImageEditorImagick::class);
|
||||
array_unshift($editors, self::CUSTOM_IMAGE_EDITOR);
|
||||
|
||||
return array_values(array_unique($editors));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sobreescribe el movimiento final del archivo para subirlo a fly:// mediante Flysystem.
|
||||
*/
|
||||
public function handlePreMoveUploadedFile($override, array $file, string $destination)
|
||||
{
|
||||
if ($override !== null) {
|
||||
|
|
@ -93,37 +100,26 @@ class MediaHooks
|
|||
);
|
||||
}
|
||||
|
||||
$directory = dirname($relativePath);
|
||||
|
||||
if ($directory !== '' && $directory !== '.') {
|
||||
try {
|
||||
$this->filesystem->createDirectory($directory);
|
||||
} catch (FilesystemException $e) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_directory_error',
|
||||
sprintf(
|
||||
__('No se pudo crear el directorio remoto "%s": %s', 'flysystem-offload'),
|
||||
esc_html($directory),
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$resource = @fopen($file['tmp_name'], 'rb');
|
||||
|
||||
if (! $resource) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_tmp_read_fail',
|
||||
__('No se pudo leer el archivo temporal subido.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
$directory = trim(dirname($relativePath), '.');
|
||||
|
||||
try {
|
||||
$this->filesystem->writeStream($relativePath, $resource);
|
||||
} catch (FilesystemException $e) {
|
||||
if (is_resource($resource)) {
|
||||
fclose($resource);
|
||||
if ($directory !== '') {
|
||||
$this->filesystem->createDirectory($directory);
|
||||
}
|
||||
|
||||
$stream = @fopen($file['tmp_name'], 'rb');
|
||||
|
||||
if (! $stream) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_tmp_read_fail',
|
||||
__('No se pudo leer el archivo temporal subido.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
$this->filesystem->writeStream($relativePath, $stream);
|
||||
} catch (\Throwable $e) {
|
||||
if (isset($stream) && is_resource($stream)) {
|
||||
fclose($stream);
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
|
|
@ -135,8 +131,8 @@ class MediaHooks
|
|||
);
|
||||
}
|
||||
|
||||
if (is_resource($resource)) {
|
||||
fclose($resource);
|
||||
if (isset($stream) && is_resource($stream)) {
|
||||
fclose($stream);
|
||||
}
|
||||
|
||||
@unlink($file['tmp_name']);
|
||||
|
|
@ -246,6 +242,10 @@ class MediaHooks
|
|||
|
||||
protected function mirrorToLocal(string $remotePath)
|
||||
{
|
||||
if (! $this->filesystem || ! $this->isFlyPath($remotePath)) {
|
||||
return $this->mirrorViaNativeCopy($remotePath);
|
||||
}
|
||||
|
||||
if (! function_exists('wp_tempnam')) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
}
|
||||
|
|
@ -259,21 +259,90 @@ class MediaHooks
|
|||
);
|
||||
}
|
||||
|
||||
if (! @copy($remotePath, $temp)) {
|
||||
$relative = $this->relativeFlyPath($remotePath);
|
||||
|
||||
if ($relative === null) {
|
||||
@unlink($temp);
|
||||
|
||||
return new WP_Error(
|
||||
'flysystem_offload_remote_copy_fail',
|
||||
__('No se pudo copiar la imagen desde el almacenamiento remoto.', 'flysystem-offload')
|
||||
__('No se pudo determinar la ruta remota del archivo.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$source = $this->filesystem->readStream($relative);
|
||||
|
||||
if (! is_resource($source)) {
|
||||
throw new \RuntimeException('No se pudo abrir el stream remoto.');
|
||||
}
|
||||
|
||||
$target = fopen($temp, 'wb');
|
||||
|
||||
if (! $target) {
|
||||
throw new \RuntimeException('No se pudo abrir el archivo temporal en disco.');
|
||||
}
|
||||
|
||||
stream_copy_to_stream($source, $target);
|
||||
} catch (\Throwable $e) {
|
||||
if (isset($source) && is_resource($source)) {
|
||||
fclose($source);
|
||||
}
|
||||
if (isset($target) && is_resource($target)) {
|
||||
fclose($target);
|
||||
}
|
||||
|
||||
@unlink($temp);
|
||||
|
||||
return new WP_Error(
|
||||
'flysystem_offload_remote_copy_fail',
|
||||
sprintf(
|
||||
__('No se pudo copiar la imagen desde el almacenamiento remoto: %s', 'flysystem-offload'),
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($source) && is_resource($source)) {
|
||||
fclose($source);
|
||||
}
|
||||
if (isset($target) && is_resource($target)) {
|
||||
fclose($target);
|
||||
}
|
||||
|
||||
return $temp;
|
||||
}
|
||||
|
||||
private function mirrorViaNativeCopy(string $remotePath)
|
||||
{
|
||||
if (! function_exists('wp_tempnam')) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
}
|
||||
|
||||
$temp = wp_tempnam(wp_basename($remotePath));
|
||||
|
||||
if (! $temp) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_temp_fail',
|
||||
__('No se pudo crear un archivo temporal para la lectura de metadatos.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
if (@copy($remotePath, $temp)) {
|
||||
return $temp;
|
||||
}
|
||||
|
||||
@unlink($temp);
|
||||
|
||||
return new WP_Error(
|
||||
'flysystem_offload_remote_copy_fail',
|
||||
__('No se pudo copiar la imagen desde el almacenamiento remoto.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
protected function isFlyPath(string $path): bool
|
||||
{
|
||||
return strpos($path, 'fly://') === 0;
|
||||
return strncmp($path, 'fly://', 6) === 0;
|
||||
}
|
||||
|
||||
protected function relativeFlyPath(string $path): ?string
|
||||
|
|
|
|||
|
|
@ -1,22 +1,38 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FlysystemOffload\StreamWrapper;
|
||||
|
||||
use League\Flysystem\FilesystemException;
|
||||
use League\Flysystem\FilesystemOperator;
|
||||
use League\Flysystem\StorageAttributes;
|
||||
use League\Flysystem\UnableToDeleteFile;
|
||||
use League\Flysystem\UnableToReadFile;
|
||||
use League\Flysystem\UnableToWriteFile;
|
||||
|
||||
class FlysystemStreamWrapper
|
||||
{
|
||||
/**
|
||||
* @var array<string, FilesystemOperator>
|
||||
*/
|
||||
private static array $filesystems = [];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private static array $prefixes = [];
|
||||
|
||||
private $stream;
|
||||
private string $protocol;
|
||||
private string $path;
|
||||
private string $mode;
|
||||
private string $flyPath;
|
||||
/** @var resource|null */
|
||||
private $stream = null;
|
||||
private string $protocol = '';
|
||||
private string $path = '';
|
||||
private string $mode = '';
|
||||
private string $flyPath = '';
|
||||
private array $dirEntries = [];
|
||||
private int $dirPosition = 0;
|
||||
|
||||
/** @var resource|null */
|
||||
public $context = null;
|
||||
|
||||
public static function register(FilesystemOperator $filesystem, string $protocol, string $prefix): void
|
||||
{
|
||||
|
|
@ -25,37 +41,50 @@ class FlysystemStreamWrapper
|
|||
}
|
||||
|
||||
self::$filesystems[$protocol] = $filesystem;
|
||||
self::$prefixes[$protocol] = $prefix;
|
||||
self::$prefixes[$protocol] = trim($prefix, '/');
|
||||
|
||||
stream_wrapper_register($protocol, static::class, STREAM_IS_URL);
|
||||
}
|
||||
|
||||
public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool
|
||||
{
|
||||
$this->protocol = strtok($path, ':');
|
||||
$this->path = $path;
|
||||
$this->mode = $mode;
|
||||
$this->flyPath = $this->resolveFlyPath($path);
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$this->protocol = $protocol;
|
||||
$this->path = $path;
|
||||
$this->mode = $mode;
|
||||
$this->flyPath = $this->resolveFlyPath($path, $protocol);
|
||||
|
||||
$filesystem = $this->filesystem();
|
||||
$filesystem = $this->filesystem($protocol);
|
||||
|
||||
if (strpbrk($mode, 'waxc')) {
|
||||
$this->stream = fopen('php://temp', str_contains($mode, 'b') ? 'w+b' : 'w+');
|
||||
|
||||
if (str_contains($mode, 'a') && $filesystem->fileExists($this->flyPath)) {
|
||||
$contents = $filesystem->readStream($this->flyPath);
|
||||
stream_copy_to_stream($contents, $this->stream);
|
||||
fclose($contents);
|
||||
try {
|
||||
$contents = $filesystem->readStream($this->flyPath);
|
||||
if (is_resource($contents)) {
|
||||
stream_copy_to_stream($contents, $this->stream);
|
||||
fclose($contents);
|
||||
}
|
||||
} catch (UnableToReadFile $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
rewind($this->stream);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$resource = $filesystem->readStream($this->flyPath);
|
||||
if (! is_resource($resource)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->stream = $resource;
|
||||
return is_resource($resource);
|
||||
|
||||
return true;
|
||||
} catch (UnableToReadFile $e) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -73,18 +102,21 @@ class FlysystemStreamWrapper
|
|||
|
||||
public function stream_flush(): bool
|
||||
{
|
||||
if (!strpbrk($this->mode, 'waxc')) {
|
||||
if (! strpbrk($this->mode, 'waxc')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$filesystem = $this->filesystem();
|
||||
$filesystem = $this->filesystem($this->protocol);
|
||||
|
||||
try {
|
||||
rewind($this->stream);
|
||||
$filesystem->writeStream($this->flyPath, $this->stream);
|
||||
rewind($this->stream);
|
||||
|
||||
return true;
|
||||
} catch (UnableToWriteFile $e) {
|
||||
error_log('[Flysystem Offload] Unable to flush stream: ' . $e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -96,6 +128,8 @@ class FlysystemStreamWrapper
|
|||
if (is_resource($this->stream)) {
|
||||
fclose($this->stream);
|
||||
}
|
||||
|
||||
$this->stream = null;
|
||||
}
|
||||
|
||||
public function stream_stat(): array|false
|
||||
|
|
@ -105,52 +139,71 @@ class FlysystemStreamWrapper
|
|||
|
||||
public function url_stat(string $path, int $flags): array|false
|
||||
{
|
||||
$filesystem = $this->filesystem();
|
||||
$flyPath = $this->resolveFlyPath($path);
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$filesystem = $this->filesystem($protocol);
|
||||
$flyPath = $this->resolveFlyPath($path, $protocol);
|
||||
|
||||
try {
|
||||
if (!$filesystem->fileExists($flyPath) && !$filesystem->directoryExists($flyPath)) {
|
||||
if (! $filesystem->fileExists($flyPath) && ! $filesystem->directoryExists($flyPath)) {
|
||||
if ($flags & STREAM_URL_STAT_QUIET) {
|
||||
return false;
|
||||
}
|
||||
|
||||
trigger_error("File or directory not found: {$path}", E_USER_WARNING);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$isDir = $filesystem->directoryExists($flyPath);
|
||||
$size = $isDir ? 0 : $filesystem->fileSize($flyPath);
|
||||
$size = $isDir ? 0 : $filesystem->fileSize($flyPath);
|
||||
$mtime = $filesystem->lastModified($flyPath);
|
||||
|
||||
return [
|
||||
'dev' => 0,
|
||||
'ino' => 0,
|
||||
'mode' => $isDir ? 0040777 : 0100777,
|
||||
'nlink' => 0,
|
||||
'uid' => 0,
|
||||
'gid' => 0,
|
||||
'rdev' => 0,
|
||||
'size' => $size,
|
||||
'atime' => $mtime,
|
||||
'mtime' => $mtime,
|
||||
'ctime' => $mtime,
|
||||
0 => 0,
|
||||
'dev' => 0,
|
||||
1 => 0,
|
||||
'ino' => 0,
|
||||
2 => $isDir ? 0040777 : 0100777,
|
||||
'mode' => $isDir ? 0040777 : 0100777,
|
||||
3 => 0,
|
||||
'nlink' => 0,
|
||||
4 => 0,
|
||||
'uid' => 0,
|
||||
5 => 0,
|
||||
'gid' => 0,
|
||||
6 => 0,
|
||||
'rdev' => 0,
|
||||
7 => $size,
|
||||
'size' => $size,
|
||||
8 => $mtime,
|
||||
'atime' => $mtime,
|
||||
9 => $mtime,
|
||||
'mtime' => $mtime,
|
||||
10 => $mtime,
|
||||
'ctime' => $mtime,
|
||||
11 => -1,
|
||||
'blksize' => -1,
|
||||
'blocks' => -1
|
||||
12 => -1,
|
||||
'blocks' => -1,
|
||||
];
|
||||
} catch (FilesystemException $e) {
|
||||
if (!($flags & STREAM_URL_STAT_QUIET)) {
|
||||
if (! ($flags & STREAM_URL_STAT_QUIET)) {
|
||||
trigger_error($e->getMessage(), E_USER_WARNING);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function unlink(string $path): bool
|
||||
{
|
||||
$filesystem = $this->filesystem();
|
||||
$flyPath = $this->resolveFlyPath($path);
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$filesystem = $this->filesystem($protocol);
|
||||
$flyPath = $this->resolveFlyPath($path, $protocol);
|
||||
|
||||
try {
|
||||
$filesystem->delete($flyPath);
|
||||
|
||||
return true;
|
||||
} catch (UnableToDeleteFile $e) {
|
||||
return false;
|
||||
|
|
@ -159,11 +212,13 @@ class FlysystemStreamWrapper
|
|||
|
||||
public function mkdir(string $path, int $mode, int $options): bool
|
||||
{
|
||||
$filesystem = $this->filesystem();
|
||||
$flyPath = $this->resolveFlyPath($path);
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$filesystem = $this->filesystem($protocol);
|
||||
$flyPath = $this->resolveFlyPath($path, $protocol);
|
||||
|
||||
try {
|
||||
$filesystem->createDirectory($flyPath);
|
||||
|
||||
return true;
|
||||
} catch (FilesystemException $e) {
|
||||
return false;
|
||||
|
|
@ -172,11 +227,13 @@ class FlysystemStreamWrapper
|
|||
|
||||
public function rmdir(string $path, int $options): bool
|
||||
{
|
||||
$filesystem = $this->filesystem();
|
||||
$flyPath = $this->resolveFlyPath($path);
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$filesystem = $this->filesystem($protocol);
|
||||
$flyPath = $this->resolveFlyPath($path, $protocol);
|
||||
|
||||
try {
|
||||
$filesystem->deleteDirectory($flyPath);
|
||||
|
||||
return true;
|
||||
} catch (FilesystemException $e) {
|
||||
return false;
|
||||
|
|
@ -185,71 +242,95 @@ class FlysystemStreamWrapper
|
|||
|
||||
public function dir_opendir(string $path, int $options): bool
|
||||
{
|
||||
$this->protocol = strtok($path, ':');
|
||||
$this->flyPath = $this->resolveFlyPath($path);
|
||||
$this->stream = $this->filesystem()->listContents($this->flyPath, false)->getIterator();
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$this->protocol = $protocol;
|
||||
$this->flyPath = $this->resolveFlyPath($path, $protocol);
|
||||
$filesystem = $this->filesystem($protocol);
|
||||
$this->dirEntries = [];
|
||||
|
||||
foreach ($filesystem->listContents($this->flyPath, false) as $item) {
|
||||
if ($item instanceof StorageAttributes) {
|
||||
$this->dirEntries[] = basename($item->path());
|
||||
} elseif (is_array($item) && isset($item['path'])) {
|
||||
$this->dirEntries[] = basename($item['path']);
|
||||
}
|
||||
}
|
||||
|
||||
$this->dirPosition = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function dir_readdir(): string|false
|
||||
{
|
||||
if ($this->stream instanceof \Iterator) {
|
||||
if ($this->stream->valid()) {
|
||||
$current = $this->stream->current();
|
||||
$this->stream->next();
|
||||
return $current['basename'] ?? $current['path'] ?? false;
|
||||
}
|
||||
if ($this->dirPosition >= count($this->dirEntries)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
return $this->dirEntries[$this->dirPosition++];
|
||||
}
|
||||
|
||||
public function dir_rewinddir(): bool
|
||||
{
|
||||
if ($this->stream instanceof \Iterator) {
|
||||
$this->stream->rewind();
|
||||
return true;
|
||||
}
|
||||
$this->dirPosition = 0;
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function dir_closedir(): bool
|
||||
{
|
||||
$this->stream = null;
|
||||
$this->dirEntries = [];
|
||||
$this->dirPosition = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rename(string $oldPath, string $newPath): bool
|
||||
{
|
||||
$filesystem = $this->filesystem();
|
||||
$from = $this->resolveFlyPath($oldPath);
|
||||
$to = $this->resolveFlyPath($newPath);
|
||||
$oldProtocol = $this->extractProtocol($oldPath);
|
||||
$newProtocol = $this->extractProtocol($newPath);
|
||||
|
||||
if ($oldProtocol !== $newProtocol) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$filesystem = $this->filesystem($oldProtocol);
|
||||
$from = $this->resolveFlyPath($oldPath, $oldProtocol);
|
||||
$to = $this->resolveFlyPath($newPath, $newProtocol);
|
||||
|
||||
try {
|
||||
$filesystem->move($from, $to);
|
||||
|
||||
return true;
|
||||
} catch (FilesystemException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function filesystem(): FilesystemOperator
|
||||
private function filesystem(string $protocol): FilesystemOperator
|
||||
{
|
||||
if (!isset(self::$filesystems[$this->protocol])) {
|
||||
throw new \RuntimeException('No filesystem registered for protocol: ' . $this->protocol);
|
||||
if (! isset(self::$filesystems[$protocol])) {
|
||||
throw new \RuntimeException('No filesystem registered for protocol: ' . $protocol);
|
||||
}
|
||||
|
||||
return self::$filesystems[$this->protocol];
|
||||
return self::$filesystems[$protocol];
|
||||
}
|
||||
|
||||
private function resolveFlyPath(string $path): string
|
||||
private function resolveFlyPath(string $path, ?string $protocol = null): string
|
||||
{
|
||||
$protocol = strtok($path, ':');
|
||||
$prefix = self::$prefixes[$protocol] ?? '';
|
||||
$raw = preg_replace('#^[^:]+://#', '', $path);
|
||||
$protocol ??= $this->extractProtocol($path);
|
||||
$prefix = self::$prefixes[$protocol] ?? '';
|
||||
$raw = preg_replace('#^[^:]+://#', '', $path) ?? '';
|
||||
|
||||
return ltrim($prefix . ltrim($raw, '/'), '/');
|
||||
$combined = $prefix !== '' ? $prefix . '/' . ltrim($raw, '/') : ltrim($raw, '/');
|
||||
|
||||
return ltrim($combined, '/');
|
||||
}
|
||||
|
||||
private function extractProtocol(string $path): string
|
||||
{
|
||||
$pos = strpos($path, '://');
|
||||
|
||||
return $pos === false ? $this->protocol : substr($path, 0, $pos);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue