python 41 lines · 7 steps

Serving files with access checks in Django

A Django view that authorizes, validates, and streams a protected avatar image with proper HTTP caching.

Explained by highlit
1from pathlib import Path
2 
3from django.contrib.auth.decorators import login_required
4from django.http import FileResponse, Http404, HttpResponseForbidden
5from django.shortcuts import get_object_or_404
6from django.utils.http import http_date
7from django.views.decorators.http import require_GET
8 
9from .models import Avatar
10 
11 
12@require_GET
13@login_required
14def serve_avatar(request, avatar_id):
15 avatar = get_object_or_404(Avatar.objects.select_related("owner"), pk=avatar_id)
16 
17 if not avatar.is_public and avatar.owner_id != request.user.id:
18 if not request.user.has_perm("accounts.view_avatar", avatar):
19 return HttpResponseForbidden("You may not view this avatar.")
20 
21 path = Path(avatar.image.path)
22 if not path.is_file():
23 raise Http404("Avatar file is missing.")
24 
25 stat = path.stat()
26 etag = f'"{avatar_id}-{int(stat.st_mtime)}-{stat.st_size}"'
27 if request.headers.get("If-None-Match") == etag:
28 response = FileResponse(status=304)
29 response["ETag"] = etag
30 return response
31 
32 response = FileResponse(
33 path.open("rb"),
34 content_type=avatar.content_type or "application/octet-stream",
35 )
36 response["ETag"] = etag
37 response["Last-Modified"] = http_date(stat.st_mtime)
38 response["Content-Length"] = stat.st_size
39 response["Cache-Control"] = "private, max-age=3600"
40 response["Content-Disposition"] = f'inline; filename="{path.name}"'
41 return response
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Layer authorization checks before touching the filesystem so unauthorized requests never reach the file.
  2. 2An ETag built from mutable file attributes lets clients skip re-downloading unchanged content via 304 responses.
  3. 3Setting Last-Modified, Content-Length, and Cache-Control headers explicitly gives you control over how responses are cached.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Serving files with access checks in Django — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code