"} so the frontend can * trigger the actual browser download. * * The frontend fires this POST on click, then uses the returned URL to * trigger the file download (or falls back to the pre-resolved URL it * already has). */ final class ArtworkDownloadController extends Controller { public function __construct(private readonly ArtworkStatsService $stats) {} public function __invoke(Request $request, int $id): JsonResponse { $artwork = Artwork::public() ->published() ->with(['user:id']) ->where('id', $id) ->first(); if (! $artwork) { return response()->json(['error' => 'Not found'], 404); } // Record the download event — non-blocking, errors are swallowed. $this->recordDownload($request, $artwork); // Increment counters — deferred via Redis when available. try { $this->stats->incrementDownloads((int) $artwork->id, 1, defer: true); } catch (\Throwable) { // Stats failure must never interrupt the download. } // Resolve the highest-resolution download URL available. $url = $this->resolveDownloadUrl($artwork); // Build a user-friendly download filename: "title-slug.file_ext" $ext = $artwork->file_ext ?: $artwork->thumb_ext ?: 'webp'; $slug = Str::slug((string) ($artwork->slug ?: $artwork->title)) ?: (string) $artwork->id; $filename = $slug . '.' . $ext; return response()->json([ 'ok' => true, 'url' => $url, 'filename' => $filename, 'size' => (int) ($artwork->file_size ?? 0), ]); } /** * Insert a row in artwork_downloads. * Uses a raw insert for the binary(16) IP column. * Silently ignores failures (analytics should never break user flow). */ private function recordDownload(Request $request, Artwork $artwork): void { try { $ip = $request->ip() ?? '0.0.0.0'; $bin = @inet_pton($ip); DB::table('artwork_downloads')->insert([ 'artwork_id' => $artwork->id, 'user_id' => $request->user()?->id, 'ip' => $bin !== false ? $bin : null, 'user_agent' => mb_substr((string) $request->userAgent(), 0, 512), 'created_at' => now(), ]); } catch (\Throwable) { // Analytics failure must never interrupt the download. } } /** * Resolve the original full-resolution CDN URL. * * Originals are stored at: {cdn}/original/{h1}/{h2}/{hash}.{file_ext} * h1 = first 2 chars of hash, h2 = next 2 chars, filename = full hash + file_ext. * Falls back to XL → LG → MD thumbnail when hash is unavailable. */ private function resolveDownloadUrl(Artwork $artwork): string { $hash = $artwork->hash ?? null; $ext = ltrim((string) ($artwork->file_ext ?: $artwork->thumb_ext ?: 'webp'), '.'); if (!empty($hash)) { $h = strtolower(preg_replace('/[^a-f0-9]/', '', $hash)); $h1 = substr($h, 0, 2); $h2 = substr($h, 2, 2); $cdn = rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/'); return sprintf('%s/original/%s/%s/%s.%s', $cdn, $h1, $h2, $h, $ext); } // Fallback: best available thumbnail size foreach (['xl', 'lg', 'md'] as $size) { $thumb = ThumbnailPresenter::present($artwork, $size); if (!empty($thumb['url'])) { return (string) $thumb['url']; } } return ''; } }