# File Upload and EXIF Processing Implementation ## Overview This document describes the implementation of file upload and EXIF metadata extraction for the Beach Litter Mapping Platform backend. ## Components ### 1. Multer Middleware (`src/middleware/upload.ts`) Handles file uploads to the local filesystem with the following features: - **Storage Configuration**: Files are stored in the `./uploads` directory with unique filenames - **File Naming**: `{fieldname}-{timestamp}-{random}.{ext}` format to prevent collisions - **File Type Validation**: Only accepts JPEG, PNG, and WebP images - **Size Limits**: Maximum file size of 5MB (configurable via `MAX_FILE_SIZE` env var) - **Auto-directory Creation**: Creates upload directory if it doesn't exist #### Configuration ```typescript const storage = multer.diskStorage({ destination: './uploads', filename: 'photo-{timestamp}-{random}.{ext}' }); const fileFilter = (file) => { allowedTypes: ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'] }; const limits = { fileSize: 5242880 // 5MB }; ``` ### 2. EXIF Extraction Utility (`src/utils/exif.ts`) Extracts metadata from uploaded photos using the `exifr` library: - **GPS Coordinates**: Latitude and longitude from EXIF GPS data - **Timestamp**: Original photo capture time (DateTimeOriginal) - **Camera Info**: Make and model of the camera/device - **Error Handling**: Returns empty object if EXIF data is unavailable #### Extracted Data ```typescript interface ExifData { latitude?: number; longitude?: number; timestamp?: Date; make?: string; model?: string; } ``` ### 3. Upload Endpoint (`POST /api/reports/upload`) Protected endpoint for uploading individual photos: - **Authentication**: Requires valid Clerk session token - **Single File Upload**: Accepts one photo at a time via `photo` field - **EXIF Processing**: Automatically extracts metadata from uploaded photo - **Response**: Returns file URL and extracted EXIF data #### Request ```bash POST /api/reports/upload Authorization: Bearer {clerk_token} Content-Type: multipart/form-data photo: {file} ``` #### Response ```json { "url": "/uploads/photo-1234567890-123456789.jpg", "exifData": { "latitude": 53.3498, "longitude": -6.2603, "timestamp": "2024-11-16T12:30:00.000Z" } } ``` ### 4. Static File Serving Uploaded files are served via Express static middleware: ```typescript app.use('/uploads', express.static(path.join(__dirname, '../uploads'))); ``` Files are accessible at: `http://localhost:3005/uploads/{filename}` ## Usage Flow ### 1. Upload Photo **Important**: The user must be signed in with Clerk to upload photos. The frontend automatically includes the Clerk session token in the Authorization header. ```typescript // Frontend usage (with Clerk authentication) const formData = new FormData(); formData.append('photo', file); // The reportService automatically includes the auth token const { url, exifData } = await reportService.uploadPhoto(file); ``` **Manual API call** (for testing): ```typescript const formData = new FormData(); formData.append('photo', file); // Get Clerk token (in a Clerk-authenticated component) const { getToken } = useAuth(); const token = await getToken(); const response = await fetch('/api/reports/upload', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData }); const { url, exifData } = await response.json(); ``` ### 2. Create Report with Photo URLs ```typescript const report = await fetch('/api/reports', { method: 'POST', headers: { 'Authorization': `Bearer ${clerkToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ latitude: exifData.latitude || manualLat, longitude: exifData.longitude || manualLng, locationSource: exifData.latitude ? 'exif' : 'manual', photoUrls: [url], photoTimestamp: exifData.timestamp, litterType: 'plastic', quantity: 'moderate', description: 'Beach litter report' }) }); ``` ## Error Handling ### File Too Large ```json { "error": { "code": "FILE_TOO_LARGE", "message": "File size exceeds the maximum limit of 5MB", "timestamp": "2024-11-16T12:00:00.000Z" } } ``` ### Invalid File Type ```json { "error": { "code": "INVALID_FILE_TYPE", "message": "Invalid file type. Only JPEG, PNG, and WebP images are allowed.", "timestamp": "2024-11-16T12:00:00.000Z" } } ``` ### No File Uploaded ```json { "error": { "code": "NO_FILE", "message": "No file uploaded", "timestamp": "2024-11-16T12:00:00.000Z" } } ``` ## Environment Variables ```env # File Upload Configuration UPLOAD_DIR=./uploads # Directory for uploaded files MAX_FILE_SIZE=5242880 # Maximum file size in bytes (5MB) ``` ## Security Considerations 1. **File Type Validation**: Only image MIME types are accepted 2. **Size Limits**: Prevents large file uploads that could exhaust storage 3. **Authentication Required**: All uploads require valid Clerk session 4. **Unique Filenames**: Prevents file overwrites and collisions 5. **EXIF Stripping**: Consider stripping sensitive EXIF data before public display (future enhancement) ## Testing Run the test script to verify the implementation: ```bash npm run test:upload # or npx tsx src/scripts/testFileUpload.ts ``` The test verifies: - ✅ Uploads directory exists - ✅ EXIF extraction works correctly - ✅ Configuration is properly set - ✅ Multer middleware loads successfully ## Requirements Satisfied This implementation satisfies the following requirements: - **1.2**: Extract GPS coordinates from EXIF metadata - **1.3**: Prompt for location when EXIF GPS data is unavailable - **1.4**: Accept at least one photograph with minimum resolution - **1.5**: Extract and display original photo capture time from EXIF ## Future Enhancements 1. **Cloud Storage**: Migrate from local filesystem to AWS S3 or Cloudinary 2. **Image Optimization**: Resize and compress images on upload 3. **Multiple File Upload**: Support batch photo uploads 4. **EXIF Privacy**: Strip sensitive EXIF data (location, device info) before public display 5. **Virus Scanning**: Integrate antivirus scanning for uploaded files 6. **CDN Integration**: Serve images via CDN for better performance