Uploading Images to Amazon S3
Learning Objectives
Students will be able to: |
---|
Set up Amazon S3 to hold an app's uploaded images |
Use an HTML <form> to send a file to the server |
Upload a file to S3 from the server |
Roadmap
- Intro
- Ready the Starter Code
- Set up Amazon S3
- Add a
Photo
Model - Add the
add_photo
URL - Add the
add_photo
View Function - Update the details.html Template
- Test it Out!
- Check it Out in Admin
Intro
Many applications we develop benefit from the ability of its users being able to upload and display images.
For example, an application that tracks a user's hikes would allow the user to upload photos of those hikes. Or better yet, our Cat Collector app can allow images to be uploaded for a cat!
This lesson will cover how to add this ability to your Django project should you choose to.
Ready the Starter Code
This lesson's starter code picks up from the many-to-many models lesson.
Once inside the catcollector directory, spin up the Django development server:
$ python3 manage.py runserver
Set up Amazon S3
AWS (Amazon Web Services) is a cloud platform offering a slew of web services such as compute power, database storage, content delivery and more.
You can see all of the services available by clicking here Amazon AWS.
The specific web service we are going to use to host our uploaded images is the Simple Storage Service, better known as Amazon S3.
Set up an Amazon AWS Account
Before we can use S3, or any Amazon Web Service, we'll need an Amazon AWS account.
Notice: Even though AWS has a "free tier" and we will do nothing in SEI that will cost money, AWS requires a credit card to open an account. If you don't wish to provide a credit card, you will not be able to complete this lesson and you won't be able to upload images using Amazon Web Services. Unfortunately, alternative services such as Microsoft Azure have the same credit card requirement.
Click the orange "Sign in to the Console" button, then click "Create a new AWS account".
Unfortunately, the signup process is a bit lengthy...
Sign in to the AWS Console
After you have signed up, log in to the Console.
Click the > All services
drop-down.
The first thing we're going to do is create access keys to access S3 with.
We will be obtaining two keys: An Access Key ID and a Secret Access Key.
Locate the "Security, Identity & Compliance" section and click IAM:
Click "Users" in the sidebar:
Click the "Add user" button:
Enter a user name and select "Programmatic access":
Click the "Next: Permissions" button.
Now we need to create a group that will have S3 permissions.
Click the "Create group" button.
Enter a "Group name" - django-s3-assets
is fine. Then scroll way down and select the "AmazonS3FullAccess" checkbox:
Click the "Create group" button (bottom-right).
Click the "Next: Review" button (bottom-right).
Then click the "Create user" button (bottom-right):
Copy both access keys to a safe place - we'll need them later in the lesson.
Click the "Close" button (bottom-right).
Now click the "Services" drop-down (top-left):
Click the S3
link under the "Storage" section.
Create an S3 Bucket for the catcollector
App
Buckets are containers for stuff you store in S3.
Typically, you would want to create an S3 bucket for each web application you develop that needs to use S3.
Let's click the blue button to get started:
You'll have to enter a globally unique Bucket name. I'm willing to sell "catcollector" to the highest bidder :)
Then select the nearest Region as follows:
For the best performance, always be sure to select the nearest location to where most of your users will be. For this lesson, be sure to select the Region nearest you.
Click the Next button (bottom-right of popup) - TWICE to get to this screen:
Be sure to unselect all four checkboxes as shown above.
Click Next, then a new screen will appear where you will click the Create Bucket button!
The bucket has been created!
However, we need to make sure that web browsers will be able to download cat images when using catcollector.
To do so, we need to specify a "bucket policy" that enables read-only access to a bucket's objects.
Click on the bucket name:
Click the "Permissions" tab at the top:
Click on the square "Bucket Policy" button.
Then at the bottom click the "Policy generator" link:
A new tab will open in your browser.
Start entering the data as follows. BTW, that's a *
in the "Principle" input.
This next one's a bit challenging. In the "Amazon Resources Name (ARN)" input enter this:
arn:aws:s3:::catcollector/*
but substituting your bucket name for catcollector
.
Click "Add Statement"
Once that is done, click the "Generate Policy" button.
Copy the text inside the box, including the curly braces:
Now go back to the main tab and paste in that text:
Click the "Save" button (top-right).
You should see something like this at the top of the page:
Congrats! You now have an S3 bucket that, using the access keys, an application can upload files of any type to; that also allows any browser to download by using a known URL.
Now let's get on with the app!
Install & Configure - Boto 3
Install boto3
The official Amazon AWS SDK (Software Development Kit) for Python is a library called Boto 3.
Let's install it:
$ pip3 install boto3
Configure Credentials
Do not ever put your Amazon AWS, or any other secret keys, in your source code!
If you do, and push your code to GitHub, it will be discovered within minutes and could result in a major financial liability!
Have I scared you? Good...
During development (not in a deployed app), boto3 will automatically look in a special file for your AWS keys.
First let's create the folder it needs:
$ mkdir ~/.aws
Now let's create the file:
$ touch ~/.aws/credentials
Note that there is no file extension on the credentials file.
Now let's open it and put our keys in there:
$ code ~/.aws/credentials
Then type the following in the file, substituting your real keys:
[default]
aws_access_key_id=YOUR_ACCESS_KEY
aws_secret_access_key=YOUR_SECRET_KEY
If you use AWS S3 in your project 3 - when deploying your projects next week, you will need to set these same keys on Heroku using
heroku config:set
, however, the key names will need to be CAPITALIZED when setting them on Heroku.
Add a Photo
Model
The Relationship
We're going to add a Photo
Model that will hold the URL of a cat image in S3.
The relationship looks like this: Cat -----< Photo
How does the above relationship "read"? (Click for Answer)
A Cat has many Photos -and- A Photo belongs to a Cat
Define the Photo
Model
To the models.py file we go:
class Photo(models.Model):
url = models.CharField(max_length=200)
cat = models.ForeignKey(Cat, on_delete=models.CASCADE)
def __str__(self):
return f"Photo for cat_id: {self.cat_id} @{self.url}"
Nice and simple!
Reminder: If a Model "belongs to" another Model, it must have a foreign key. More than one "belongs to" relationship - means more than one foreign key.
What needs to be done now?
$ python manage.py makemigrations
$ python manage.py migrate
Add the add_photo
URL
We need to add a new path()
to the urlpatterns
list that will match the request sent when the user submits a photo.
In urls.py:
urlpatterns = [
...
path('cats/<int:pk>/delete/', views.CatDelete.as_view(), name='cats_delete'),
path('cats/<int:cat_id>/add_feeding/', views.add_feeding, name='add_feeding'),
# new path below
path('cats/<int:cat_id>/add_photo/', views.add_photo, name='add_photo'),
]
Pretty much like the add_feeding
route!
Notice once again, we're going to capture the cat's id
using a URL parameter named cat_id
.
The server currently shows an error because we've referenced an add_photo
view function that doesn't exist. Let's take care of that next...
Add the add_photo
View Function
Where do we define the view functions?(Click for Answer)
views.py
First, we need to import three more things:
- The
boto3
library - The
Photo
Model - Python's
uuid
utility that will help us generate random strings
# views.py
from django.shortcuts import render, redirect
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from .forms import FeedingForm
# new/updated imports below
import uuid
import boto3
from .models import Cat, Toy, Photo
Next, we're going to define a couple of variables we'll use in the view function.
First, find the endpoint for your the region you selected when you created the bucket on the following list:
The above screenshot came from this AWS Regions and Endpoints reference
Use the format of the endpoint below as a guide on how to enter yours.
from .forms import FeedingForm
# Add these "constants" below the imports
S3_BASE_URL = 'https://s3-us-west-1.amazonaws.com/'
BUCKET = 'catcollector'
Make sure that you use YOUR S3 bucket name instead of catcollector
.
We'll be using S3_BASE_URL
, BUCKET
and a randomly generated key to build a unique URL used for uploading to Amazon S3 and for saving in the url
attribute or each Photo
instance.
Finally, this is where the magic happens, we'll review the code as we type it in:
def add_photo(request, cat_id):
# photo-file will be the "name" attribute on the <input type="file">
photo_file = request.FILES.get('photo-file', None)
if photo_file:
s3 = boto3.client('s3')
# need a unique "key" for S3 / needs image file extension too
key = uuid.uuid4().hex[:6] + photo_file.name[photo_file.name.rfind('.'):]
# just in case something goes wrong
try:
s3.upload_fileobj(photo_file, BUCKET, key)
# build the full url string
url = f"{S3_BASE_URL}{BUCKET}/{key}"
# we can assign to cat_id or cat (if you have a cat object)
photo = Photo(url=url, cat_id=cat_id)
photo.save()
except:
print('An error occurred uploading file to S3')
return redirect('detail', cat_id=cat_id)
The
url
has to be unique, otherwise we risk overwriting existing files.
On to the UI...
Update the details.html Template
We're going to need to update the details.html template to:
- Display each image beneath the cat's detail "card" (left column).
- Show a "No Photos Uploaded" message if there are no images for the cat.
- Show a form below the images used to upload photos.
Display Cat Images
We're going to use Django's nifty for...empty
template tags to iterate through each cat's photos like this:
...
</div>
<!-- Insert photo markup below this comment -->
{% for photo in cat.photo_set.all %}
<img class="responsive-img card-panel" src="{{photo.url}}">
{% empty %}
<div class="card-panel teal-text center-align">No Photos Uploaded</div>
{% endfor %}
...
The
for...empty
template tags avoid having to wrap afor...in
loop with anif...else
like we did earlier to display a "No Toys" message.
Let's see how it looks:
Since we haven't uploaded any photos yet, we are seeing the No Photos Uploaded <div>
as expected thanks to the {% empty %}
tag.
Reminder - we don't actually invoke methods within Django template tags. For example, notice that there's no ()
in this line of code:
{% for photo in cat.photo_set.all %}
This is because Django templates automatically call an attribute if it's a function. This can be a problem if you ever need to actually call a function that takes arguments. For example, the following will not work:
{% if len(cat.photo_set.all) > 0 %}
Django templates provide filters for use in both {{ }}
(variable) and {% %}
(tags).
For example, to check the length, you can use the length
filter like this:
{% if cat.photo_set.all|length > 0 %}
Filters can also be used to transform/format data, e.g.,
Form for Uploading Photos
Okay, let's code a <form>
that we can use to upload files to the server:
{% for photo in cat.photo_set.all %}
<img class="responsive-img card-panel" src="{{photo.url}}" />
{% empty %}
<div class="card-panel teal-text center-align">No Photos Uploaded</div>
{% endfor %}
<!-- new code below -->
<form
action="{% url 'add_photo' cat.id %}"
enctype="multipart/form-data"
method="POST"
class="card-panel"
>
{% csrf_token %}
<input type="file" name="photo-file" />
<br /><br />
<input type="submit" class="btn" value="Upload Photo" />
</form>
When using HTML forms to upload files, it's important to add the enctype="multipart/form-data"
attribute to the <form>
tag.
Other than {% csrf_token %}
, it's pretty much a generic form that is typically used to upload files to any web app.
Another refresh:
Looking good...
Test it Out!
Try uploading your favorite cat pic.
Success!
Check it Out in Admin
Don't forget to register the new Photo
model so that you can use the built-in Admin app to add/edit/delete model instances.
Update the admin.py file to look like this:
from django.contrib import admin
# Import your models here
from .models import Cat, Feeding, Photo
# Register you models here
admin.site.register(Cat)
admin.site.register(Feeding)
admin.site.register(Photo)
Then browse to localhost:8000/admin
and click through to view the added photo.
You'll see something like this:
Note how the photo's url is formed.