NeahStable/app/api/missions/image/[...path]/route.ts
2026-01-21 12:13:01 +01:00

126 lines
4.5 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/options';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { NoSuchKey } from '@aws-sdk/client-s3';
import { logger } from '@/lib/logger';
// S3 Configuration for missions - uses environment variables
const MISSIONS_S3_CONFIG = {
endpoint: (process.env.MINIO_S3_UPLOAD_BUCKET_URL || process.env.S3_ENDPOINT || 'https://dome-api.slm-lab.net').replace(/\/$/, ''),
region: process.env.MINIO_AWS_REGION || process.env.S3_REGION || 'us-east-1',
accessKey: process.env.MINIO_ACCESS_KEY || process.env.S3_ACCESS_KEY || '',
secretKey: process.env.MINIO_SECRET_KEY || process.env.S3_SECRET_KEY || '',
bucket: 'missions' // Missions bucket is always 'missions'
};
// Validate required S3 configuration
if (!MISSIONS_S3_CONFIG.accessKey || !MISSIONS_S3_CONFIG.secretKey) {
const errorMsg = '⚠️ S3 credentials are missing! Please set MINIO_ACCESS_KEY and MINIO_SECRET_KEY environment variables.';
console.error(errorMsg);
if (process.env.NODE_ENV === 'production') {
throw new Error('S3 credentials are required in production environment');
}
}
// Initialize S3 client
const s3Client = new S3Client({
region: MISSIONS_S3_CONFIG.region,
endpoint: MISSIONS_S3_CONFIG.endpoint,
credentials: {
accessKeyId: MISSIONS_S3_CONFIG.accessKey,
secretAccessKey: MISSIONS_S3_CONFIG.secretKey
},
forcePathStyle: true // Required for MinIO
});
// This endpoint serves mission images from Minio using the server's credentials
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
try {
const { path: pathSegments } = await params;
if (!pathSegments || pathSegments.length === 0) {
logger.error('No path segments provided');
return new NextResponse('Path is required', { status: 400 });
}
// Reconstruct the full path from path segments
const filePath = pathSegments.join('/');
logger.debug('Fetching mission image', {
originalPath: filePath,
segments: pathSegments
});
// Remove the missions/ prefix from the URL path since the file is already in the missions bucket
const minioPath = filePath.replace(/^missions\//, '');
logger.debug('Full Minio path', {
minioPath,
bucket: 'missions'
});
const command = new GetObjectCommand({
Bucket: MISSIONS_S3_CONFIG.bucket,
Key: minioPath,
});
try {
const response = await s3Client.send(command);
if (!response.Body) {
logger.error('File not found in Minio', {
path: filePath,
minioPath,
bucket: 'missions'
});
return new NextResponse('File not found', { status: 404 });
}
// Set appropriate content type and cache control
const contentType = response.ContentType || 'image/png'; // Default to image/png if not specified
const headers = new Headers();
headers.set('Content-Type', contentType);
headers.set('Cache-Control', 'public, max-age=31536000');
logger.debug('Serving image', {
path: filePath,
minioPath,
contentType,
contentLength: response.ContentLength
});
return new NextResponse(response.Body as any, { headers });
} catch (error) {
// Check if it's a NoSuchKey error (file doesn't exist)
const isNoSuchKey = error instanceof NoSuchKey ||
(error instanceof Error && error.name === 'NoSuchKey') ||
(error instanceof Error && error.message.includes('does not exist'));
logger.error('Error fetching file from Minio', {
error: error instanceof Error ? error.message : String(error),
path: filePath,
minioPath,
bucket: MISSIONS_S3_CONFIG.bucket,
errorType: isNoSuchKey ? 'NoSuchKey' : 'Unknown',
errorName: error instanceof Error ? error.name : typeof error
});
if (isNoSuchKey) {
// Return 404 with proper headers for image requests
return new NextResponse('File not found', {
status: 404,
headers: {
'Content-Type': 'text/plain',
'Cache-Control': 'no-cache'
}
});
}
return new NextResponse('Internal Server Error', { status: 500 });
}
} catch (error) {
logger.error('Error in image serving', {
error: error instanceof Error ? error.message : String(error)
});
return new NextResponse('Internal Server Error', { status: 500 });
}
}