Media files are user-uploaded content that changes dynamically based on user interactions. Unlike static files, media files are not part of your application code and require special handling for security, storage, and processing.
Media files include any content uploaded by users or generated dynamically:
Media Files:
Static Files:
# settings.py
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
# Media files configuration
MEDIA_URL = '/media/' # URL prefix for media files
MEDIA_ROOT = BASE_DIR / 'media' # Directory where uploaded files are stored
# File upload settings
FILE_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB
DATA_UPLOAD_MAX_MEMORY_SIZE = 10485760 # 10MB
FILE_UPLOAD_PERMISSIONS = 0o644
FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755
# Allowed file types (security)
ALLOWED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp']
ALLOWED_DOCUMENT_EXTENSIONS = ['.pdf', '.doc', '.docx', '.txt']
MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10MB
# urls.py (main project)
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('blog.urls')),
]
# Serve media files during development
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
myproject/
├── media/ # Media root directory
│ ├── uploads/
│ │ ├── profiles/ # User profile images
│ │ │ ├── user_1/
│ │ │ │ ├── avatar.jpg
│ │ │ │ └── cover.jpg
│ │ │ └── user_2/
│ │ │ └── avatar.png
│ │ ├── documents/ # Document uploads
│ │ │ ├── 2024/
│ │ │ │ ├── 01/
│ │ │ │ └── 02/
│ │ │ └── contracts/
│ │ └── blog/ # Blog-related uploads
│ │ ├── featured/
│ │ └── gallery/
│ ├── thumbnails/ # Generated thumbnails
│ │ ├── small/
│ │ ├── medium/
│ │ └── large/
│ └── temp/ # Temporary files
└── static/ # Static files (separate)
# models.py
from django.db import models
from django.contrib.auth.models import User
from django.core.validators import FileExtensionValidator
import os
def user_profile_path(instance, filename):
"""Generate upload path for user profile images"""
return f'uploads/profiles/user_{instance.user.id}/{filename}'
def document_upload_path(instance, filename):
"""Generate upload path for documents with date organization"""
return f'uploads/documents/{instance.created_at.year}/{instance.created_at.month:02d}/{filename}'
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
# Image field with custom upload path
avatar = models.ImageField(
upload_to=user_profile_path,
blank=True,
null=True,
help_text="Profile picture (max 5MB)"
)
# File field with validation
resume = models.FileField(
upload_to='uploads/resumes/',
blank=True,
null=True,
validators=[FileExtensionValidator(allowed_extensions=['pdf', 'doc', 'docx'])],
help_text="Resume in PDF or Word format"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Document(models.Model):
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
# File field with custom path and validation
file = models.FileField(
upload_to=document_upload_path,
validators=[
FileExtensionValidator(
allowed_extensions=['pdf', 'doc', 'docx', 'txt', 'jpg', 'png']
)
]
)
# Additional metadata
file_size = models.PositiveIntegerField(blank=True, null=True)
content_type = models.CharField(max_length=100, blank=True)
uploaded_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def save(self, *args, **kwargs):
if self.file:
self.file_size = self.file.size
self.content_type = self.file.file.content_type
super().save(*args, **kwargs)
def get_file_extension(self):
return os.path.splitext(self.file.name)[1].lower()
def is_image(self):
return self.get_file_extension() in ['.jpg', '.jpeg', '.png', '.gif', '.webp']
def __str__(self):
return self.title
# models.py (continued)
from PIL import Image
from django.core.files.base import ContentFile
import io
class BlogPost(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
# Featured image with automatic thumbnail generation
featured_image = models.ImageField(
upload_to='uploads/blog/featured/',
blank=True,
null=True
)
# Thumbnail (auto-generated)
thumbnail = models.ImageField(
upload_to='uploads/blog/thumbnails/',
blank=True,
null=True,
editable=False
)
author = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# Generate thumbnail if featured image exists
if self.featured_image and not self.thumbnail:
self.create_thumbnail()
def create_thumbnail(self, size=(300, 200)):
"""Create thumbnail from featured image"""
if not self.featured_image:
return
# Open the image
image = Image.open(self.featured_image.path)
# Convert to RGB if necessary
if image.mode != 'RGB':
image = image.convert('RGB')
# Create thumbnail
image.thumbnail(size, Image.Resampling.LANCZOS)
# Save thumbnail
thumb_io = io.BytesIO()
image.save(thumb_io, format='JPEG', quality=85)
thumb_io.seek(0)
# Generate filename
thumb_name = f"thumb_{os.path.basename(self.featured_image.name)}"
thumb_name = os.path.splitext(thumb_name)[0] + '.jpg'
# Save to model
self.thumbnail.save(
thumb_name,
ContentFile(thumb_io.read()),
save=False
)
# Save the model (without triggering save again)
super().save(update_fields=['thumbnail'])
class Gallery(models.Model):
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class GalleryImage(models.Model):
gallery = models.ForeignKey(Gallery, on_delete=models.CASCADE, related_name='images')
title = models.CharField(max_length=200, blank=True)
# Original image
image = models.ImageField(upload_to='uploads/gallery/original/')
# Multiple sizes (auto-generated)
image_small = models.ImageField(upload_to='uploads/gallery/small/', blank=True, editable=False)
image_medium = models.ImageField(upload_to='uploads/gallery/medium/', blank=True, editable=False)
image_large = models.ImageField(upload_to='uploads/gallery/large/', blank=True, editable=False)
alt_text = models.CharField(max_length=200, blank=True)
order = models.PositiveIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['order', 'created_at']
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.image:
self.create_image_sizes()
def create_image_sizes(self):
"""Create multiple image sizes"""
sizes = {
'small': (150, 150),
'medium': (400, 300),
'large': (800, 600)
}
for size_name, dimensions in sizes.items():
field_name = f'image_{size_name}'
field = getattr(self, field_name)
if not field:
resized_image = self.resize_image(dimensions)
if resized_image:
filename = f"{size_name}_{os.path.basename(self.image.name)}"
field.save(filename, resized_image, save=False)
# Save all size fields
self.save(update_fields=['image_small', 'image_medium', 'image_large'])
def resize_image(self, size):
"""Resize image to specified dimensions"""
try:
image = Image.open(self.image.path)
if image.mode != 'RGB':
image = image.convert('RGB')
# Resize maintaining aspect ratio
image.thumbnail(size, Image.Resampling.LANCZOS)
# Save to BytesIO
output = io.BytesIO()
image.save(output, format='JPEG', quality=85)
output.seek(0)
return ContentFile(output.read())
except Exception as e:
print(f"Error resizing image: {e}")
return None
# forms.py
from django import forms
from django.core.exceptions import ValidationError
from .models import UserProfile, Document, BlogPost
import os
class UserProfileForm(forms.ModelForm):
class Meta:
model = UserProfile
fields = ['avatar', 'resume']
widgets = {
'avatar': forms.FileInput(attrs={
'class': 'form-control',
'accept': 'image/*'
}),
'resume': forms.FileInput(attrs={
'class': 'form-control',
'accept': '.pdf,.doc,.docx'
})
}
def clean_avatar(self):
avatar = self.cleaned_data.get('avatar')
if avatar:
# Check file size (5MB limit)
if avatar.size > 5 * 1024 * 1024:
raise ValidationError("Image file too large (max 5MB)")
# Check file extension
ext = os.path.splitext(avatar.name)[1].lower()
if ext not in ['.jpg', '.jpeg', '.png', '.gif']:
raise ValidationError("Invalid image format. Use JPG, PNG, or GIF.")
# Check image dimensions
try:
from PIL import Image
image = Image.open(avatar)
width, height = image.size
if width > 2000 or height > 2000:
raise ValidationError("Image dimensions too large (max 2000x2000)")
if width < 100 or height < 100:
raise ValidationError("Image dimensions too small (min 100x100)")
except Exception:
raise ValidationError("Invalid image file")
return avatar
def clean_resume(self):
resume = self.cleaned_data.get('resume')
if resume:
# Check file size (10MB limit)
if resume.size > 10 * 1024 * 1024:
raise ValidationError("File too large (max 10MB)")
# Check file extension
ext = os.path.splitext(resume.name)[1].lower()
if ext not in ['.pdf', '.doc', '.docx']:
raise ValidationError("Invalid file format. Use PDF or Word documents.")
return resume
class DocumentUploadForm(forms.ModelForm):
class Meta:
model = Document
fields = ['title', 'description', 'file']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'file': forms.FileInput(attrs={'class': 'form-control'})
}
def clean_file(self):
file = self.cleaned_data.get('file')
if file:
# File size validation
max_size = 20 * 1024 * 1024 # 20MB
if file.size > max_size:
raise ValidationError(f"File too large (max {max_size // (1024*1024)}MB)")
# File type validation
allowed_types = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain',
'image/jpeg',
'image/png'
]
if file.content_type not in allowed_types:
raise ValidationError("File type not allowed")
return file
class MultipleFileUploadForm(forms.Form):
files = forms.FileField(
widget=forms.ClearableFileInput(attrs={
'multiple': True,
'class': 'form-control'
}),
help_text="Select multiple files to upload"
)
def clean_files(self):
files = self.files.getlist('files')
if not files:
raise ValidationError("No files selected")
# Validate each file
for file in files:
if file.size > 5 * 1024 * 1024: # 5MB per file
raise ValidationError(f"File {file.name} is too large (max 5MB)")
return files
# views.py
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.contrib.auth.decorators import login_required
import json
@login_required
@require_http_methods(["POST"])
def ajax_file_upload(request):
"""Handle AJAX file uploads"""
try:
uploaded_file = request.FILES.get('file')
if not uploaded_file:
return JsonResponse({'error': 'No file provided'}, status=400)
# Validate file
if uploaded_file.size > 10 * 1024 * 1024: # 10MB
return JsonResponse({'error': 'File too large'}, status=400)
# Create document
document = Document.objects.create(
title=uploaded_file.name,
file=uploaded_file,
uploaded_by=request.user
)
return JsonResponse({
'success': True,
'file_id': document.id,
'file_name': document.file.name,
'file_url': document.file.url,
'file_size': document.file_size
})
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
@login_required
def upload_progress(request):
"""Track upload progress (requires additional setup)"""
# This requires django-uploadify or similar package
# for real upload progress tracking
pass
<!-- templates/profile.html -->
{% load static %}
<div class="user-profile">
<div class="profile-header">
{% if user.userprofile.avatar %}
<img src="{{ user.userprofile.avatar.url }}"
alt="{{ user.username }}'s avatar"
class="avatar-large">
{% else %}
<img src="{% static 'images/default-avatar.png' %}"
alt="Default avatar"
class="avatar-large">
{% endif %}
<h2>{{ user.get_full_name|default:user.username }}</h2>
</div>
{% if user.userprofile.resume %}
<div class="resume-section">
<h3>Resume</h3>
<a href="{{ user.userprofile.resume.url }}"
class="btn btn-primary"
download>
<i class="fas fa-download"></i> Download Resume
</a>
</div>
{% endif %}
</div>
<!-- Document list -->
<div class="documents">
<h3>Documents</h3>
{% for document in documents %}
<div class="document-item">
<div class="document-info">
<h4>{{ document.title }}</h4>
<p>{{ document.description }}</p>
<small>
Uploaded by {{ document.uploaded_by.username }}
on {{ document.created_at|date:"M d, Y" }}
({{ document.file_size|filesizeformat }})
</small>
</div>
<div class="document-actions">
{% if document.is_image %}
<a href="{{ document.file.url }}"
class="btn btn-sm btn-outline-primary"
target="_blank">
<i class="fas fa-eye"></i> View
</a>
{% endif %}
<a href="{{ document.file.url }}"
class="btn btn-sm btn-primary"
download>
<i class="fas fa-download"></i> Download
</a>
</div>
</div>
{% empty %}
<p>No documents uploaded yet.</p>
{% endfor %}
</div>
<!-- templates/blog/post_detail.html -->
{% load static %}
<article class="blog-post">
<header class="post-header">
<h1>{{ post.title }}</h1>
{% if post.featured_image %}
<div class="featured-image">
<picture>
<!-- Mobile -->
<source media="(max-width: 480px)"
srcset="{{ post.thumbnail.url }}">
<!-- Tablet -->
<source media="(max-width: 768px)"
srcset="{{ post.featured_image.url }}">
<!-- Desktop -->
<img src="{{ post.featured_image.url }}"
alt="{{ post.title }}"
class="img-fluid">
</picture>
</div>
{% endif %}
</header>
<div class="post-content">
{{ post.content|linebreaks }}
</div>
</article>
<!-- Gallery display -->
<div class="gallery">
{% for image in gallery.images.all %}
<div class="gallery-item">
<a href="{{ image.image.url }}"
data-lightbox="gallery"
data-title="{{ image.title }}">
<img src="{{ image.image_medium.url|default:image.image.url }}"
alt="{{ image.alt_text|default:image.title }}"
loading="lazy">
</a>
</div>
{% endfor %}
</div>
<!-- templates/includes/file_icon.html -->
{% load static %}
{% if document.is_image %}
<i class="fas fa-image text-success"></i>
{% else %}
{% with ext=document.get_file_extension %}
{% if ext == '.pdf' %}
<i class="fas fa-file-pdf text-danger"></i>
{% elif ext == '.doc' or ext == '.docx' %}
<i class="fas fa-file-word text-primary"></i>
{% elif ext == '.xls' or ext == '.xlsx' %}
<i class="fas fa-file-excel text-success"></i>
{% elif ext == '.txt' %}
<i class="fas fa-file-alt text-secondary"></i>
{% else %}
<i class="fas fa-file text-muted"></i>
{% endif %}
{% endwith %}
{% endif %}
# views.py
from django.http import Http404, HttpResponse
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404
import os
import mimetypes
@login_required
def secure_file_download(request, file_id):
"""Secure file download with access control"""
document = get_object_or_404(Document, id=file_id)
# Check permissions
if not can_access_file(request.user, document):
raise Http404("File not found")
# Get file path
file_path = document.file.path
if not os.path.exists(file_path):
raise Http404("File not found")
# Determine content type
content_type, _ = mimetypes.guess_type(file_path)
if not content_type:
content_type = 'application/octet-stream'
# Create response
with open(file_path, 'rb') as f:
response = HttpResponse(f.read(), content_type=content_type)
response['Content-Disposition'] = f'attachment; filename="{document.title}"'
response['Content-Length'] = os.path.getsize(file_path)
return response
def can_access_file(user, document):
"""Check if user can access the file"""
# Owner can always access
if document.uploaded_by == user:
return True
# Admin can access all files
if user.is_staff:
return True
# Add custom logic here
# e.g., shared files, team access, etc.
return False
# URL pattern for secure downloads
# urls.py
urlpatterns = [
path('files/<int:file_id>/download/', secure_file_download, name='secure_download'),
]
# validators.py
from django.core.exceptions import ValidationError
from django.core.files.images import get_image_dimensions
import magic
import os
def validate_file_extension(value):
"""Validate file extension"""
allowed_extensions = ['.pdf', '.doc', '.docx', '.jpg', '.jpeg', '.png', '.gif']
ext = os.path.splitext(value.name)[1].lower()
if ext not in allowed_extensions:
raise ValidationError(f'File extension {ext} not allowed')
def validate_file_size(value):
"""Validate file size"""
max_size = 10 * 1024 * 1024 # 10MB
if value.size > max_size:
raise ValidationError(f'File size {value.size} exceeds maximum {max_size}')
def validate_image_dimensions(value):
"""Validate image dimensions"""
try:
width, height = get_image_dimensions(value)
if width and height:
if width > 4000 or height > 4000:
raise ValidationError('Image dimensions too large (max 4000x4000)')
if width < 50 or height < 50:
raise ValidationError('Image dimensions too small (min 50x50)')
except Exception:
raise ValidationError('Invalid image file')
def validate_file_content(value):
"""Validate file content matches extension"""
try:
# Read file content to determine actual type
file_content = value.read()
value.seek(0) # Reset file pointer
# Use python-magic to detect file type
file_type = magic.from_buffer(file_content, mime=True)
# Map extensions to MIME types
allowed_types = {
'.pdf': 'application/pdf',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
}
ext = os.path.splitext(value.name)[1].lower()
expected_type = allowed_types.get(ext)
if expected_type and file_type != expected_type:
raise ValidationError(f'File content does not match extension {ext}')
except Exception as e:
raise ValidationError(f'File validation error: {str(e)}')
# Usage in models
class SecureDocument(models.Model):
file = models.FileField(
upload_to='secure_uploads/',
validators=[
validate_file_extension,
validate_file_size,
validate_file_content
]
)
# utils.py
from PIL import Image, ImageOps
from django.core.files.base import ContentFile
import io
import os
def create_thumbnail(image_field, size=(300, 300), quality=85):
"""Create thumbnail from image field"""
if not image_field:
return None
try:
# Open image
image = Image.open(image_field.path)
# Convert to RGB if necessary
if image.mode in ('RGBA', 'LA', 'P'):
image = image.convert('RGB')
# Create thumbnail maintaining aspect ratio
image = ImageOps.fit(image, size, Image.Resampling.LANCZOS)
# Save to BytesIO
thumb_io = io.BytesIO()
image.save(thumb_io, format='JPEG', quality=quality, optimize=True)
thumb_io.seek(0)
# Generate filename
name, ext = os.path.splitext(os.path.basename(image_field.name))
thumb_name = f"{name}_thumb.jpg"
return ContentFile(thumb_io.read(), name=thumb_name)
except Exception as e:
print(f"Error creating thumbnail: {e}")
return None
def resize_image(image_field, width=None, height=None, quality=85):
"""Resize image to specific dimensions"""
if not image_field:
return None
try:
image = Image.open(image_field.path)
if image.mode in ('RGBA', 'LA', 'P'):
image = image.convert('RGB')
# Calculate new size
original_width, original_height = image.size
if width and height:
new_size = (width, height)
elif width:
ratio = width / original_width
new_size = (width, int(original_height * ratio))
elif height:
ratio = height / original_height
new_size = (int(original_width * ratio), height)
else:
return None
# Resize image
image = image.resize(new_size, Image.Resampling.LANCZOS)
# Save to BytesIO
output = io.BytesIO()
image.save(output, format='JPEG', quality=quality, optimize=True)
output.seek(0)
# Generate filename
name, ext = os.path.splitext(os.path.basename(image_field.name))
resized_name = f"{name}_{new_size[0]}x{new_size[1]}.jpg"
return ContentFile(output.read(), name=resized_name)
except Exception as e:
print(f"Error resizing image: {e}")
return None
def optimize_image(image_field, quality=85, max_width=1920, max_height=1080):
"""Optimize image for web"""
if not image_field:
return None
try:
image = Image.open(image_field.path)
# Convert to RGB
if image.mode in ('RGBA', 'LA', 'P'):
image = image.convert('RGB')
# Resize if too large
width, height = image.size
if width > max_width or height > max_height:
image.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
# Optimize and save
output = io.BytesIO()
image.save(output, format='JPEG', quality=quality, optimize=True)
output.seek(0)
return ContentFile(output.read(), name=image_field.name)
except Exception as e:
print(f"Error optimizing image: {e}")
return None
# settings/production.py
from storages.backends.s3boto3 import S3Boto3Storage
# AWS S3 Configuration
AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME')
AWS_S3_REGION_NAME = os.environ.get('AWS_S3_REGION_NAME', 'us-east-1')
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
# Media files storage
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/media/'
# Custom storage classes
class MediaStorage(S3Boto3Storage):
location = 'media'
default_acl = 'public-read'
file_overwrite = False
class PrivateMediaStorage(S3Boto3Storage):
location = 'private'
default_acl = 'private'
file_overwrite = False
custom_domain = False
# Usage in models
class Document(models.Model):
public_file = models.FileField(storage=MediaStorage())
private_file = models.FileField(storage=PrivateMediaStorage())
# settings/production.py
# CloudFront CDN
AWS_S3_CUSTOM_DOMAIN = 'cdn.example.com'
MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/media/'
# Cache control headers
AWS_S3_OBJECT_PARAMETERS = {
'CacheControl': 'max-age=86400', # 1 day
}
# Different cache times for different file types
class OptimizedMediaStorage(S3Boto3Storage):
def get_object_parameters(self, name):
params = super().get_object_parameters(name)
# Longer cache for images
if name.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')):
params['CacheControl'] = 'max-age=2592000' # 30 days
# Shorter cache for documents
elif name.lower().endswith(('.pdf', '.doc', '.docx')):
params['CacheControl'] = 'max-age=86400' # 1 day
return params
Media files require careful handling for security, performance, and user experience. Proper validation, processing, and storage strategies ensure your Django application can handle user uploads safely and efficiently at scale.
Including Static Files
Static files (CSS, JavaScript, images, fonts) are essential components of modern web applications. Django provides a robust system for managing, serving, and optimizing static files across development and production environments.
Using Alternative Template Engines
While Django's built-in template engine is powerful and secure, alternative template engines like Jinja2 offer different features and performance characteristics. This chapter covers integrating alternative engines and choosing the right tool for your needs.