import { NextRequest, NextResponse } from 'next/server'; export const dynamic = 'force-dynamic'; function getCandidateUrls(filePath: string): string[] { const set = new Set(); if (process.env.INTERNAL_API_URL) { const base = process.env.INTERNAL_API_URL.replace(/\/api\/?$/, ''); set.add(`${base}/uploads/${filePath}`); } if (process.env.NEXT_PUBLIC_API_URL && !process.env.NEXT_PUBLIC_API_URL.startsWith('/')) { const base = process.env.NEXT_PUBLIC_API_URL.replace(/\/api\/?$/, ''); set.add(`${base}/uploads/${filePath}`); } // Production backend port and domains set.add(`http://127.0.0.1:4400/uploads/${filePath}`); set.add(`http://localhost:4400/uploads/${filePath}`); set.add(`http://127.0.0.1:4000/uploads/${filePath}`); set.add(`http://localhost:4000/uploads/${filePath}`); set.add(`https://api.canina.ir/uploads/${filePath}`); set.add(`http://backend_prod:3000/uploads/${filePath}`); set.add(`http://canino_backend_prod:3000/uploads/${filePath}`); set.add(`http://backend:3000/uploads/${filePath}`); set.add(`http://127.0.0.1:4001/uploads/${filePath}`); set.add(`http://localhost:4001/uploads/${filePath}`); return Array.from(set); } function getMimeType(filePath: string, fallback: string): string { const ext = filePath.split('.').pop()?.toLowerCase(); switch (ext) { case 'webp': return 'image/webp'; case 'jpg': case 'jpeg': return 'image/jpeg'; case 'png': return 'image/png'; case 'gif': return 'image/gif'; case 'svg': return 'image/svg+xml'; case 'avif': return 'image/avif'; case 'mp4': return 'video/mp4'; case 'webm': return 'video/webm'; case 'mp3': return 'audio/mpeg'; case 'pdf': return 'application/pdf'; default: return fallback || 'application/octet-stream'; } } export async function GET( request: NextRequest, { params }: { params: Promise<{ path: string[] }> } ) { try { const { path } = await params; if (!path || path.length === 0) { return new NextResponse('Bad Request: Missing file path', { status: 400 }); } const rawFilePath = path.join('/'); const filePath = path.map(segment => encodeURIComponent(segment)).join('/'); const candidates = getCandidateUrls(filePath); // Forward streaming/caching headers from client const forwardedHeaders: HeadersInit = {}; const rangeHeader = request.headers.get('range'); if (rangeHeader) forwardedHeaders['range'] = rangeHeader; const ifNoneMatch = request.headers.get('if-none-match'); if (ifNoneMatch) forwardedHeaders['if-none-match'] = ifNoneMatch; const ifModifiedSince = request.headers.get('if-modified-since'); if (ifModifiedSince) forwardedHeaders['if-modified-since'] = ifModifiedSince; let backendRes: Response | null = null; for (const targetUrl of candidates) { try { const res = await fetch(targetUrl, { method: 'GET', headers: forwardedHeaders, cache: 'no-store', }); if (res.ok || res.status === 304 || res.status === 206) { backendRes = res; break; } } catch { // Continue trying next candidate } } // If 304 received without body or if not found with conditional headers, retry unconditionally if (backendRes && backendRes.status === 304 && !backendRes.body) { for (const targetUrl of candidates) { try { const freshRes = await fetch(targetUrl, { method: 'GET', cache: 'no-store', }); if (freshRes.ok) { backendRes = freshRes; break; } } catch { // Continue } } } if (!backendRes) { return new NextResponse('Media file not found upstream', { status: 404 }); } const responseHeaders = new Headers(); const rawContentType = backendRes.headers.get('content-type') || ''; const contentType = getMimeType(rawFilePath, rawContentType); responseHeaders.set('Content-Type', contentType); const contentLength = backendRes.headers.get('content-length'); if (contentLength) responseHeaders.set('Content-Length', contentLength); const contentRange = backendRes.headers.get('content-range'); if (contentRange) responseHeaders.set('Content-Range', contentRange); const acceptRanges = backendRes.headers.get('accept-ranges') || 'bytes'; responseHeaders.set('Accept-Ranges', acceptRanges); const etag = backendRes.headers.get('etag'); if (etag) responseHeaders.set('ETag', etag); const lastModified = backendRes.headers.get('last-modified'); if (lastModified) responseHeaders.set('Last-Modified', lastModified); responseHeaders.set('Cache-Control', 'public, max-age=31536000, s-maxage=31536000, immutable'); responseHeaders.set('X-Content-Type-Options', 'nosniff'); return new Response(backendRes.body, { status: backendRes.status, statusText: backendRes.statusText, headers: responseHeaders, }); } catch (err: unknown) { console.error('[Media Proxy Error]', err); return new NextResponse('Internal Media Proxy Error', { status: 502 }); } } export async function HEAD( request: NextRequest, { params }: { params: Promise<{ path: string[] }> } ) { try { const { path } = await params; if (!path || path.length === 0) { return new NextResponse(null, { status: 400 }); } const filePath = path.map(segment => encodeURIComponent(segment)).join('/'); const candidates = getCandidateUrls(filePath); let backendRes: Response | null = null; for (const targetUrl of candidates) { try { const res = await fetch(targetUrl, { method: 'HEAD', cache: 'no-store' }); if (res.ok || res.status === 304) { backendRes = res; break; } } catch { // Continue } } if (!backendRes) { return new NextResponse(null, { status: 404 }); } const responseHeaders = new Headers(); const contentType = backendRes.headers.get('content-type') || 'application/octet-stream'; responseHeaders.set('Content-Type', contentType); const contentLength = backendRes.headers.get('content-length'); if (contentLength) responseHeaders.set('Content-Length', contentLength); responseHeaders.set('Accept-Ranges', 'bytes'); responseHeaders.set('Cache-Control', 'public, max-age=31536000, s-maxage=31536000, immutable'); return new NextResponse(null, { status: backendRes.status, headers: responseHeaders, }); } catch (err: unknown) { console.error('[Media Proxy HEAD Error]', err); return new NextResponse(null, { status: 502 }); } }