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
99 lines
2.5 KiB
TypeScript
99 lines
2.5 KiB
TypeScript
/**
|
|
* File Management API Endpoint
|
|
* Agent 3: File Upload & Storage System
|
|
*
|
|
* GET /api/upload/[fileId] - Get file info or signed URL
|
|
* DELETE /api/upload/[fileId] - Delete a file
|
|
*/
|
|
|
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
import { getUploadService } from '../../../lib/storage';
|
|
|
|
interface FileResponse {
|
|
success: boolean;
|
|
signedUrl?: string;
|
|
exists?: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
interface DeleteResponse {
|
|
success: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
export default async function handler(
|
|
req: NextApiRequest,
|
|
res: NextApiResponse<FileResponse | DeleteResponse>
|
|
) {
|
|
const { fileId } = req.query;
|
|
|
|
if (!fileId || typeof fileId !== 'string') {
|
|
return res.status(400).json({ success: false, error: 'File ID is required' });
|
|
}
|
|
|
|
// Decode the file key (it may be URL encoded)
|
|
const fileKey = decodeURIComponent(fileId);
|
|
|
|
const uploadService = getUploadService();
|
|
|
|
switch (req.method) {
|
|
case 'GET': {
|
|
try {
|
|
// Check if file exists
|
|
const exists = await uploadService.exists(fileKey);
|
|
|
|
if (!exists) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
exists: false,
|
|
error: 'File not found',
|
|
});
|
|
}
|
|
|
|
// Get expiration time from query params (default 1 hour)
|
|
const expiresIn = parseInt(req.query.expiresIn as string) || 3600;
|
|
|
|
// Get signed URL for private file access
|
|
const signedUrl = await uploadService.getSignedUrl(fileKey, expiresIn);
|
|
|
|
return res.status(200).json({
|
|
success: true,
|
|
exists: true,
|
|
signedUrl,
|
|
});
|
|
} catch (error) {
|
|
console.error('Get file error:', error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Internal server error',
|
|
});
|
|
}
|
|
}
|
|
|
|
case 'DELETE': {
|
|
try {
|
|
const deleted = await uploadService.delete(fileKey);
|
|
|
|
if (!deleted) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'File not found or could not be deleted',
|
|
});
|
|
}
|
|
|
|
return res.status(200).json({
|
|
success: true,
|
|
});
|
|
} catch (error) {
|
|
console.error('Delete file error:', error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Internal server error',
|
|
});
|
|
}
|
|
}
|
|
|
|
default:
|
|
return res.status(405).json({ success: false, error: 'Method not allowed' });
|
|
}
|
|
}
|