153 lines
5.0 KiB
TypeScript
153 lines
5.0 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
function getCandidateUrls(filePath: string): string[] {
|
|
const set = new Set<string>();
|
|
|
|
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}`);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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 filePath = path.map(segment => encodeURIComponent(segment)).join('/');
|
|
const candidates = getCandidateUrls(filePath);
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
if (!backendRes) {
|
|
return new NextResponse('Media file not found upstream', { 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);
|
|
|
|
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 });
|
|
}
|
|
}
|