localgreenchain/pages/api/upload/[fileId].ts
Claude d74128d3cd
Add Agent 3: File Upload & Storage System
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
2025-11-23 03:51:31 +00:00

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