feat: 实现图片压缩功能
This commit is contained in:
@@ -1,57 +1,137 @@
|
||||
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;
|
||||
|
||||
if (!fileId) {
|
||||
// Validate request
|
||||
if (!fileId || typeof fileId !== "string") {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "No file ID provided" },
|
||||
{ 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
|
||||
if (!config.quality || config.quality < 1 || config.quality > 100) {
|
||||
const configValidation = validateCompressConfig(config);
|
||||
if (!configValidation.valid) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Invalid quality value" },
|
||||
{ success: false, error: configValidation.error },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// In production, you would:
|
||||
// 1. Retrieve the file from storage
|
||||
// 2. Use Sharp to compress the image
|
||||
// 3. Apply format conversion if needed
|
||||
// 4. Upload to R2/S3
|
||||
// 5. Return download URL
|
||||
// Find uploaded file
|
||||
const uploadedFile = await findUploadedFile(fileId);
|
||||
if (!uploadedFile) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "File not found or expired" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Mock processing for now
|
||||
const resultFileId = `processed-${Date.now()}`;
|
||||
// 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: `/api/download/${resultFileId}`,
|
||||
filename: `compressed-${resultFileId}`,
|
||||
fileUrl: downloadInfo.fileUrl,
|
||||
filename: downloadInfo.filename,
|
||||
metadata: {
|
||||
format: config.format,
|
||||
format: result.format,
|
||||
quality: config.quality,
|
||||
compressionRatio: Math.floor(Math.random() * 30) + 40, // Mock 40-70%
|
||||
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: "Processing failed" },
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Processing failed",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user