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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Layer authorization checks before touching the filesystem so unauthorized requests never reach the file.
- 2An ETag built from mutable file attributes lets clients skip re-downloading unchanged content via 304 responses.
- 3Setting Last-Modified, Content-Length, and Cache-Control headers explicitly gives you control over how responses are cached.
Related explainers
python
from configparser import ConfigParser, ExtendedInterpolation from pathlib import Path
Layered INI config loading in Python
configuration
parsing
defaults
Intermediate
8 steps
python
from urllib.parse import urlparse class RobotsRules:
Parsing and applying a robots.txt file
parsing
longest-prefix-match
state-machine
Intermediate
10 steps
python
from copy import deepcopy class CowSnapshot:
Copy-on-write attribute snapshots in Python
copy-on-write
descriptors
attribute-interception
Advanced
7 steps
python
from flask import Blueprint, jsonify from marshmallow import Schema, fields, validate, EXCLUDE from webargs.flaskparser import use_args
Validating query params in Flask with webargs
validation
schema
query-building
Intermediate
10 steps
python
import hashlib import json from fastapi import APIRouter, Request, Response, Depends, HTTPException, status
HTTP ETag caching in a FastAPI route
http-caching
etag
conditional-requests
Intermediate
9 steps
python
from django.db import models from django.db.models import Q from django.conf import settings
Enforcing one default address per user in Django
data modeling
database constraints
partial index
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/serving-files-with-access-checks-in-django-explained-python-8b55/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.