Implements cloud-based file storage for plant photos, documents, and certificates: Storage Layer: - Multi-provider support (AWS S3, Cloudflare R2, MinIO, local filesystem) - S3-compatible provider with presigned URL generation - Local storage provider for development with signed URL verification - Configurable via environment variables Image Processing: - Automatic thumbnail generation (150x150, 300x300, 600x600, 1200x1200) - WebP conversion for optimized file sizes - EXIF data extraction for image metadata - Image optimization with Sharp API Endpoints: - POST /api/upload/image - Upload images with automatic processing - POST /api/upload/document - Upload documents (PDF, DOC, DOCX) - POST /api/upload/presigned - Get presigned URLs for direct uploads - GET/DELETE /api/upload/[fileId] - File management UI Components: - ImageUploader - Drag & drop image upload with preview - PhotoGallery - Grid gallery with lightbox view - DocumentUploader - Document upload with file type icons - ProgressBar - Animated upload progress indicator Database: - FileStore service with in-memory storage (Prisma schema ready for Agent 2) - File metadata tracking with soft delete support - Category-based file organization
92 lines
2.5 KiB
TypeScript
92 lines
2.5 KiB
TypeScript
/**
|
|
* Presigned URL API Endpoint
|
|
* Agent 3: File Upload & Storage System
|
|
*
|
|
* POST /api/upload/presigned - Get a presigned URL for direct upload
|
|
*/
|
|
|
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
import { getUploadService } from '../../../lib/storage';
|
|
import type { FileCategory, PresignedUrlResponse } from '../../../lib/storage/types';
|
|
|
|
interface RequestBody {
|
|
filename: string;
|
|
contentType: string;
|
|
category: FileCategory;
|
|
expiresIn?: number;
|
|
}
|
|
|
|
interface PresignedResponse {
|
|
success: boolean;
|
|
data?: PresignedUrlResponse;
|
|
error?: string;
|
|
}
|
|
|
|
export default async function handler(
|
|
req: NextApiRequest,
|
|
res: NextApiResponse<PresignedResponse>
|
|
) {
|
|
if (req.method !== 'POST') {
|
|
return res.status(405).json({ success: false, error: 'Method not allowed' });
|
|
}
|
|
|
|
try {
|
|
const { filename, contentType, category, expiresIn } = req.body as RequestBody;
|
|
|
|
// Validate required fields
|
|
if (!filename || !contentType || !category) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Missing required fields: filename, contentType, category',
|
|
});
|
|
}
|
|
|
|
// Validate category
|
|
const validCategories: FileCategory[] = ['plant-photo', 'certificate', 'document', 'report', 'avatar'];
|
|
if (!validCategories.includes(category)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: `Invalid category. Must be one of: ${validCategories.join(', ')}`,
|
|
});
|
|
}
|
|
|
|
// Validate content type
|
|
const allowedContentTypes = [
|
|
'image/jpeg',
|
|
'image/png',
|
|
'image/gif',
|
|
'image/webp',
|
|
'image/heic',
|
|
'image/heif',
|
|
'application/pdf',
|
|
'application/msword',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
];
|
|
|
|
if (!allowedContentTypes.includes(contentType)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: `Invalid content type. Must be one of: ${allowedContentTypes.join(', ')}`,
|
|
});
|
|
}
|
|
|
|
const uploadService = getUploadService();
|
|
const presignedUrl = await uploadService.getPresignedUploadUrl({
|
|
filename,
|
|
contentType,
|
|
category,
|
|
expiresIn: expiresIn || 3600, // Default 1 hour
|
|
});
|
|
|
|
return res.status(200).json({
|
|
success: true,
|
|
data: presignedUrl,
|
|
});
|
|
} catch (error) {
|
|
console.error('Presigned URL error:', error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Internal server error',
|
|
});
|
|
}
|
|
}
|