197 lines
6.5 KiB
TypeScript
197 lines
6.5 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
||
import { writeFile, mkdir } from "fs/promises";
|
||
import { join } from "path";
|
||
import sharp from "sharp";
|
||
import { db } from "@/db";
|
||
import { images } from "@/db/schema";
|
||
import { nanoid } from "nanoid";
|
||
import { auth } from "@/app/lib/auth";
|
||
|
||
async function getUserId(request: NextRequest): Promise<string | null> {
|
||
try {
|
||
const session = await auth.api.getSession({
|
||
headers: request.headers,
|
||
});
|
||
return session?.user?.id || null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function getBaseUrl(request: NextRequest): string {
|
||
// First, check environment variables (production should set this)
|
||
if (process.env.NEXT_PUBLIC_APP_URL) {
|
||
return process.env.NEXT_PUBLIC_APP_URL;
|
||
}
|
||
if (process.env.APP_URL) {
|
||
return process.env.APP_URL;
|
||
}
|
||
|
||
// Check for reverse proxy headers (X-Forwarded-Host, X-Forwarded-Proto)
|
||
const forwardedHost = request.headers.get("x-forwarded-host");
|
||
const forwardedProto = request.headers.get("x-forwarded-proto");
|
||
|
||
if (forwardedHost && forwardedProto) {
|
||
return `${forwardedProto}://${forwardedHost}`;
|
||
}
|
||
|
||
// Fallback to request origin
|
||
return request.nextUrl.origin;
|
||
}
|
||
|
||
export async function POST(request: NextRequest) {
|
||
try {
|
||
const userId = await getUserId(request);
|
||
if (!userId) {
|
||
return NextResponse.json(
|
||
{ message: "Yetkisiz erişim" },
|
||
{ status: 401 }
|
||
);
|
||
}
|
||
|
||
const formData = await request.formData();
|
||
const file = formData.get("file") as File;
|
||
|
||
if (!file) {
|
||
return NextResponse.json(
|
||
{ message: "Dosya bulunamadı" },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
// File size validation (max 10MB)
|
||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||
if (file.size > MAX_FILE_SIZE) {
|
||
return NextResponse.json(
|
||
{ message: "Dosya boyutu çok büyük. Maksimum 10MB olmalıdır." },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
// File type validation
|
||
const allowedMimeTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp", "image/avif"];
|
||
if (!allowedMimeTypes.includes(file.type)) {
|
||
return NextResponse.json(
|
||
{ message: "Geçersiz dosya tipi. Sadece resim dosyaları kabul edilir." },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
// Input validation
|
||
const widthInput = formData.get("width") as string;
|
||
const heightInput = formData.get("height") as string;
|
||
const qualityInput = formData.get("quality") as string;
|
||
const formatInput = (formData.get("format") as string) || "jpeg";
|
||
|
||
const width = Math.max(1, Math.min(10000, parseInt(widthInput) || 800));
|
||
const height = Math.max(1, Math.min(10000, parseInt(heightInput) || 600));
|
||
const quality = Math.max(1, Math.min(100, parseInt(qualityInput) || 90));
|
||
const allowedFormats = ["jpeg", "jpg", "png", "webp", "avif"];
|
||
const format = allowedFormats.includes(formatInput) ? formatInput : "jpeg";
|
||
|
||
const bytes = await file.arrayBuffer();
|
||
const buffer = Buffer.from(bytes);
|
||
|
||
// Resim manipülasyonu - Tam istenen boyuta getir (crop ile, bozmadan)
|
||
// fit: "cover" kullanarak resmi tam boyuta getiriyoruz
|
||
// Aspect ratio korunur, fazla kısımlar ortadan kesilir (crop)
|
||
let processedBuffer = sharp(buffer).resize(width, height, {
|
||
fit: "cover", // Tam boyuta getir, aspect ratio koru, fazla kısımları kes
|
||
position: "center", // Ortadan crop yap
|
||
withoutEnlargement: false, // Gerekirse büyüt de tam boyuta getir
|
||
});
|
||
|
||
// Format ve kalite ayarları
|
||
const normalizedFormat = format === "jpg" ? "jpeg" : format;
|
||
if (normalizedFormat === "jpeg") {
|
||
processedBuffer = processedBuffer.jpeg({ quality });
|
||
} else if (normalizedFormat === "png") {
|
||
processedBuffer = processedBuffer.png({ quality });
|
||
} else if (normalizedFormat === "webp") {
|
||
processedBuffer = processedBuffer.webp({ quality });
|
||
} else if (normalizedFormat === "avif") {
|
||
processedBuffer = processedBuffer.avif({ quality });
|
||
}
|
||
|
||
const processedImage = await processedBuffer.toBuffer();
|
||
const metadata = await sharp(processedImage).metadata();
|
||
|
||
// Dosya adı oluştur
|
||
const fileId = nanoid();
|
||
const originalName = file.name;
|
||
const fileExtension = normalizedFormat === "jpeg" ? "jpg" : normalizedFormat;
|
||
const fileName = `${fileId}.${fileExtension}`;
|
||
|
||
// Uploads klasörünü oluştur
|
||
// Docker volume mount: /app/public/uploads
|
||
// Standalone build'de process.cwd() = /app olmalı
|
||
const uploadsDir = join(process.cwd(), "public", "uploads");
|
||
|
||
try {
|
||
await mkdir(uploadsDir, { recursive: true });
|
||
} catch (mkdirError: any) {
|
||
console.error("Uploads klasörü oluşturulamadı:", mkdirError);
|
||
console.error("Klasör yolu:", uploadsDir);
|
||
console.error("Current working directory:", process.cwd());
|
||
throw new Error(`Uploads klasörü oluşturulamadı: ${mkdirError.message}`);
|
||
}
|
||
|
||
// Dosyayı kaydet
|
||
const filePath = join(uploadsDir, fileName);
|
||
try {
|
||
await writeFile(filePath, processedImage);
|
||
} catch (writeError: any) {
|
||
console.error("Dosya yazılamadı:", writeError);
|
||
console.error("Dosya yolu:", filePath);
|
||
console.error("Dosya boyutu:", processedImage.length);
|
||
throw new Error(`Dosya yazılamadı: ${writeError.message}`);
|
||
}
|
||
|
||
// Veritabanına kaydet
|
||
const imageUrl = `/uploads/${fileName}`;
|
||
const imageId = nanoid();
|
||
|
||
await db.insert(images).values({
|
||
id: imageId,
|
||
userId,
|
||
originalName,
|
||
fileName,
|
||
filePath: filePath,
|
||
url: imageUrl,
|
||
width: metadata.width || null,
|
||
height: metadata.height || null,
|
||
quality,
|
||
format: normalizedFormat,
|
||
fileSize: processedImage.length,
|
||
});
|
||
|
||
// Get base URL
|
||
const baseUrl = getBaseUrl(request);
|
||
const fullImageUrl = `${baseUrl}${imageUrl}`;
|
||
|
||
return NextResponse.json({
|
||
message: "Resim başarıyla yüklendi",
|
||
image: {
|
||
id: imageId,
|
||
url: fullImageUrl,
|
||
width: metadata.width,
|
||
height: metadata.height,
|
||
},
|
||
});
|
||
} catch (error: any) {
|
||
console.error("Upload hatası:", error);
|
||
console.error("Error stack:", error?.stack);
|
||
console.error("Error message:", error?.message);
|
||
|
||
// Production'da detaylı hata mesajı döndür (debug için)
|
||
const errorMessage = process.env.NODE_ENV === "production"
|
||
? `Yükleme başarısız: ${error?.message || "Bilinmeyen hata"}`
|
||
: "Yükleme başarısız";
|
||
|
||
return NextResponse.json(
|
||
{ message: errorMessage },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
}
|