Try   HackMD

Using S3 for image upload with Flask

Setup

Follow these instructions 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, again stopping after you finish the On AWS S3 Console section.

Finally, use pipenv to install the boto3 library in your project folder.

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

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, 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.

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.

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:

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.

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.

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 to see this code in context.

Credits

Inspired by this boto3 tutorial, as well as this tutorial that uses Express.