Many Django tutorials don't get file uploads right.
Even the official docs β typically very good β demonstrate server-side uploads. This is bad for a live application: without extra technologies, your server will freeze if users upload multiple large files server-side at the same time.
Instead, the effective way is to upload files straight from the user's web browser (client-side) to your file hosting service, such as AWS S3.
I've shown how to do this with HTMX before.
This guide uses Alpine.js and is even faster to add. I challenge you to complete the guide in under 4 minutes and 30 seconds π
Optional video tutorial (featuring me ππΏ) is here:
pip install django boto3 python-dotenv
django-admin startproject core .
python manage.py startapp sim
- Register your new app by adding it to your `INSTALLED_APPS` in `settings.py`.
# settings.py
INSTALLED_APPS = [
...
'sim',
...
]
Joke: Why did the file apply for a job at AWS?
It was on its S3 bucket list.
Connecting your AWS account (and S3 bucket πͺ£) is fast to do. Each step is very small.
IAM
> Users
> Create user
). Use any username.On 'Set Permissions'
:
'Attach policies directly'
AmazonS3FullAccess
(full S3 read/write access to your IAM role). (Search for AmazonS3FullAccess
, rather than scrolling through the 1131 available permissions policies).
Access keys
, click Create access key
Set description tag
Create access key
and leave this page open. We'll need the key shortly.[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
"AllowedOrigins": ["*"],
"ExposeHeaders": []
}
]
.env
into your project at core/.env
AWS_ACCESS_KEY_ID=123123-eefse-your-key
AWS_SECRET_ACCESS_KEY=123123-eefse-another-key
BUCKET_NAME=your-bucket-name
Note: these keys allow you to access your aws account. Don't upload them to github. If you want to share your repo, add your .env
file to a .gitignore
file to make git ignore the file with your keys.
core/settings.py
Β to load the AWS keys from yourΒ .env
Β file into your Django app as environment variables when you run your Django server.from pathlib import Path
from dotenv import load_dotenv
import os
load_dotenv()
# The below are optional checks that we've connected the keys.
if not os.getenv('AWS_ACCESS_KEY_ID'):
print('Missing your AWS_ACCESS_KEY_ID')
if not os.getenv('AWS_SECRET_ACCESS_KEY'):
print('Missing your AWS_SECRET_ACCESS_KEY')
if not os.getenv('BUCKET_NAME'):
print('Missing your BUCKET_NAME')
sim/services.py
, create a view to handle generating a presigned URL:import os
import boto3
def generate_presigned_post(bucket_name, filename, expiration=600):
"""
Docs: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/generate_presigned_post.html#generate-presigned-post
"""
s3_client = boto3.client(
's3',
aws_access_key_id=os.getenv('AWS_ACCESS_KEY_ID'),
aws_secret_access_key=os.getenv('AWS_SECRET_ACCESS_KEY')
)
return s3_client.generate_presigned_post(
Bucket=bucket_name, Key=filename,
ExpiresIn=expiration,
)
sim/views.py
, add:import json
import os
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.shortcuts import render
from django.http import JsonResponse
from .services import generate_presigned_post
@method_decorator(csrf_exempt, name='dispatch')
def uploader(request):
if request.method == 'GET':
return render(request, 'upload.html')
elif request.method == 'POST':
body = json.loads(request.body)
file = body.get('file')
if not file:
return JsonResponse({'error': 'Missing file in request body'}, status=400)
presigned_data = generate_presigned_post(
bucket_name=os.getenv('BUCKET_NAME'), filename=file['name']
)
return JsonResponse(presigned_data)
core/urls.py
:from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('sim.urls')),
]
sim/urls.py
and add:from django.urls import path
from . import views
urlpatterns = [
path('uploader/', views.uploader, name='generate_presigned_url'),
]
sim/templates
sim/templates/upload.html
with the below:<!DOCTYPE html>
<html lang="en" xmlns:x-on="http://www.w3.org/1999/xhtml" xmlns:x-bind="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Upload</title>
<script src="//unpkg.com/alpinejs" defer></script>
</head>
<body>
<div x-data="{ file: null, presignedData: null, message: '' }">
<input type="file" x-on:click="message = ''" x-on:change="getPresignedData">
<button x-on:click="upload" x-bind:disabled="!presignedData">Upload</button>
<div x-text="message"></div>
<script>
async function getPresignedData(event) {
/*
Create a presigned URL for uploading a file to S3.
*/
const files = event.target.files
const file = files[0]
const response = await fetch('/uploader/', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}' },
body: JSON.stringify({ file: {name: file.name, content_type: file.type} })
});
if (!response.ok) {
const data = await response.json();
console.error('Error:', data.error);
}
else{
this.presignedData = await response.json();
}
}
async function upload() {
/*
Upload a file to S3 using the presigned URL.
*/
const formData = new FormData();
for (const [key, value] of Object.entries(this.presignedData.fields)) {
formData.append(key, value);
}
formData.append('file', this.file);
const response = await fetch(this.presignedData.url, { method: 'POST', body: formData });
this.message = response.ok ? 'β
Upload successful' : `β Upload failed: The error message is ${response.statusText}`;
}
</script>
</div>
</body>
</html>
Run your Django server locally from your terminal:
python manage.py runserver
βΆοΈ Navigate to http://127.0.0.1:8000/
in your web browser, select a file, and click the "Upload" button to upload the file directly to your S3 bucket using the presigned URL.
<!DOCTYPE html>
<html lang="en" xmlns:x-on="http://www.w3.org/1999/xhtml" xmlns:x-bind="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Upload</title>
<script src="//unpkg.com/alpinejs" defer></script>
<style>
body {
background-color: #fafafa;
font-family: 'Roboto', sans-serif;
}
.uploader {
padding: 30px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
width: 300px;
margin: auto;
border-radius: 16px;
}
.file-input {
border: 1px solid #e0e0e0;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
width: calc(100% - 24px);
transition: border-color 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
}
.file-input:focus {
border-color: #007aff;
box-shadow: 0 0 0 3px rgba(0,122,255, 0.25);
}
.upload-button {
background-color: #007aff;
padding: 12px 20px;
border-radius: 8px;
transition: background-color 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
width: 100%;
border: none;
}
.upload-button:hover{
}
.upload-button:hover:not(:disabled) {
background-color: #005bb5;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
color: white
}
.upload-button:disabled {
background-color: #e0e0e0;
}
.message {
margin-top: 20px;
font-size: 14px;
color: #2c2c2c;
text-align: center;
}
</style>
</head>
<body>
<div x-data="{ file: null, presignedData: null, message: '' }" class="uploader">
<input type="file" x-on:click="message = ''" x-on:change="getPresignedData" class="file-input">
<button x-on:click="upload" x-bind:disabled="!presignedData" class="upload-button">Upload</button>
<div x-text="message" class="message"></div>
<script>
async function getPresignedData(event) {
/*
Create a presigned URL for uploading a file to S3.
*/
const files = event.target.files
const file = files[0]
const response = await fetch('/uploader/', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}' },
body: JSON.stringify({ file: {name: file.name, content_type: file.type} })
});
if (!response.ok) {
const data = await response.json();
console.error('Error:', data.error);
}
else{
this.presignedData = await response.json();
}
}
async function upload() {
/*
Upload a file to S3 using the presigned URL.
*/
const formData = new FormData();
for (const [key, value] of Object.entries(this.presignedData.fields)) {
formData.append(key, value);
}
formData.append('file', this.file);
const response = await fetch(this.presignedData.url, { method: 'POST', body: formData });
this.message = response.ok ? 'β
Upload successful' : `β Upload failed: The error message is ${response.statusText}`;
}
</script>
</div>
</body>
</html>
Want to build your Django frontend faster? Probably like you, I'm eager to turn ideas into products asap.
Enter Photon Designer: the visual editor that I'm building to craft Django frontends at light-speed (and much faster than a weaver) π‘