Created
May 19, 2025 15:06
-
-
Save anatooly/d3678da0b75897dbb251531e269a39c0 to your computer and use it in GitHub Desktop.
/api/image/route.ts - next.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { NextRequest, NextResponse } from "next/server"; | |
| import path from "path"; | |
| import fs from "fs/promises"; | |
| import sharp from "sharp"; | |
| const SUPPORTED_FORMATS = ["avif", "webp", "jpeg", "png"] as const; | |
| type Format = (typeof SUPPORTED_FORMATS)[number]; | |
| const PUBLIC_ROOT = path.join(process.cwd(), "public"); | |
| const CACHE_ROOT = path.join(process.cwd(), ".next/cache/images"); | |
| function sanitizeFileName(str: string) { | |
| return str.replace(/[^a-z0-9_\-\.]/gi, "_").toLowerCase(); | |
| } | |
| export async function GET(request: NextRequest) { | |
| try { | |
| const { searchParams } = new URL(request.url); | |
| const src = searchParams.get("src"); | |
| const widthParam = searchParams.get("width"); | |
| const formatParam = searchParams.get("format"); | |
| if (!src) { | |
| return NextResponse.json({ error: 'Missing "src" parameter' }, { status: 400 }); | |
| } | |
| const outputFormat: Format = SUPPORTED_FORMATS.includes(formatParam as Format) ? (formatParam as Format) : "webp"; | |
| const width = widthParam ? parseInt(widthParam, 10) : undefined; | |
| const normalizedSrc = path.posix.normalize(src).replace(/^(\.\.[/\\])+/, ""); | |
| const absolutePath = path.join(PUBLIC_ROOT, normalizedSrc); | |
| if (!absolutePath.startsWith(PUBLIC_ROOT)) { | |
| return NextResponse.json({ error: "Access denied" }, { status: 403 }); | |
| } | |
| const ext = path.extname(absolutePath).toLowerCase(); | |
| if (![".png", ".jpg", ".jpeg", ".webp"].includes(ext)) { | |
| return NextResponse.json({ error: "Unsupported input image format" }, { status: 400 }); | |
| } | |
| const baseName = path.basename(absolutePath); | |
| if (baseName.startsWith(".") || baseName.includes("..")) { | |
| return NextResponse.json({ error: "Forbidden file access" }, { status: 403 }); | |
| } | |
| const cacheFileName = | |
| sanitizeFileName(`${normalizedSrc.replace(/\//g, "_")}_w${width || "auto"}_f${outputFormat}`) + "." + outputFormat; | |
| const cacheFilePath = path.join(CACHE_ROOT, cacheFileName); | |
| try { | |
| const cachedBuffer = await fs.readFile(cacheFilePath); | |
| return new NextResponse(cachedBuffer, { | |
| status: 200, | |
| headers: { | |
| "Content-Type": `image/${outputFormat}`, | |
| "Cache-Control": "public, max-age=31536000, immutable", | |
| "X-Cache": "HIT", | |
| }, | |
| }); | |
| } catch {} | |
| const imageBuffer = await fs.readFile(absolutePath); | |
| const resizedBuffer = await sharp(imageBuffer).resize(width).toFormat(outputFormat).toBuffer(); | |
| await fs.mkdir(CACHE_ROOT, { recursive: true }); | |
| await fs.writeFile(cacheFilePath, resizedBuffer); | |
| return new NextResponse(resizedBuffer, { | |
| status: 200, | |
| headers: { | |
| "Content-Type": `image/${outputFormat}`, | |
| "Cache-Control": "public, max-age=31536000, immutable", | |
| "X-Cache": "MISS", | |
| }, | |
| }); | |
| } catch (error) { | |
| console.error("Image processing error:", error); | |
| return NextResponse.json({ error: "Image processing failed" }, { status: 500 }); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment