flysystem-offload/src/Media/MediaHooks.php

260 lines
8.5 KiB
PHP

<?php
declare(strict_types=1);
namespace FlysystemOffload\Media;
use FlysystemOffload\Helpers\PathHelper;
use League\Flysystem\FilesystemOperator;
final class MediaHooks {
private FilesystemOperator $filesystem;
private array $config;
private string $protocol;
private string $streamHost;
private string $streamRootPrefix;
private string $s3Prefix;
private string $baseUrl;
private string $effectiveBaseUrl;
private string $remoteUrlPathPrefix;
private bool $deleteRemote;
private bool $preferLocal;
public function __construct(FilesystemOperator $filesystem, array $config) {
$this->filesystem = $filesystem;
$this->config = $config;
$this->protocol = (string) ($config['stream']['protocol'] ?? 'flysystem');
$this->streamRootPrefix = PathHelper::normalize((string) ($config['stream']['root_prefix'] ?? ''));
$this->streamHost = PathHelper::normalize((string) ($config['stream']['host'] ?? ''));
$this->s3Prefix = PathHelper::normalize((string) ($config['s3']['prefix'] ?? ''));
$this->baseUrl = $this->normaliseBaseUrl((string) ($config['uploads']['base_url'] ?? content_url('uploads')));
$this->remoteUrlPathPrefix = $this->buildRemoteUrlPathPrefix();
$this->effectiveBaseUrl = $this->calculateEffectiveBaseUrl($this->baseUrl, $this->remoteUrlPathPrefix);
$this->deleteRemote = (bool) ($config['uploads']['delete_remote'] ?? true);
$this->preferLocal = (bool) ($config['uploads']['prefer_local_for_missing'] ?? false);
error_log(sprintf(
'[MediaHooks] Initialized - protocol: %s, host: %s, root_prefix: %s, base_url: %s, effective_base_url: %s',
$this->protocol,
$this->streamHost,
$this->streamRootPrefix,
$this->baseUrl,
$this->effectiveBaseUrl
));
}
public function register(): void {
add_filter('upload_dir', [$this, 'filterUploadDir'], 20);
add_filter('pre_option_upload_path', '__return_false');
add_filter('pre_option_upload_url_path', '__return_false');
add_filter('wp_get_attachment_url', [$this, 'rewriteAttachmentUrl'], 9, 2);
add_filter('image_downsize', [$this, 'filterImageDownsize'], 10, 3);
add_action('delete_attachment', [$this, 'deleteRemoteFiles']);
}
public function filterUploadDir(array $uploads): array {
$subdir = $uploads['subdir'] ?? '';
$normalizedSubdir = $subdir !== '' ? PathHelper::normalize($subdir) : '';
$streamSubdir = $normalizedSubdir !== '' ? '/' . $normalizedSubdir : '';
$streamBase = sprintf('%s://%s', $this->protocol, $this->streamHost ?: 'default');
$uploads['path'] = $streamBase . $streamSubdir;
$uploads['basedir'] = $streamBase;
$uploads['baseurl'] = rtrim($this->effectiveBaseUrl, '/');
$uploads['url'] = $this->buildPublicUrl($normalizedSubdir);
$uploads['subdir'] = $normalizedSubdir !== '' ? '/' . $normalizedSubdir : '';
$uploads['error'] = false;
$uploads['flysystem_protocol'] = $this->protocol;
$uploads['flysystem_host'] = $this->streamHost;
$uploads['flysystem_root_prefix'] = $this->streamRootPrefix;
error_log(sprintf(
'[MediaHooks] Upload dir filtered - path: %s, url: %s',
$uploads['path'],
$uploads['url']
));
return $uploads;
}
public function rewriteAttachmentUrl(string $url, int $attachmentId): string {
$file = get_post_meta($attachmentId, '_wp_attached_file', true);
if (! $file) {
return $url;
}
$relativePath = PathHelper::normalize($file);
if ($relativePath === '') {
return $url;
}
if ($this->remoteUrlPathPrefix !== '') {
$prefixWithSlash = $this->remoteUrlPathPrefix . '/';
if (str_starts_with($relativePath, $prefixWithSlash)) {
$relativePath = substr($relativePath, strlen($prefixWithSlash));
} elseif ($relativePath === $this->remoteUrlPathPrefix) {
$relativePath = '';
}
}
$remoteUrl = $this->buildPublicUrl($relativePath);
if (! $this->preferLocal) {
return $remoteUrl;
}
try {
if ($this->filesystem->fileExists($this->toRemotePath($relativePath))) {
return $remoteUrl;
}
} catch (\Throwable $exception) {
error_log(sprintf(
'[Flysystem Offload] No se pudo verificar la existencia remota de "%s": %s',
$relativePath,
$exception->getMessage()
));
}
return $url;
}
public function filterImageDownsize(bool|array $out, int $attachmentId, array|string $size): bool|array {
return false;
}
public function deleteRemoteFiles(int $attachmentId): void {
if (! $this->deleteRemote) {
return;
}
$files = $this->gatherAttachmentFiles($attachmentId);
foreach ($files as $file) {
$key = $this->toRemotePath($file);
try {
if ($this->filesystem->fileExists($key)) {
$this->filesystem->delete($key);
}
} catch (\Throwable $exception) {
error_log(sprintf(
'[Flysystem Offload] No se pudo eliminar el archivo remoto "%s": %s',
$key,
$exception->getMessage()
));
}
}
}
/**
* @return list<string>
*/
private function gatherAttachmentFiles(int $attachmentId): array {
$files = [];
$attachedFile = get_post_meta($attachmentId, '_wp_attached_file', true);
if ($attachedFile) {
$files[] = $attachedFile;
}
$meta = wp_get_attachment_metadata($attachmentId);
if (is_array($meta)) {
if (! empty($meta['file'])) {
$files[] = $meta['file'];
}
if (! empty($meta['sizes']) && is_array($meta['sizes'])) {
$baseDir = $this->dirName($meta['file'] ?? '');
foreach ($meta['sizes'] as $sizeMeta) {
if (! empty($sizeMeta['file'])) {
$files[] = ($baseDir !== '' ? $baseDir . '/' : '') . $sizeMeta['file'];
}
}
}
}
$files = array_filter($files, static fn ($file) => is_string($file) && $file !== '');
return array_values(array_unique($files, SORT_STRING));
}
private function dirName(string $path): string {
$directory = dirname($path);
return $directory === '.' ? '' : $directory;
}
private function toRemotePath(string $file): string {
$segments = [];
// ✅ Solo agregar si NO están vacíos
if ($this->streamRootPrefix !== '') {
$segments[] = $this->streamRootPrefix;
}
if ($this->streamHost !== '') {
$segments[] = $this->streamHost;
}
$segments[] = $file;
$remotePath = PathHelper::join(...$segments);
error_log(sprintf(
'[MediaHooks] toRemotePath - file: %s, remote: %s',
$file,
$remotePath
));
return $remotePath;
}
private function normaliseBaseUrl(string $baseUrl): string {
$baseUrl = trim($baseUrl);
if ($baseUrl === '') {
$baseUrl = content_url('uploads');
}
return rtrim($baseUrl, '/');
}
private function buildRemoteUrlPathPrefix(): string {
$segments = array_filter(
[$this->s3Prefix, $this->streamRootPrefix, $this->streamHost],
static fn (string $segment): bool => $segment !== ''
);
return PathHelper::join(...$segments);
}
private function calculateEffectiveBaseUrl(string $baseUrl, string $pathPrefix): string {
$baseUrl = rtrim($baseUrl, '/');
if ($pathPrefix === '') {
return $baseUrl;
}
$basePath = trim((string) parse_url($baseUrl, PHP_URL_PATH), '/');
if ($basePath !== '' && str_ends_with($basePath, $pathPrefix)) {
return $baseUrl;
}
return $baseUrl;
}
private function buildPublicUrl(string $relativePath): string {
$base = rtrim($this->effectiveBaseUrl, '/');
if ($relativePath === '') {
return $base;
}
return $base . '/' . PathHelper::normalize($relativePath);
}
}