2.0.1
This commit is contained in:
parent
995e1dfd80
commit
3a316ce2cf
|
|
@ -1,21 +1,36 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FlysystemOffload\Filesystem\Adapters;
|
||||
|
||||
use Aws\S3\S3Client;
|
||||
use FlysystemOffload\Filesystem\AdapterInterface;
|
||||
use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
|
||||
use League\Flysystem\FilesystemAdapter;
|
||||
use League\Flysystem\PathPrefixing\PathPrefixedAdapter;
|
||||
use League\MimeTypeDetection\ExtensionMimeTypeDetector;
|
||||
use League\MimeTypeDetection\MimeTypeDetector;
|
||||
use WP_Error;
|
||||
|
||||
class S3Adapter implements AdapterInterface
|
||||
{
|
||||
private MimeTypeDetector $mimeTypeDetector;
|
||||
|
||||
public function __construct(?MimeTypeDetector $mimeTypeDetector = null)
|
||||
{
|
||||
$this->mimeTypeDetector = $mimeTypeDetector ?? new ExtensionMimeTypeDetector();
|
||||
}
|
||||
|
||||
public function validate(array $settings)
|
||||
{
|
||||
$required = ['access_key', 'secret_key', 'region', 'bucket'];
|
||||
|
||||
foreach ($required as $field) {
|
||||
if (empty($settings[$field])) {
|
||||
return new WP_Error('flysystem_offload_invalid_s3', sprintf(__('Campo S3 faltante: %s', 'flysystem-offload'), $field));
|
||||
return new WP_Error(
|
||||
'flysystem_offload_invalid_s3',
|
||||
sprintf(__('Campo S3 faltante: %s', 'flysystem-offload'), $field)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -27,21 +42,40 @@ class S3Adapter implements AdapterInterface
|
|||
try {
|
||||
$clientConfig = [
|
||||
'credentials' => [
|
||||
'key' => $settings['access_key'],
|
||||
'secret' => $settings['secret_key']
|
||||
'key' => $settings['access_key'],
|
||||
'secret' => $settings['secret_key'],
|
||||
],
|
||||
'region' => $settings['region'],
|
||||
'version' => 'latest'
|
||||
'region' => $settings['region'],
|
||||
'version' => 'latest',
|
||||
];
|
||||
|
||||
if (!empty($settings['endpoint'])) {
|
||||
$clientConfig['endpoint'] = $settings['endpoint'];
|
||||
$clientConfig['use_path_style_endpoint'] = true;
|
||||
if (! empty($settings['endpoint'])) {
|
||||
$clientConfig['endpoint'] = rtrim($settings['endpoint'], '/');
|
||||
$clientConfig['use_path_style_endpoint'] = (bool) ($settings['use_path_style_endpoint'] ?? true);
|
||||
}
|
||||
|
||||
if (! empty($settings['http_client'])) {
|
||||
$clientConfig['http_client'] = $settings['http_client'];
|
||||
}
|
||||
|
||||
$client = new S3Client($clientConfig);
|
||||
|
||||
return new AwsS3V3Adapter($client, $settings['bucket'], $settings['prefix'] ?? '');
|
||||
$adapter = new AwsS3V3Adapter(
|
||||
$client,
|
||||
$settings['bucket'],
|
||||
'',
|
||||
options: [],
|
||||
mimeTypeDetector: $this->mimeTypeDetector
|
||||
);
|
||||
|
||||
if (! empty($settings['prefix'])) {
|
||||
$adapter = new PathPrefixedAdapter(
|
||||
$adapter,
|
||||
trim($settings['prefix'], '/')
|
||||
);
|
||||
}
|
||||
|
||||
return $adapter;
|
||||
} catch (\Throwable $e) {
|
||||
return new WP_Error('flysystem_offload_s3_error', $e->getMessage());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ 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;
|
||||
|
|
|
|||
|
|
@ -1,60 +1,42 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FlysystemOffload\Helpers;
|
||||
|
||||
class PathHelper
|
||||
final class PathHelper
|
||||
{
|
||||
public static function normalizePrefix(string $prefix): string
|
||||
{
|
||||
$prefix = trim($prefix);
|
||||
$prefix = trim($prefix, '/');
|
||||
|
||||
return $prefix ? $prefix . '/' : '';
|
||||
}
|
||||
private function __construct() {}
|
||||
|
||||
public static function stripProtocol(string $path): string
|
||||
{
|
||||
$path = preg_replace('#^(fly://|https?://[^/]+/uploads/?)#', '', $path);
|
||||
return ltrim(preg_replace('#^[^:]+://#', '', $path) ?? '', '/');
|
||||
}
|
||||
|
||||
public static function trimLeadingSlash(string $path): string
|
||||
{
|
||||
return ltrim($path, '/');
|
||||
}
|
||||
|
||||
public static function ensureFlyProtocol(string $path): string
|
||||
public static function trimTrailingSlash(string $path): string
|
||||
{
|
||||
if (str_starts_with($path, 'fly://')) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
$relative = self::stripProtocol($path);
|
||||
|
||||
return 'fly://' . $relative;
|
||||
return rtrim($path, '/');
|
||||
}
|
||||
|
||||
public static function collectFilesFromAttachment(int $postId): array
|
||||
public static function trimSlashes(string $path): string
|
||||
{
|
||||
$files = [];
|
||||
return trim($path, '/');
|
||||
}
|
||||
|
||||
if ($mainFile = get_attached_file($postId, true)) {
|
||||
$files[] = self::stripProtocol($mainFile);
|
||||
}
|
||||
public static function ensureTrailingSlash(string $path): string
|
||||
{
|
||||
return rtrim($path, '/') . '/';
|
||||
}
|
||||
|
||||
$metadata = wp_get_attachment_metadata($postId) ?: [];
|
||||
$dir = isset($metadata['file']) ? dirname(self::stripProtocol($metadata['file'])) : '';
|
||||
public static function normalizeDirectory(string $path): string
|
||||
{
|
||||
$path = str_replace('\\', '/', $path);
|
||||
$path = preg_replace('#/{2,}#', '/', $path) ?? $path;
|
||||
|
||||
if (!empty($metadata['sizes'])) {
|
||||
foreach ($metadata['sizes'] as $size) {
|
||||
if (!empty($size['file'])) {
|
||||
$files[] = trailingslashit($dir) . ltrim($size['file'], '/');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$backupSizes = get_post_meta($postId, '_wp_attachment_backup_sizes', true) ?: [];
|
||||
foreach ($backupSizes as $size) {
|
||||
if (!empty($size['file'])) {
|
||||
$files[] = trailingslashit($dir) . ltrim($size['file'], '/');
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique(array_filter($files));
|
||||
return self::trimTrailingSlash($path);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ class ImageEditorGD extends \WP_Image_Editor_GD
|
|||
}
|
||||
|
||||
try {
|
||||
self::$filesystem->writeStream($remotePath, $stream);
|
||||
self::$filesystem?->writeStream($remotePath, $stream);
|
||||
} catch (\Throwable $e) {
|
||||
if (is_resource($stream)) {
|
||||
fclose($stream);
|
||||
|
|
@ -172,7 +172,7 @@ class ImageEditorGD extends \WP_Image_Editor_GD
|
|||
}
|
||||
|
||||
try {
|
||||
$source = self::$filesystem->readStream($remotePath);
|
||||
$source = self::$filesystem?->readStream($remotePath);
|
||||
if (! is_resource($source)) {
|
||||
throw new \RuntimeException('No se pudo abrir el stream remoto.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,17 +11,11 @@ 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;
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +23,7 @@ class ImageEditorImagick extends \WP_Image_Editor_Imagick
|
|||
{
|
||||
protected static ?FilesystemOperator $filesystem = null;
|
||||
protected ?string $remotePath = null;
|
||||
protected ?string $localPath = null;
|
||||
protected ?string $localPath = null;
|
||||
|
||||
public static function bootWithFilesystem(?FilesystemOperator $filesystem): void
|
||||
{
|
||||
|
|
@ -40,7 +34,7 @@ class ImageEditorImagick extends \WP_Image_Editor_Imagick
|
|||
{
|
||||
if ($this->isFlyPath($this->file) && self::$filesystem) {
|
||||
$this->remotePath = PathHelper::stripProtocol($this->file);
|
||||
$temp = $this->downloadToTemp($this->remotePath);
|
||||
$temp = $this->downloadToTemp($this->remotePath);
|
||||
|
||||
if (is_wp_error($temp)) {
|
||||
return $temp;
|
||||
|
|
@ -137,7 +131,7 @@ class ImageEditorImagick extends \WP_Image_Editor_Imagick
|
|||
}
|
||||
|
||||
try {
|
||||
self::$filesystem->writeStream($remotePath, $stream);
|
||||
self::$filesystem?->writeStream($remotePath, $stream);
|
||||
} catch (\Throwable $e) {
|
||||
if (is_resource($stream)) {
|
||||
fclose($stream);
|
||||
|
|
@ -178,7 +172,7 @@ class ImageEditorImagick extends \WP_Image_Editor_Imagick
|
|||
}
|
||||
|
||||
try {
|
||||
$source = self::$filesystem->readStream($remotePath);
|
||||
$source = self::$filesystem?->readStream($remotePath);
|
||||
if (! is_resource($source)) {
|
||||
throw new \RuntimeException('No se pudo abrir el stream remoto.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,354 +3,238 @@ declare(strict_types=1);
|
|||
|
||||
namespace FlysystemOffload\Media;
|
||||
|
||||
use League\Flysystem\FilesystemException;
|
||||
use FlysystemOffload\Helpers\PathHelper;
|
||||
use League\Flysystem\FilesystemOperator;
|
||||
use League\Flysystem\StorageAttributes;
|
||||
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;
|
||||
private string $basePrefix;
|
||||
|
||||
private const IMAGE_EDITOR_IMAGICK = 'FlysystemOffload\\Media\\ImageEditorImagick';
|
||||
private const IMAGE_EDITOR_GD = 'FlysystemOffload\\Media\\ImageEditorGD';
|
||||
|
||||
public function __construct(string $basePrefix = '')
|
||||
{
|
||||
$this->basePrefix = PathHelper::trimSlashes($basePrefix);
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
add_filter('upload_dir', [$this, 'filterUploadDir'], 20);
|
||||
add_filter('wp_get_attachment_url', [$this, 'filterAttachmentUrl'], 20, 2);
|
||||
add_filter('get_attached_file', [$this, 'filterGetAttachedFile'], 20, 2);
|
||||
add_filter('update_attached_file', [$this, 'filterUpdateAttachedFile'], 20, 2);
|
||||
add_filter('wp_read_image_metadata', [$this, 'ensureLocalPathForMetadata'], 5, 2);
|
||||
add_filter('image_editors', [$this, 'filterImageEditors'], 5);
|
||||
add_action('delete_attachment', [$this, 'handleDeleteAttachment'], 20);
|
||||
}
|
||||
|
||||
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);
|
||||
if (class_exists(self::IMAGE_EDITOR_IMAGICK)) {
|
||||
\call_user_func([self::IMAGE_EDITOR_IMAGICK, 'bootWithFilesystem'], $filesystem);
|
||||
}
|
||||
if (class_exists(self::IMAGE_EDITOR_GD)) {
|
||||
\call_user_func([self::IMAGE_EDITOR_GD, 'bootWithFilesystem'], $filesystem);
|
||||
}
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
public function filterUploadDir(array $uploadDir): array
|
||||
{
|
||||
if ($this->registered) {
|
||||
return;
|
||||
}
|
||||
$subdir = trim($uploadDir['subdir'] ?? '', '/');
|
||||
|
||||
add_filter('wp_image_editors', [$this, 'filterImageEditors'], 5);
|
||||
add_filter('pre_move_uploaded_file', [$this, 'handlePreMoveUploadedFile'], 10, 3);
|
||||
add_filter('wp_read_image_metadata', [$this, 'filterReadImageMetadata'], 10, 2);
|
||||
add_filter('wp_generate_attachment_metadata', [$this, 'filterGenerateAttachmentMetadata'], 10, 2);
|
||||
add_filter('pre_wp_unique_filename_file_list', [$this, 'filterUniqueFilenameFileList'], 10, 3);
|
||||
add_action('delete_attachment', [$this, 'handleDeleteAttachment'], 10);
|
||||
$remoteBase = $this->basePrefix !== '' ? $this->basePrefix . '/' : '';
|
||||
$remoteBase .= $subdir !== '' ? $subdir : '';
|
||||
|
||||
$this->registered = true;
|
||||
$remoteBase = trim($remoteBase, '/');
|
||||
|
||||
$uploadDir['path'] = $remoteBase !== '' ? 'fly://' . $remoteBase : 'fly://';
|
||||
$uploadDir['basedir'] = $uploadDir['path'];
|
||||
$uploadDir['subdir'] = $subdir !== '' ? '/' . $subdir : '';
|
||||
$uploadDir['url'] = $uploadDir['baseurl'] = $uploadDir['url']; // baseurl se sobrescribe en Plugin::filterUploadDir
|
||||
|
||||
return $uploadDir;
|
||||
}
|
||||
|
||||
public function unregister(): void
|
||||
public function filterAttachmentUrl(string $url, int $attachmentId): string
|
||||
{
|
||||
if (! $this->registered) {
|
||||
return;
|
||||
$file = get_post_meta($attachmentId, '_wp_attached_file', true);
|
||||
|
||||
if (empty($file)) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
remove_filter('wp_image_editors', [$this, 'filterImageEditors'], 5);
|
||||
remove_filter('pre_move_uploaded_file', [$this, 'handlePreMoveUploadedFile'], 10);
|
||||
remove_filter('wp_read_image_metadata', [$this, 'filterReadImageMetadata'], 10);
|
||||
remove_filter('wp_generate_attachment_metadata', [$this, 'filterGenerateAttachmentMetadata'], 10);
|
||||
remove_filter('pre_wp_unique_filename_file_list', [$this, 'filterUniqueFilenameFileList'], 10);
|
||||
remove_action('delete_attachment', [$this, 'handleDeleteAttachment']);
|
||||
$relative = PathHelper::trimLeadingSlash($file);
|
||||
|
||||
$this->registered = false;
|
||||
return trailingslashit($this->getBaseUrl()) . $relative;
|
||||
}
|
||||
|
||||
public function filterImageEditors(array $editors): array
|
||||
public function filterGetAttachedFile(string $file, int $attachmentId): string
|
||||
{
|
||||
if (! class_exists(self::CUSTOM_IMAGE_EDITOR)) {
|
||||
return $editors;
|
||||
$meta = wp_get_attachment_metadata($attachmentId);
|
||||
$relative = $meta['file'] ?? get_post_meta($attachmentId, '_wp_attached_file', true);
|
||||
|
||||
if (empty($relative)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
$imagickIndex = array_search(\WP_Image_Editor_Imagick::class, $editors, true);
|
||||
|
||||
if ($imagickIndex !== false) {
|
||||
unset($editors[$imagickIndex]);
|
||||
}
|
||||
|
||||
array_unshift($editors, self::CUSTOM_IMAGE_EDITOR);
|
||||
|
||||
return array_values(array_unique($editors));
|
||||
return 'fly://' . PathHelper::trimLeadingSlash($relative);
|
||||
}
|
||||
|
||||
public function handlePreMoveUploadedFile($override, array $file, string $destination)
|
||||
public function filterUpdateAttachedFile(string $file, int $attachmentId): string
|
||||
{
|
||||
if ($override !== null) {
|
||||
return $override;
|
||||
if (str_starts_with($file, 'fly://')) {
|
||||
update_post_meta($attachmentId, '_wp_attached_file', PathHelper::stripProtocol($file));
|
||||
} else {
|
||||
update_post_meta($attachmentId, '_wp_attached_file', PathHelper::trimLeadingSlash($file));
|
||||
}
|
||||
|
||||
if (! $this->isFlyPath($destination)) {
|
||||
return $override;
|
||||
}
|
||||
|
||||
if (! $this->filesystem) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_missing_filesystem',
|
||||
__('No se pudo acceder al filesystem remoto.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
$relativePath = $this->relativeFlyPath($destination);
|
||||
|
||||
if ($relativePath === null) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_invalid_destination',
|
||||
__('Ruta de destino inválida para el stream fly://.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
$directory = trim(dirname($relativePath), '.');
|
||||
|
||||
try {
|
||||
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(
|
||||
'flysystem_offload_write_fail',
|
||||
sprintf(
|
||||
__('No se pudo guardar el archivo en el almacenamiento remoto: %s', 'flysystem-offload'),
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($stream) && is_resource($stream)) {
|
||||
fclose($stream);
|
||||
}
|
||||
|
||||
@unlink($file['tmp_name']);
|
||||
|
||||
return true;
|
||||
return $file;
|
||||
}
|
||||
|
||||
public function filterReadImageMetadata($metadata, string $file)
|
||||
public function ensureLocalPathForMetadata($metadata, string $file)
|
||||
{
|
||||
if ($this->metadataMirrorInProgress || ! $this->isFlyPath($file)) {
|
||||
if (! str_starts_with($file, 'fly://') || ! $this->filesystem) {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
$this->metadataMirrorInProgress = true;
|
||||
|
||||
$temp = $this->mirrorToLocal($file);
|
||||
// Fuerza a WP a usar una copia temporal local durante la lectura de EXIF/IPTC
|
||||
$remotePath = PathHelper::stripProtocol($file);
|
||||
$temp = $this->downloadToTemp($remotePath);
|
||||
|
||||
if (! is_wp_error($temp)) {
|
||||
$metadata = wp_read_image_metadata($temp);
|
||||
@unlink($temp);
|
||||
}
|
||||
|
||||
$this->metadataMirrorInProgress = false;
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
public function filterGenerateAttachmentMetadata(array $metadata, int $attachmentId): array
|
||||
public function filterImageEditors(array $editors): array
|
||||
{
|
||||
if (isset($metadata['filesize'])) {
|
||||
return $metadata;
|
||||
if (class_exists(self::IMAGE_EDITOR_IMAGICK)) {
|
||||
$editors = array_filter(
|
||||
$editors,
|
||||
static fn (string $editor) => $editor !== \WP_Image_Editor_Imagick::class
|
||||
);
|
||||
array_unshift($editors, self::IMAGE_EDITOR_IMAGICK);
|
||||
}
|
||||
|
||||
$file = get_attached_file($attachmentId);
|
||||
|
||||
if ($file && file_exists($file)) {
|
||||
$metadata['filesize'] = filesize($file);
|
||||
if (class_exists(self::IMAGE_EDITOR_GD)) {
|
||||
$editors = array_filter(
|
||||
$editors,
|
||||
static fn (string $editor) => $editor !== \WP_Image_Editor_GD::class
|
||||
);
|
||||
array_unshift($editors, self::IMAGE_EDITOR_GD);
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
public function filterUniqueFilenameFileList($files, string $dir, string $filename)
|
||||
{
|
||||
if (! $this->isFlyPath($dir) || ! $this->filesystem) {
|
||||
return $files;
|
||||
}
|
||||
|
||||
$relativeDir = $this->relativeFlyPath($dir);
|
||||
|
||||
if ($relativeDir === null) {
|
||||
return $files;
|
||||
}
|
||||
|
||||
$existing = [];
|
||||
|
||||
foreach ($this->filesystem->listContents($relativeDir, false) as $item) {
|
||||
/** @var StorageAttributes $item */
|
||||
if ($item->isDir()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing[] = basename($item->path());
|
||||
}
|
||||
|
||||
return $existing;
|
||||
return array_values(array_unique($editors));
|
||||
}
|
||||
|
||||
public function handleDeleteAttachment(int $attachmentId): void
|
||||
{
|
||||
$file = get_attached_file($attachmentId);
|
||||
|
||||
if (! $file || ! $this->isFlyPath($file)) {
|
||||
if (! $this->filesystem) {
|
||||
return;
|
||||
}
|
||||
|
||||
$meta = wp_get_attachment_metadata($attachmentId);
|
||||
$meta = wp_get_attachment_metadata($attachmentId);
|
||||
$relative = $meta['file'] ?? get_post_meta($attachmentId, '_wp_attached_file', true);
|
||||
|
||||
if (empty($relative)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$base = PathHelper::trimLeadingSlash($relative);
|
||||
$directory = trim(dirname($base), './');
|
||||
|
||||
$targets = [$base];
|
||||
|
||||
if (! empty($meta['sizes'])) {
|
||||
foreach ($meta['sizes'] as $sizeInfo) {
|
||||
if (empty($sizeInfo['file'])) {
|
||||
continue;
|
||||
foreach ($meta['sizes'] as $size) {
|
||||
if (! empty($size['file'])) {
|
||||
$targets[] = ltrim(($directory ? $directory . '/' : '') . $size['file'], '/');
|
||||
}
|
||||
|
||||
wp_delete_file(str_replace(basename($file), $sizeInfo['file'], $file));
|
||||
}
|
||||
}
|
||||
|
||||
$original = get_post_meta($attachmentId, 'original_image', true);
|
||||
if ($original) {
|
||||
wp_delete_file(str_replace(basename($file), $original, $file));
|
||||
}
|
||||
|
||||
$backup = get_post_meta($attachmentId, '_wp_attachment_backup_sizes', true);
|
||||
if (is_array($backup)) {
|
||||
foreach ($backup as $sizeInfo) {
|
||||
if (empty($sizeInfo['file'])) {
|
||||
continue;
|
||||
foreach ($targets as $target) {
|
||||
try {
|
||||
if ($this->filesystem->fileExists($target)) {
|
||||
$this->filesystem->delete($target);
|
||||
}
|
||||
|
||||
wp_delete_file(str_replace(basename($file), $sizeInfo['file'], $file));
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[Flysystem Offload] Error eliminando ' . $target . ': ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
wp_delete_file($file);
|
||||
}
|
||||
|
||||
protected function mirrorToLocal(string $remotePath)
|
||||
private function downloadToTemp(string $remotePath)
|
||||
{
|
||||
if (! $this->filesystem || ! $this->isFlyPath($remotePath)) {
|
||||
return $this->mirrorViaNativeCopy($remotePath);
|
||||
if (! $this->filesystem) {
|
||||
return new WP_Error(
|
||||
'flysystem_offload_no_fs',
|
||||
__('No hay filesystem remoto configurado.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
if (! function_exists('wp_tempnam')) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
}
|
||||
|
||||
$temp = wp_tempnam(wp_basename($remotePath));
|
||||
$temp = wp_tempnam(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')
|
||||
);
|
||||
}
|
||||
|
||||
$relative = $this->relativeFlyPath($remotePath);
|
||||
|
||||
if ($relative === null) {
|
||||
@unlink($temp);
|
||||
|
||||
return new WP_Error(
|
||||
'flysystem_offload_remote_copy_fail',
|
||||
__('No se pudo determinar la ruta remota del archivo.', 'flysystem-offload')
|
||||
__('No se pudo crear un archivo temporal.', 'flysystem-offload')
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$source = $this->filesystem->readStream($relative);
|
||||
|
||||
if (! is_resource($source)) {
|
||||
$stream = $this->filesystem->readStream($remotePath);
|
||||
if (! is_resource($stream)) {
|
||||
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.');
|
||||
throw new \RuntimeException('No se pudo abrir el archivo temporal.');
|
||||
}
|
||||
|
||||
stream_copy_to_stream($source, $target);
|
||||
stream_copy_to_stream($stream, $target);
|
||||
|
||||
fclose($stream);
|
||||
fclose($target);
|
||||
} catch (\Throwable $e) {
|
||||
if (isset($source) && is_resource($source)) {
|
||||
fclose($source);
|
||||
if (isset($stream) && is_resource($stream)) {
|
||||
fclose($stream);
|
||||
}
|
||||
if (isset($target) && is_resource($target)) {
|
||||
fclose($target);
|
||||
}
|
||||
|
||||
@unlink($temp);
|
||||
|
||||
return new WP_Error(
|
||||
'flysystem_offload_remote_copy_fail',
|
||||
'flysystem_offload_download_fail',
|
||||
sprintf(
|
||||
__('No se pudo copiar la imagen desde el almacenamiento remoto: %s', 'flysystem-offload'),
|
||||
__('Fallo al descargar "%s" del almacenamiento remoto: %s', 'flysystem-offload'),
|
||||
$remotePath,
|
||||
$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)
|
||||
private function getBaseUrl(): string
|
||||
{
|
||||
if (! function_exists('wp_tempnam')) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
}
|
||||
$uploadDir = wp_get_upload_dir();
|
||||
|
||||
$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 strncmp($path, 'fly://', 6) === 0;
|
||||
}
|
||||
|
||||
protected function relativeFlyPath(string $path): ?string
|
||||
{
|
||||
if (! $this->isFlyPath($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ltrim(substr($path, 6), '/');
|
||||
return $uploadDir['baseurl'] ?? content_url('/uploads');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,18 +12,15 @@ use League\Flysystem\UnableToWriteFile;
|
|||
|
||||
class FlysystemStreamWrapper
|
||||
{
|
||||
/**
|
||||
* @var array<string, FilesystemOperator>
|
||||
*/
|
||||
/** @var array<string, FilesystemOperator> */
|
||||
private static array $filesystems = [];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
/** @var array<string, string> */
|
||||
private static array $prefixes = [];
|
||||
|
||||
/** @var resource|null */
|
||||
private $stream = null;
|
||||
|
||||
private string $protocol = '';
|
||||
private string $path = '';
|
||||
private string $mode = '';
|
||||
|
|
@ -31,10 +28,10 @@ class FlysystemStreamWrapper
|
|||
private array $dirEntries = [];
|
||||
private int $dirPosition = 0;
|
||||
|
||||
/** @var resource|null */
|
||||
/** @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 = ''): void
|
||||
{
|
||||
if (in_array($protocol, stream_get_wrappers(), true)) {
|
||||
stream_wrapper_unregister($protocol);
|
||||
|
|
@ -48,23 +45,25 @@ class FlysystemStreamWrapper
|
|||
|
||||
public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool
|
||||
{
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$this->protocol = $protocol;
|
||||
$this->path = $path;
|
||||
$this->mode = $mode;
|
||||
$this->flyPath = $this->resolveFlyPath($path, $protocol);
|
||||
$this->flyPath = $this->resolveFlyPath($path);
|
||||
|
||||
$filesystem = $this->filesystem($protocol);
|
||||
|
||||
$binary = str_contains($mode, 'b') ? 'b' : '';
|
||||
|
||||
if (strpbrk($mode, 'waxc')) {
|
||||
$this->stream = fopen('php://temp', str_contains($mode, 'b') ? 'w+b' : 'w+');
|
||||
$this->stream = fopen('php://temp', 'w+' . $binary);
|
||||
|
||||
if (str_contains($mode, 'a') && $filesystem->fileExists($this->flyPath)) {
|
||||
try {
|
||||
$contents = $filesystem->readStream($this->flyPath);
|
||||
if (is_resource($contents)) {
|
||||
stream_copy_to_stream($contents, $this->stream);
|
||||
fclose($contents);
|
||||
$remote = $filesystem->readStream($this->flyPath);
|
||||
if (is_resource($remote)) {
|
||||
stream_copy_to_stream($remote, $this->stream);
|
||||
fclose($remote);
|
||||
}
|
||||
} catch (UnableToReadFile $e) {
|
||||
return false;
|
||||
|
|
@ -77,12 +76,18 @@ class FlysystemStreamWrapper
|
|||
}
|
||||
|
||||
try {
|
||||
$resource = $filesystem->readStream($this->flyPath);
|
||||
if (! is_resource($resource)) {
|
||||
$remote = $filesystem->readStream($this->flyPath);
|
||||
if (! is_resource($remote)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->stream = $resource;
|
||||
$local = fopen('php://temp', 'w+' . $binary);
|
||||
stream_copy_to_stream($remote, $local);
|
||||
|
||||
fclose($remote);
|
||||
rewind($local);
|
||||
|
||||
$this->stream = $local;
|
||||
|
||||
return true;
|
||||
} catch (UnableToReadFile $e) {
|
||||
|
|
@ -109,9 +114,16 @@ class FlysystemStreamWrapper
|
|||
$filesystem = $this->filesystem($this->protocol);
|
||||
|
||||
try {
|
||||
rewind($this->stream);
|
||||
$meta = stream_get_meta_data($this->stream);
|
||||
if ($meta['seekable'] ?? false) {
|
||||
rewind($this->stream);
|
||||
}
|
||||
|
||||
$filesystem->writeStream($this->flyPath, $this->stream);
|
||||
rewind($this->stream);
|
||||
|
||||
if ($meta['seekable'] ?? false) {
|
||||
rewind($this->stream);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (UnableToWriteFile $e) {
|
||||
|
|
@ -132,6 +144,58 @@ class FlysystemStreamWrapper
|
|||
$this->stream = null;
|
||||
}
|
||||
|
||||
public function stream_tell(): int|false
|
||||
{
|
||||
return is_resource($this->stream) ? ftell($this->stream) : false;
|
||||
}
|
||||
|
||||
public function stream_seek(int $offset, int $whence = SEEK_SET): bool
|
||||
{
|
||||
if (! is_resource($this->stream)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return fseek($this->stream, $offset, $whence) === 0;
|
||||
}
|
||||
|
||||
public function stream_eof(): bool
|
||||
{
|
||||
return is_resource($this->stream) ? feof($this->stream) : true;
|
||||
}
|
||||
|
||||
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.
|
||||
return true;
|
||||
}
|
||||
|
||||
public function stream_cast(int $cast_as)
|
||||
{
|
||||
// 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)) {
|
||||
$meta = stream_get_meta_data($this->stream);
|
||||
if ($meta['seekable'] ?? false) {
|
||||
rewind($this->stream);
|
||||
}
|
||||
|
||||
return $this->stream;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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ó.
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_stat(): array|false
|
||||
{
|
||||
return $this->url_stat($this->path, 0);
|
||||
|
|
@ -139,12 +203,14 @@ class FlysystemStreamWrapper
|
|||
|
||||
public function url_stat(string $path, int $flags): array|false
|
||||
{
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$filesystem = $this->filesystem($protocol);
|
||||
$flyPath = $this->resolveFlyPath($path, $protocol);
|
||||
$flyPath = $this->resolveFlyPath($path);
|
||||
|
||||
try {
|
||||
if (! $filesystem->fileExists($flyPath) && ! $filesystem->directoryExists($flyPath)) {
|
||||
$exists = $filesystem->fileExists($flyPath) || $filesystem->directoryExists($flyPath);
|
||||
|
||||
if (! $exists) {
|
||||
if ($flags & STREAM_URL_STAT_QUIET) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -197,9 +263,9 @@ class FlysystemStreamWrapper
|
|||
|
||||
public function unlink(string $path): bool
|
||||
{
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$filesystem = $this->filesystem($protocol);
|
||||
$flyPath = $this->resolveFlyPath($path, $protocol);
|
||||
$flyPath = $this->resolveFlyPath($path);
|
||||
|
||||
try {
|
||||
$filesystem->delete($flyPath);
|
||||
|
|
@ -212,9 +278,9 @@ class FlysystemStreamWrapper
|
|||
|
||||
public function mkdir(string $path, int $mode, int $options): bool
|
||||
{
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$filesystem = $this->filesystem($protocol);
|
||||
$flyPath = $this->resolveFlyPath($path, $protocol);
|
||||
$flyPath = $this->resolveFlyPath($path);
|
||||
|
||||
try {
|
||||
$filesystem->createDirectory($flyPath);
|
||||
|
|
@ -227,9 +293,9 @@ class FlysystemStreamWrapper
|
|||
|
||||
public function rmdir(string $path, int $options): bool
|
||||
{
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$filesystem = $this->filesystem($protocol);
|
||||
$flyPath = $this->resolveFlyPath($path, $protocol);
|
||||
$flyPath = $this->resolveFlyPath($path);
|
||||
|
||||
try {
|
||||
$filesystem->deleteDirectory($flyPath);
|
||||
|
|
@ -240,13 +306,35 @@ class FlysystemStreamWrapper
|
|||
}
|
||||
}
|
||||
|
||||
public function rename(string $oldPath, string $newPath): bool
|
||||
{
|
||||
$oldProtocol = $this->extractProtocol($oldPath);
|
||||
$newProtocol = $this->extractProtocol($newPath);
|
||||
|
||||
if ($oldProtocol !== $newProtocol) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$filesystem = $this->filesystem($oldProtocol);
|
||||
$from = $this->resolveFlyPath($oldPath);
|
||||
$to = $this->resolveFlyPath($newPath);
|
||||
|
||||
try {
|
||||
$filesystem->move($from, $to);
|
||||
|
||||
return true;
|
||||
} catch (FilesystemException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function dir_opendir(string $path, int $options): bool
|
||||
{
|
||||
$protocol = $this->extractProtocol($path);
|
||||
$this->protocol = $protocol;
|
||||
$this->flyPath = $this->resolveFlyPath($path, $protocol);
|
||||
$this->flyPath = $this->resolveFlyPath($path);
|
||||
$filesystem = $this->filesystem($protocol);
|
||||
$this->dirEntries = [];
|
||||
$this->dirEntries = ['.', '..'];
|
||||
|
||||
foreach ($filesystem->listContents($this->flyPath, false) as $item) {
|
||||
if ($item instanceof StorageAttributes) {
|
||||
|
|
@ -285,28 +373,6 @@ class FlysystemStreamWrapper
|
|||
return true;
|
||||
}
|
||||
|
||||
public function rename(string $oldPath, string $newPath): bool
|
||||
{
|
||||
$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(string $protocol): FilesystemOperator
|
||||
{
|
||||
if (! isset(self::$filesystems[$protocol])) {
|
||||
|
|
@ -316,24 +382,12 @@ class FlysystemStreamWrapper
|
|||
return self::$filesystems[$protocol];
|
||||
}
|
||||
|
||||
private function resolveFlyPath(string $path, ?string $protocol = null): string
|
||||
{
|
||||
$protocol ??= $this->extractProtocol($path);
|
||||
$prefix = self::$prefixes[$protocol] ?? '';
|
||||
$raw = preg_replace('#^[^:]+://#', '', $path) ?? '';
|
||||
|
||||
$normalized = ltrim($raw, '/');
|
||||
|
||||
if ($prefix !== '' && str_starts_with($normalized, $prefix . '/')) {
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
if ($prefix === '') {
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
return $prefix . '/' . $normalized;
|
||||
}
|
||||
private function resolveFlyPath(string $path): string
|
||||
{
|
||||
$raw = preg_replace('#^[^:]+://#', '', $path) ?? '';
|
||||
|
||||
return ltrim($raw, '/');
|
||||
}
|
||||
|
||||
private function extractProtocol(string $path): string
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue