import { NextResponse } from 'next/server'; import fs from 'fs'; import path from 'path'; export const dynamic = 'force-dynamic'; const MIME: Record = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.mp4': 'video/mp4', '.webm': 'video/webm', '.mov': 'video/quicktime', '.m4v': 'video/x-m4v', '.ogv': 'video/ogg', }; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const name = searchParams.get('name'); if (!name) return new NextResponse('File name required', { status: 400 }); const filePath = path.join(process.cwd(), 'data', 'uploads', name); let stat: fs.Stats; try { stat = fs.statSync(filePath); } catch { return new NextResponse('File not found', { status: 404 }); } const ext = path.extname(name).toLowerCase(); const mimeType = MIME[ext] || 'application/octet-stream'; const fileSize = stat.size; // Handle Range requests (essential for video seeking) const range = request.headers.get('range'); if (range) { const match = /bytes=(\d*)-(\d*)/.exec(range); if (match) { const start = match[1] ? parseInt(match[1], 10) : 0; const end = match[2] ? parseInt(match[2], 10) : fileSize - 1; const chunkSize = end - start + 1; const stream = fs.createReadStream(filePath, { start, end }); // Convert Node stream to Web ReadableStream const webStream = new ReadableStream({ start(controller) { stream.on('data', chunk => controller.enqueue(new Uint8Array(chunk as Buffer))); stream.on('end', () => controller.close()); stream.on('error', err => controller.error(err)); }, cancel() { stream.destroy(); }, }); return new NextResponse(webStream, { status: 206, headers: { 'Content-Type': mimeType, 'Content-Length': chunkSize.toString(), 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Cache-Control': 'public, max-age=86400', }, }); } } // Full file response (for images, or videos without Range header) const buffer = fs.readFileSync(filePath); return new NextResponse(buffer, { headers: { 'Content-Type': mimeType, 'Content-Length': fileSize.toString(), 'Accept-Ranges': 'bytes', 'Cache-Control': 'public, max-age=86400', }, }); }