Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 

90 rader
2.7 KiB

  1. import { NextResponse } from 'next/server';
  2. import fs from 'fs';
  3. import path from 'path';
  4. export const dynamic = 'force-dynamic';
  5. const MIME: Record<string, string> = {
  6. '.png': 'image/png',
  7. '.jpg': 'image/jpeg',
  8. '.jpeg': 'image/jpeg',
  9. '.gif': 'image/gif',
  10. '.webp': 'image/webp',
  11. '.mp4': 'video/mp4',
  12. '.webm': 'video/webm',
  13. '.mov': 'video/quicktime',
  14. '.m4v': 'video/x-m4v',
  15. '.ogv': 'video/ogg',
  16. '.ogg': 'video/ogg',
  17. '.pdf': 'application/pdf',
  18. };
  19. export async function GET(request: Request) {
  20. const { searchParams } = new URL(request.url);
  21. const name = searchParams.get('name');
  22. if (!name) return new NextResponse('File name required', { status: 400 });
  23. // Strip any directory traversal attempt; we only ever serve from data/uploads/
  24. const safeName = path.basename(name);
  25. const filePath = path.join(process.cwd(), 'data', 'uploads', safeName);
  26. let stat: fs.Stats;
  27. try {
  28. stat = fs.statSync(filePath);
  29. } catch {
  30. return new NextResponse('File not found', { status: 404 });
  31. }
  32. const ext = path.extname(safeName).toLowerCase();
  33. const mimeType = MIME[ext] || 'application/octet-stream';
  34. const disposition = `inline; filename="${safeName.replace(/"/g, '')}"`;
  35. const fileSize = stat.size;
  36. // Handle Range requests (essential for video seeking)
  37. const range = request.headers.get('range');
  38. if (range) {
  39. const match = /bytes=(\d*)-(\d*)/.exec(range);
  40. if (match) {
  41. const start = match[1] ? parseInt(match[1], 10) : 0;
  42. const end = match[2] ? parseInt(match[2], 10) : fileSize - 1;
  43. const chunkSize = end - start + 1;
  44. const stream = fs.createReadStream(filePath, { start, end });
  45. // Convert Node stream to Web ReadableStream
  46. const webStream = new ReadableStream({
  47. start(controller) {
  48. stream.on('data', chunk => controller.enqueue(new Uint8Array(chunk as Buffer)));
  49. stream.on('end', () => controller.close());
  50. stream.on('error', err => controller.error(err));
  51. },
  52. cancel() {
  53. stream.destroy();
  54. },
  55. });
  56. return new NextResponse(webStream, {
  57. status: 206,
  58. headers: {
  59. 'Content-Type': mimeType,
  60. 'Content-Length': chunkSize.toString(),
  61. 'Content-Range': `bytes ${start}-${end}/${fileSize}`,
  62. 'Accept-Ranges': 'bytes',
  63. 'Cache-Control': 'public, max-age=86400',
  64. 'Content-Disposition': disposition,
  65. },
  66. });
  67. }
  68. }
  69. // Full file response (for images, or videos without Range header)
  70. const buffer = fs.readFileSync(filePath);
  71. return new NextResponse(buffer, {
  72. headers: {
  73. 'Content-Type': mimeType,
  74. 'Content-Length': fileSize.toString(),
  75. 'Accept-Ranges': 'bytes',
  76. 'Cache-Control': 'public, max-age=86400',
  77. },
  78. });
  79. }