2026-08-09 12:43:21 +03:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Jobs;
|
|
|
|
|
|
|
|
|
|
use App\Models\Tool;
|
2026-08-11 00:06:19 +03:00
|
|
|
use App\Support\FaviconResolver;
|
2026-08-09 12:43:21 +03:00
|
|
|
use Illuminate\Bus\Queueable;
|
|
|
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
|
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
2026-08-11 00:06:19 +03:00
|
|
|
use Illuminate\Http\Client\Response;
|
2026-08-09 12:43:21 +03:00
|
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
|
|
|
use Illuminate\Queue\SerializesModels;
|
|
|
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
|
use Illuminate\Support\Str;
|
|
|
|
|
|
|
|
|
|
class MirrorToolLogo implements ShouldQueue
|
|
|
|
|
{
|
|
|
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
|
|
2026-08-11 00:06:19 +03:00
|
|
|
public function __construct(public Tool $tool) {}
|
2026-08-09 12:43:21 +03:00
|
|
|
|
2026-08-11 00:06:19 +03:00
|
|
|
public function handle(FaviconResolver $resolver): void
|
2026-08-09 12:43:21 +03:00
|
|
|
{
|
2026-08-11 00:06:19 +03:00
|
|
|
$tool = $this->tool->fresh();
|
|
|
|
|
|
|
|
|
|
// The tool may have been deleted, or already mirrored by an earlier
|
|
|
|
|
// run of this same job, since this was queued.
|
|
|
|
|
if (! $tool || $tool->hasLocalLogo()) {
|
2026-08-09 12:43:21 +03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-11 00:06:19 +03:00
|
|
|
// logo_url is a hint, not a guarantee -- it's often a guessed
|
|
|
|
|
// `{domain}/favicon.ico` that 404s or isn't actually an image.
|
|
|
|
|
// Fall back to properly resolving the site's real favicon.
|
|
|
|
|
$response = ($tool->logo_url ? $this->fetchImage($tool->logo_url) : null)
|
|
|
|
|
?? $this->fetchImage($resolver->resolve($tool->url));
|
2026-08-09 12:43:21 +03:00
|
|
|
|
2026-08-11 00:06:19 +03:00
|
|
|
if (! $response) {
|
2026-08-09 12:43:21 +03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$extension = match ($response->header('Content-Type')) {
|
|
|
|
|
'image/png' => 'png',
|
|
|
|
|
'image/jpeg' => 'jpg',
|
|
|
|
|
'image/webp' => 'webp',
|
|
|
|
|
'image/x-icon', 'image/vnd.microsoft.icon' => 'ico',
|
2026-08-11 00:06:19 +03:00
|
|
|
default => 'png',
|
2026-08-09 12:43:21 +03:00
|
|
|
};
|
|
|
|
|
|
2026-08-11 00:06:19 +03:00
|
|
|
$path = "tools/{$tool->slug}-".Str::random(8).".{$extension}";
|
2026-08-09 12:43:21 +03:00
|
|
|
|
|
|
|
|
Storage::disk('public')->put($path, $response->body());
|
|
|
|
|
|
2026-08-11 00:06:19 +03:00
|
|
|
$tool->update(['logo_url' => Storage::disk('public')->url($path)]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function fetchImage(?string $url): ?Response
|
|
|
|
|
{
|
|
|
|
|
if (! $url) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$response = Http::timeout(10)->get($url);
|
|
|
|
|
} catch (\Throwable) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $response->successful() && str_starts_with((string) $response->header('Content-Type'), 'image/')
|
|
|
|
|
? $response
|
|
|
|
|
: null;
|
2026-08-09 12:43:21 +03:00
|
|
|
}
|
|
|
|
|
}
|