Difficulty: Intermediate
File uploads in Django require specific configuration at the form, view, model, and settings levels. Understanding each layer is essential for handling file uploads correctly and securely.
At the model level, use FileField for general files and ImageField for images (which validates that the uploaded file is a valid image). Both fields accept an 'upload_to' argument that specifies the subdirectory within MEDIA_ROOT where files will be stored. upload_to can be a string or a callable function for dynamic paths.
At the settings level, you must configure MEDIA_ROOT (the filesystem path where uploaded files are stored) and MEDIA_URL (the URL prefix for serving these files). In development, you also need to add media URL patterns to your root urls.py using static() from django.conf.urls.static.
At the form level, the HTML form tag must include enctype="multipart/form-data" for file uploads to work. Without this attribute, the browser sends files as text, which Django cannot process. In your view, you must pass request.FILES as the second argument to the form constructor alongside request.POST.
For file validation, Django's ImageField automatically validates that uploaded files are valid images using Pillow. For custom validation, you can add validators to limit file size, restrict file types, or check image dimensions. Always validate uploaded files on the server side, even if you also validate on the client side.
CSRF (Cross-Site Request Forgery) protection is Django's defense against attacks where a malicious website tricks a user's browser into making requests to your site. Django's CSRF middleware generates a unique token for each user session. This token must be included in every POST, PUT, PATCH, and DELETE request. In templates, {% csrf_token %} adds a hidden input with the token. For AJAX requests, the token can be included as an HTTP header (X-CSRFToken).
Django's CSRF protection is enabled by default through the CsrfViewMiddleware. For views that should not require CSRF tokens (like API endpoints using token authentication), use the @csrf_exempt decorator. However, be cautious - disabling CSRF protection should only be done when another authentication mechanism is in place.
# models.py
import os
from django.db import models
def user_upload_path(instance, filename):
"""Generate upload path: uploads/user_<id>/<filename>"""
return f'uploads/user_{instance.user.pk}/{filename}'
class Document(models.Model):
title = models.CharField(max_length=200)
file = models.FileField(upload_to='documents/')
image = models.ImageField(upload_to=user_upload_path, blank=True)
uploaded_at = models.DateTimeField(auto_now_add=True)
# settings.py
import os
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
# urls.py (development only)
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
# ... your URL patterns
]
if settings.DEBUG:
urlpatterns += static(
settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT,
)
FileField stores files in MEDIA_ROOT/<upload_to>. upload_to can be a string or callable for dynamic paths. In development, add static() to serve media files. In production, use Nginx or a CDN instead.
# forms.py
from django import forms
from .models import Document
class DocumentForm(forms.ModelForm):
class Meta:
model = Document
fields = ['title', 'file', 'image']
# views.py
from django.shortcuts import render, redirect
from .forms import DocumentForm
def upload_document(request):
if request.method == 'POST':
form = DocumentForm(request.POST, request.FILES) # Include FILES!
if form.is_valid():
document = form.save(commit=False)
document.user = request.user
document.save()
return redirect('document-detail', pk=document.pk)
else:
form = DocumentForm()
return render(request, 'documents/upload.html', {'form': form})
# template: documents/upload.html
# <form method="post" enctype="multipart/form-data">
# {% csrf_token %}
# {{ form.as_p }}
# <button type="submit">Upload</button>
# </form>
Three critical requirements: (1) enctype='multipart/form-data' on the form tag, (2) request.FILES passed to the form constructor, (3) {% csrf_token %} for CSRF protection. Missing any of these causes file uploads to fail.
from django.core.exceptions import ValidationError
def validate_file_size(value):
max_size = 5 * 1024 * 1024 # 5 MB
if value.size > max_size:
raise ValidationError(
f'File size must be under 5 MB. Current size: '
f'{value.size / (1024 * 1024):.1f} MB'
)
def validate_file_type(value):
allowed_types = ['application/pdf', 'image/jpeg', 'image/png']
if value.content_type not in allowed_types:
raise ValidationError(
'Only PDF, JPEG, and PNG files are allowed.'
)
class Document(models.Model):
file = models.FileField(
upload_to='documents/',
validators=[validate_file_size, validate_file_type],
)
Custom validators check file size and type. value.size is in bytes, value.content_type is the MIME type. Always validate on the server side - client-side validation can be bypassed.
// JavaScript: Get CSRF token from cookie
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.startsWith(name + '=')) {
cookieValue = decodeURIComponent(
cookie.substring(name.length + 1)
);
break;
}
}
}
return cookieValue;
}
// Include CSRF token in fetch requests
fetch('/api/data/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken'),
},
body: JSON.stringify({ key: 'value' }),
});
For AJAX POST requests, include the CSRF token as the X-CSRFToken header. The token is stored in the 'csrftoken' cookie. This authenticates the request as coming from your site, not a malicious third party.
FileField, ImageField, MEDIA_ROOT, CSRF, enctype