import { NextRequest, NextResponse } from 'next/server' import { readFile } from 'fs/promises' import { existsSync } from 'fs' import { join } from 'path' import { getManualsThumbnailsRoot } from '@/lib/manuals-paths' // API routes are not supported in static export (GHL hosting) // Thumbnails are now served as static files from /thumbnails/ export const dynamic = 'force-static' /** * API route to serve thumbnail images for PDF manuals * This allows serving files from outside the public folder * * Usage: /api/thumbnails/BevMax/manual.jpg * NOTE: This route is disabled for static export. Use /thumbnails/ paths instead. */ // Required for static export - returns empty array to skip this route export async function generateStaticParams(): Promise> { return [] } export async function GET( request: NextRequest, { params }: { params: Promise<{ path: string[] }> } ) { try { const { path: pathArray } = await params // Decode URL-encoded path segments - handle multiple encodings const decodedPath = pathArray.map(segment => { try { // Try decoding multiple times in case of double encoding let decoded = segment while (decoded !== decodeURIComponent(decoded)) { decoded = decodeURIComponent(decoded) } return decoded } catch { // If decoding fails, use original return segment } }) const filePath = decodedPath.join('/') // Security: Prevent directory traversal if (filePath.includes('..') || filePath.startsWith('/')) { return new NextResponse('Invalid path', { status: 400 }) } // Construct full path to thumbnail const thumbnailsDir = getManualsThumbnailsRoot() const fullPath = join(thumbnailsDir, filePath) // Normalize paths to handle both forward and backward slashes const normalizedFullPath = fullPath.replace(/\\/g, '/') const normalizedThumbnailsDir = thumbnailsDir.replace(/\\/g, '/') // Verify file exists and is within thumbnails directory let fileToRead = normalizedFullPath if (!existsSync(normalizedFullPath)) { // Try with original path in case of encoding issues if (existsSync(fullPath)) { fileToRead = fullPath } else { // Try with different path separators const altPath = fullPath.replace(/\//g, '\\') if (existsSync(altPath)) { fileToRead = altPath } else { // Thumbnail doesn't exist - return 404 (this is expected for manuals without thumbnails) return new NextResponse('Thumbnail not found', { status: 404 }) } } } // Verify the file is within the thumbnails directory (security check) const resolvedPath = fileToRead.replace(/\\/g, '/') if (!resolvedPath.startsWith(normalizedThumbnailsDir.replace(/\\/g, '/'))) { console.error(`Security violation: Path outside thumbnails directory: ${resolvedPath}`) return new NextResponse('Invalid path', { status: 400 }) } // Only allow image files (jpg, jpeg, png) const lowerPath = filePath.toLowerCase() if (!lowerPath.endsWith('.jpg') && !lowerPath.endsWith('.jpeg') && !lowerPath.endsWith('.png')) { return new NextResponse('Invalid file type', { status: 400 }) } // Read and serve the file const fileBuffer = await readFile(fileToRead) // Get filename for Content-Disposition header const filename = decodedPath[decodedPath.length - 1] // Determine content type const contentType = lowerPath.endsWith('.png') ? 'image/png' : 'image/jpeg' return new NextResponse(fileBuffer, { headers: { 'Content-Type': contentType, 'Content-Disposition': `inline; filename="${encodeURIComponent(filename)}"`, 'Cache-Control': 'public, max-age=31536000, immutable', 'X-Content-Type-Options': 'nosniff', }, }) } catch (error) { console.error('Error serving thumbnail:', error) if (error instanceof Error) { console.error('Error details:', error.message, error.stack) } return new NextResponse('Internal server error', { status: 500 }) } }