mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 15:51:09 -05:00
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols. - Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage. - Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
8.0 KiB
8.0 KiB
ADR-006: Media Handling with Cloudflare Images
Status
Accepted
Context
ThrillWiki handles user-uploaded images for:
- Park photos
- Ride photos
- User avatars
We needed a media handling solution that would:
- Handle image uploads efficiently
- Optimize images for different devices
- Provide CDN delivery globally
- Support image transformations (resizing, cropping)
- Minimize storage costs
Decision
We chose Cloudflare Images as our image hosting and transformation service.
Architecture
┌─────────────────────────────────────────────────────────┐
│ User Upload │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Django Backend │
│ ┌─────────────────────────────────────────────────┐ │
│ │ MediaService │ │
│ │ - Validate upload │ │
│ │ - Generate upload URL │ │
│ │ - Store Cloudflare ID │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Cloudflare Images │
│ ┌─────────────────────────────────────────────────┐ │
│ │ - Image storage │ │
│ │ - On-the-fly transformations │ │
│ │ - Global CDN delivery │ │
│ │ - Multiple variants │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Image Variants
| Variant | Dimensions | Use Case |
|---|---|---|
thumbnail |
150x150 | List views, avatars |
card |
400x300 | Card components |
hero |
1200x600 | Park/ride headers |
public |
Original (max 2000px) | Full size view |
Configuration
# Environment variables
CLOUDFLARE_IMAGES_ACCOUNT_ID = config('CLOUDFLARE_IMAGES_ACCOUNT_ID')
CLOUDFLARE_IMAGES_API_TOKEN = config('CLOUDFLARE_IMAGES_API_TOKEN')
CLOUDFLARE_IMAGES_ACCOUNT_HASH = config('CLOUDFLARE_IMAGES_ACCOUNT_HASH')
Upload Flow
# backend/apps/core/services/media_service.py
class CloudflareImagesService:
"""Service for handling image uploads to Cloudflare Images."""
def get_direct_upload_url(self):
"""Get a one-time upload URL for direct uploads."""
response = self._api_request('POST', 'direct_upload')
return {
'upload_url': response['uploadURL'],
'image_id': response['id'],
}
def get_image_url(self, image_id, variant='public'):
"""Get the delivery URL for an image."""
return f"https://imagedelivery.net/{self.account_hash}/{image_id}/{variant}"
def delete_image(self, image_id):
"""Delete an image from Cloudflare."""
return self._api_request('DELETE', f'images/{image_id}')
Consequences
Benefits
- Global CDN: Fast image delivery worldwide
- Automatic Optimization: WebP/AVIF conversion as appropriate
- On-Demand Transformations: Resize/crop without pre-generating
- Cost Effective: Pay per image stored, not per transformation
- Reduced Server Load: Direct uploads bypass our servers
- Security: Signed URLs for private content if needed
Trade-offs
- Vendor Lock-in: Cloudflare-specific API
- External Dependency: Service availability dependency
- Cost Scaling: Costs increase with storage volume
- Migration Complexity: Moving away requires re-uploading
URL Structure
https://imagedelivery.net/{account_hash}/{image_id}/{variant}
Examples:
https://imagedelivery.net/abc123/img-xyz/thumbnail
https://imagedelivery.net/abc123/img-xyz/hero
https://imagedelivery.net/abc123/img-xyz/public
Alternatives Considered
Django + S3 + CloudFront
Rejected because:
- Requires pre-generating all sizes
- More complex infrastructure
- Higher storage costs for variants
- Manual CDN configuration
Self-Hosted with ImageMagick
Rejected because:
- Server CPU overhead for transformations
- No built-in CDN
- Storage management complexity
- Scaling challenges
Imgix
Rejected because:
- Higher costs for our volume
- Already using Cloudflare for other services
- Similar feature set to Cloudflare Images
Implementation Details
Photo Model
class Photo(models.Model):
"""Base photo model using Cloudflare Images."""
cloudflare_id = models.CharField(max_length=100, unique=True)
original_filename = models.CharField(max_length=255)
uploaded_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
uploaded_at = models.DateTimeField(auto_now_add=True)
is_approved = models.BooleanField(default=False)
caption = models.TextField(blank=True)
@property
def thumbnail_url(self):
return get_image_url(self.cloudflare_id, 'thumbnail')
@property
def public_url(self):
return get_image_url(self.cloudflare_id, 'public')
Direct Upload API
# backend/apps/api/v1/views/upload.py
class DirectUploadView(APIView):
"""Get a direct upload URL for Cloudflare Images."""
permission_classes = [IsAuthenticated]
def post(self, request):
service = CloudflareImagesService()
upload_data = service.get_direct_upload_url()
return Response({
'upload_url': upload_data['upload_url'],
'image_id': upload_data['image_id'],
})
Cleanup Task
# Celery task for cleaning up orphaned images
@shared_task
def cleanup_orphaned_images():
"""Delete images not referenced by any model."""
cutoff = timezone.now() - timedelta(hours=24)
orphaned = CloudflareImage.objects.filter(
created_at__lt=cutoff,
park_photos__isnull=True,
ride_photos__isnull=True,
user_avatars__isnull=True,
)
for image in orphaned:
service.delete_image(image.cloudflare_id)
image.delete()
Fallback Strategy
def get_image_url(image_id, variant='public', fallback='/static/images/placeholder.jpg'):
"""Get image URL with fallback for missing images."""
if not image_id:
return fallback
return f"https://imagedelivery.net/{ACCOUNT_HASH}/{image_id}/{variant}"