Files
soroushasadi/app/api/uploads/[...path]/route.ts
T
soroush.asadi add78d8460
ci / build (push) Failing after 23s
deploy / deploy (push) Failing after 10m12s
first commit
2026-05-31 12:47:02 +03:30

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 });
}
}