139 lines
3.6 KiB
TypeScript
139 lines
3.6 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { readFile, readdir } from "fs/promises";
|
|
import path from "path";
|
|
import type { ImageCompressConfig } from "@/types";
|
|
import {
|
|
saveProcessedFile,
|
|
cleanupFile,
|
|
sanitizeFilename,
|
|
} from "@/lib/file-storage";
|
|
import {
|
|
compressImage,
|
|
getImageMetadata,
|
|
validateImageBuffer,
|
|
validateCompressConfig,
|
|
} from "@/lib/image-processor";
|
|
|
|
export const runtime = "nodejs";
|
|
|
|
const UPLOAD_DIR = process.env.TEMP_DIR || path.join(process.cwd(), ".temp", "uploads");
|
|
|
|
interface ProcessRequest {
|
|
fileId: string;
|
|
config: ImageCompressConfig;
|
|
}
|
|
|
|
/**
|
|
* Find file by ID in upload directory
|
|
*/
|
|
async function findUploadedFile(fileId: string): Promise<{ buffer: Buffer; name: string } | null> {
|
|
try {
|
|
const files = await readdir(UPLOAD_DIR);
|
|
const file = files.find((f) => f.startsWith(`${fileId}.`));
|
|
|
|
if (!file) {
|
|
return null;
|
|
}
|
|
|
|
const filePath = path.join(UPLOAD_DIR, file);
|
|
const buffer = await readFile(filePath);
|
|
|
|
// Validate it's actually an image
|
|
const isValid = await validateImageBuffer(buffer);
|
|
if (!isValid) {
|
|
return null;
|
|
}
|
|
|
|
// Extract original name from file (remove UUID prefix)
|
|
const originalName = file.substring(fileId.length + 1);
|
|
|
|
return { buffer, name: originalName };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const body: ProcessRequest = await request.json();
|
|
const { fileId, config } = body;
|
|
|
|
// Validate request
|
|
if (!fileId || typeof fileId !== "string") {
|
|
return NextResponse.json(
|
|
{ success: false, error: "Valid file ID is required" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Sanitize file ID to prevent path traversal
|
|
const sanitizedId = sanitizeFilename(fileId).replace(/\.[^.]+$/, "");
|
|
if (sanitizedId !== fileId) {
|
|
return NextResponse.json(
|
|
{ success: false, error: "Invalid file ID" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Validate config
|
|
const configValidation = validateCompressConfig(config);
|
|
if (!configValidation.valid) {
|
|
return NextResponse.json(
|
|
{ success: false, error: configValidation.error },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Find uploaded file
|
|
const uploadedFile = await findUploadedFile(fileId);
|
|
if (!uploadedFile) {
|
|
return NextResponse.json(
|
|
{ success: false, error: "File not found or expired" },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
// Get original metadata
|
|
const originalMetadata = await getImageMetadata(uploadedFile.buffer);
|
|
|
|
// Process image
|
|
const result = await compressImage(uploadedFile.buffer, config);
|
|
|
|
// Save processed file
|
|
const outputFormat = config.format === "original" ? originalMetadata.format : config.format;
|
|
const downloadInfo = await saveProcessedFile(
|
|
fileId, // Original file ID for tracking
|
|
result.buffer,
|
|
outputFormat,
|
|
uploadedFile.name
|
|
);
|
|
|
|
// Cleanup original file
|
|
await cleanupFile(fileId);
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
fileUrl: downloadInfo.fileUrl,
|
|
filename: downloadInfo.filename,
|
|
metadata: {
|
|
format: result.format,
|
|
quality: config.quality,
|
|
compressionRatio: result.compressionRatio,
|
|
originalSize: result.originalSize,
|
|
compressedSize: result.compressedSize,
|
|
width: result.width,
|
|
height: result.height,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error("Processing error:", error);
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: error instanceof Error ? error.message : "Processing failed",
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|