# Using S3 for image upload with Flask
## Setup
Follow [these instructions](https://github.com/jamesurobertson/aws-s3-pern-demo#create-your-aws-user-and-bucket) to create your aws user and bucket, and obtain your credentials (stop after the __Create your AWS User and Bucket__ section). You will need these credentials in subsequent steps to set up your environment.
You will also need to set up your bucket so that files can be publicly accessed—follow [these instructions](https://github.com/jamesurobertson/aws-s3-pern-demo#public-file-read-configuration), again stopping after you finish the __On AWS S3 Console__ section.
Finally, use pipenv to install the `boto3` library in your project folder.
```shell
pipenv install boto3
```
## Configuration
Put the name of your bucket, along with the Access Key ID and your Secret Access Key your `.env` file. __Make sure you include your `.env` in your `.gitignore`__. You really don't want to push this information to github.
```
S3_BUCKET=<your bucket name>
S3_KEY=<Access key Id>
S3_SECRET=<Secret access key>
```
## AWS Upload
Create a file for AWS upload functionality. You will need to import `boto3` and `botocore` to implement your s3 functionality. You will also have to get your S3 values from the environment
```python=
import boto3
import botocore
import os
s3 = boto3.client(
"s3",
aws_access_key_id=os.environ.get("S3_KEY"),
aws_secret_access_key=os.environ.get("S3_SECRET")
)
```
### Filenames
Your S3 bucket cannot have two files with the same filename—if you upload two files with the same name one will get overwritten. We can avoid issue that by generating unique names every time we upload a file.
We can generate unique filenames using a [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier), and, specifically the `uuid` module in Python. We can also limit the types of files users can upload in this step.
Here are the helper functions we'll use to get filenames.
```python=
import uuid
ALLOWED_EXTENSIONS = {"pdf", "png", "jpg", "jpeg", "gif"}
def allowed_file(filename):
return "." in filename and \
filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
def get_unique_filename(filename):
ext = filename.rsplit(".", 1)[1].lower()
unique_filename = uuid.uuid4().hex
return f"{unique_filename}.{ext}"
```
### Upload Helper
Now let's write the function that we'll need to actually upload the file—and return the url if we're successful.
```python=
BUCKET_NAME = os.environ.get("S3_BUCKET")
S3_LOCATION = f"http://{BUCKET_NAME}.s3.amazonaws.com/"
def upload_file_to_s3(file, acl="public-read"):
try:
s3.upload_fileobj(
file,
BUCKET_NAME,
file.filename,
ExtraArgs={
"ACL": acl,
"ContentType": file.content_type
}
)
except Exception as e:
# in case the our s3 upload fails
return {"errors": str(e)}
return {"url": f"{S3_LOCATION}{file.filename}"}
```
So, at this point, our file looks like this:
```python=
import boto3
import botocore
import os
import uuid
BUCKET_NAME = os.environ.get("S3_BUCKET")
S3_LOCATION = f"https://{BUCKET_NAME}.s3.amazonaws.com/"
ALLOWED_EXTENSIONS = {"pdf", "png", "jpg", "jpeg", "gif"}
s3 = boto3.client(
"s3",
aws_access_key_id=os.environ.get("S3_KEY"),
aws_secret_access_key=os.environ.get("S3_SECRET")
)
def allowed_file(filename):
return "." in filename and \
filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
def get_unique_filename(filename):
ext = filename.rsplit(".", 1)[1].lower()
unique_filename = uuid.uuid4().hex
return f"{unique_filename}.{ext}"
def upload_file_to_s3(file, acl="public-read"):
try:
s3.upload_fileobj(
file,
BUCKET_NAME,
file.filename,
ExtraArgs={
"ACL": acl,
"ContentType": file.content_type
}
)
except Exception as e:
# in case the our s3 upload fails
return {"errors": str(e)}
return {"url": f"{S3_LOCATION}{file.filename}"}
```
### On The Route
For the purposes of this tutorial, let's assume we have an Image model in our database with a column for the image url and a column for the id of the user who uploaded the image. After we've successfully uploaded the image to S3, we can store the returned URL in our database.
```python=
from flask import Blueprint, request
from app.models import db, Image
from flask_login import current_user, login_required
from app.s3_helpers import (
upload_file_to_s3, allowed_file, get_unique_filename)
image_routes = Blueprint("images", __name__)
@image_routes.route("", methods=["POST"])
@login_required
def upload_image():
if "image" not in request.files:
return {"errors": "image required"}, 400
image = request.files["image"]
if not allowed_file(image.filename):
return {"errors": "file type not permitted"}, 400
image.filename = get_unique_filename(image.filename)
upload = upload_file_to_s3(image)
if "url" not in upload:
# if the dictionary doesn't have a url key
# it means that there was an error when we tried to upload
# so we send back that error message
return upload, 400
url = upload["url"]
# flask_login allows us to get the current user from the request
new_image = Image(user=current_user, url=url)
db.session.add(new_image)
db.session.commit()
return {"url": url}
```
## Sending images from the frontend
Here is a simple example of a component for users to upload images. You will undoubtedly need to modify this for your usage, but make sure that the name of the field you attach to your `FormData` object matches what you are looking for on the backend end (i.e. the name in `formData.append("<some name>", image);` should match `image = request.files["<some name>"]`).
Note that you must NOT set the `Content-Type` header on your request. If you leave the `Content-Type` field blank, the `Content-Type` will be generated and set correctly by your browser (check it out in the network tab!). If you include `Content-Type`, your request will be missing information and your Flask backend will be unable to locate the attached files.
```javascript=
import React, {useState} from "react";
import { useHistory } from "react-router-dom";
const UploadPicture = () => {
const history = useHistory(); // so that we can redirect after the image upload is successful
const [image, setImage] = useState(null);
const [imageLoading, setImageLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData();
formData.append("image", image);
// aws uploads can be a bit slow—displaying
// some sort of loading message is a good idea
setImageLoading(true);
const res = await fetch('/api/images', {
method: "POST",
body: formData,
});
if (res.ok) {
await res.json();
setImageLoading(false);
history.push("/images");
}
else {
setImageLoading(false);
// a real app would probably use more advanced
// error handling
console.log("error");
}
}
const updateImage = (e) => {
const file = e.target.files[0];
setImage(file);
}
return (
<form onSubmit={handleSubmit}>
<input
type="file"
accept="image/*"
onChange={updateImage}
/>
<button type="submit">Submit</button>
{(imageLoading)&& <p>Loading...</p>}
</form>
)
}
export default UploadPicture;
```
## Working Demo
Check out [this repo](https://github.com/jshafto/widget_app) to see this code in context.
## Credits
Inspired by [this boto3 tutorial](https://www.zabana.me/notes/flask-tutorial-upload-files-amazon-s), as well as [this tutorial](https://github.com/jamesurobertson/aws-s3-pern-demo) that uses Express.