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', '.webp': 'image/webp', '.svg': 'image/svg+xml', '.mp4': 'video/mp4', '.webm': 'video/webm', '.mov': 'video/quicktime', '.m4v': 'video/x-m4v', '.ogv': 'video/ogg', '.ogg': 'video/ogg', '.pdf': 'application/pdf', }; 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 }); // Strip any directory traversal attempt; we only ever serve from data/uploads/ const safeName = path.basename(name); const filePath = path.join(process.cwd(), 'data', 'uploads', safeName); let stat: fs.Stats; try { stat = fs.statSync(filePath); } catch { return new NextResponse('File not found', { status: 404 }); } const ext = path.extname(safeName).toLowerCase(); const mimeType = MIME[ext] || 'application/octet-stream'; const disposition = `inline; filename="${safeName.replace(/"/g, '')}"`; const fileSize = stat.size; // Defense-in-depth per SVG: blocca qualunque script eventualmente sfuggito al // sanitizer al momento dell'upload, e impedisce caricamenti di risorse esterne. const extraHeaders: Record = ext === '.svg' ? { 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:" } : {}; // 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', 'Content-Disposition': disposition, }, }); } } // 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', ...extraHeaders, }, }); }