46 lines
1.4 KiB
TypeScript
46 lines
1.4 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import { readFile, stat } from 'node:fs/promises';
|
|
import { extname, join, normalize } from 'node:path';
|
|
import { UPLOADS_DIR } from '@/lib/db/store';
|
|
|
|
export const runtime = 'nodejs';
|
|
|
|
const MIME: Record<string, string> = {
|
|
'.png': 'image/png',
|
|
'.jpg': 'image/jpeg',
|
|
'.jpeg': 'image/jpeg',
|
|
'.webp': 'image/webp',
|
|
'.gif': 'image/gif',
|
|
'.svg': 'image/svg+xml',
|
|
'.avif': 'image/avif',
|
|
};
|
|
|
|
// Serves admin-uploaded media from the DATA_DIR volume. Public (not gated by
|
|
// middleware) so images render on the marketing site.
|
|
export async function GET(
|
|
_req: Request,
|
|
{ params }: { params: { path: string[] } },
|
|
) {
|
|
const rel = normalize(params.path.join('/'));
|
|
// Reject path traversal — the resolved file must stay inside UPLOADS_DIR.
|
|
if (rel.includes('..') || rel.startsWith('/') || rel.startsWith('\\')) {
|
|
return new NextResponse('bad path', { status: 400 });
|
|
}
|
|
|
|
const filePath = join(UPLOADS_DIR, rel);
|
|
try {
|
|
const info = await stat(filePath);
|
|
if (!info.isFile()) return new NextResponse('not found', { status: 404 });
|
|
const buf = await readFile(filePath);
|
|
const type = MIME[extname(filePath).toLowerCase()] ?? 'application/octet-stream';
|
|
return new NextResponse(buf, {
|
|
headers: {
|
|
'content-type': type,
|
|
'cache-control': 'public, max-age=31536000, immutable',
|
|
},
|
|
});
|
|
} catch {
|
|
return new NextResponse('not found', { status: 404 });
|
|
}
|
|
}
|