|
- import { NextResponse } from 'next/server';
- import fs from 'fs';
- import path from 'path';
-
- export const dynamic = 'force-dynamic';
-
- const MIME: Record<string, string> = {
- '.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',
- },
- });
- }
|