feat: implement file upload and EXIF processing (task 25)
- Configure Multer for photo uploads to local filesystem - Set up uploads directory and Express static file serving - Implement EXIF metadata extraction with exifr library - Add file validation (type: JPEG/PNG/WebP, size: 5MB max) - Create photo URL generation for local storage paths - Add upload endpoint POST /api/reports/upload - Include comprehensive error handling - Add test scripts and documentation Requirements satisfied: 1.2, 1.3, 1.4, 1.5
This commit is contained in:
226
server/FILE_UPLOAD_IMPLEMENTATION.md
Normal file
226
server/FILE_UPLOAD_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# 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
|
||||
|
||||
```typescript
|
||||
const formData = new FormData();
|
||||
formData.append('photo', file);
|
||||
|
||||
const response = await fetch('/api/reports/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${clerkToken}`
|
||||
},
|
||||
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
|
||||
Reference in New Issue
Block a user