feat: 实现图片压缩功能

This commit is contained in:
2026-01-25 22:37:40 +08:00
parent 081e2058bf
commit 54009163b1
18 changed files with 1603 additions and 167 deletions

View File

@@ -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 }
);
}