145 lines
4.5 KiB
PHP
145 lines
4.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Cdn;
|
|
|
|
use App\Services\ThumbnailService;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
final class ArtworkCdnPurgeService
|
|
{
|
|
/**
|
|
* @param array<int, string> $objectPaths
|
|
* @param array<string, mixed> $context
|
|
*/
|
|
public function purgeArtworkObjectPaths(array $objectPaths, array $context = []): bool
|
|
{
|
|
$urls = array_values(array_unique(array_filter(array_map(
|
|
fn (mixed $path): ?string => is_string($path) && trim($path) !== ''
|
|
? $this->cdnUrlForObjectPath($path)
|
|
: null,
|
|
$objectPaths,
|
|
))));
|
|
|
|
return $this->purgeUrls($urls, $context);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $variants
|
|
* @param array<string, mixed> $context
|
|
*/
|
|
public function purgeArtworkHashVariants(string $hash, string $extension = 'webp', array $variants = ['xs', 'sm', 'md', 'lg', 'xl', 'sq'], array $context = []): bool
|
|
{
|
|
$urls = array_values(array_unique(array_filter(array_map(
|
|
fn (string $variant): ?string => ThumbnailService::fromHash($hash, $extension, $variant),
|
|
$variants,
|
|
))));
|
|
|
|
return $this->purgeUrls($urls, $context + ['hash' => $hash]);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $urls
|
|
* @param array<string, mixed> $context
|
|
*/
|
|
private function purgeUrls(array $urls, array $context = []): bool
|
|
{
|
|
if ($urls === []) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->hasCloudflareCredentials()) {
|
|
return $this->purgeViaCloudflare($urls, $context);
|
|
}
|
|
|
|
$legacyPurgeUrl = trim((string) config('cdn.purge_url', ''));
|
|
if ($legacyPurgeUrl !== '') {
|
|
return $this->purgeViaLegacyWebhook($legacyPurgeUrl, $urls, $context);
|
|
}
|
|
|
|
Log::debug('CDN purge skipped - no Cloudflare or legacy purge configuration is available', $context + [
|
|
'url_count' => count($urls),
|
|
]);
|
|
|
|
return false;
|
|
}
|
|
|
|
private function purgeViaCloudflare(array $urls, array $context): bool
|
|
{
|
|
$purgeUrl = sprintf(
|
|
'https://api.cloudflare.com/client/v4/zones/%s/purge_cache',
|
|
trim((string) config('cdn.cloudflare.zone_id')),
|
|
);
|
|
|
|
try {
|
|
$response = Http::timeout(10)
|
|
->acceptJson()
|
|
->withToken(trim((string) config('cdn.cloudflare.api_token')))
|
|
->post($purgeUrl, ['files' => $urls]);
|
|
|
|
if ($response->successful()) {
|
|
return true;
|
|
}
|
|
|
|
Log::warning('Cloudflare artwork CDN purge failed', $context + [
|
|
'status' => $response->status(),
|
|
'body' => $response->body(),
|
|
'url_count' => count($urls),
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::warning('Cloudflare artwork CDN purge threw an exception', $context + [
|
|
'error' => $e->getMessage(),
|
|
'url_count' => count($urls),
|
|
]);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function purgeViaLegacyWebhook(string $purgeUrl, array $urls, array $context): bool
|
|
{
|
|
$paths = array_values(array_unique(array_filter(array_map(function (string $url): ?string {
|
|
$path = parse_url($url, PHP_URL_PATH);
|
|
|
|
return is_string($path) && $path !== '' ? $path : null;
|
|
}, $urls))));
|
|
|
|
if ($paths === []) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$response = Http::timeout(10)->acceptJson()->post($purgeUrl, ['paths' => $paths]);
|
|
|
|
if ($response->successful()) {
|
|
return true;
|
|
}
|
|
|
|
Log::warning('Legacy artwork CDN purge failed', $context + [
|
|
'status' => $response->status(),
|
|
'body' => $response->body(),
|
|
'path_count' => count($paths),
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::warning('Legacy artwork CDN purge threw an exception', $context + [
|
|
'error' => $e->getMessage(),
|
|
'path_count' => count($paths),
|
|
]);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function hasCloudflareCredentials(): bool
|
|
{
|
|
return trim((string) config('cdn.cloudflare.zone_id', '')) !== ''
|
|
&& trim((string) config('cdn.cloudflare.api_token', '')) !== '';
|
|
}
|
|
|
|
private function cdnUrlForObjectPath(string $objectPath): string
|
|
{
|
|
return rtrim((string) config('cdn.files_url', 'https://cdn.skinbase.org'), '/') . '/' . ltrim($objectPath, '/');
|
|
}
|
|
} |