DEV Community

Cover image for Canvas to S3 Buckets with Rails and React
benjaminolmsted
benjaminolmsted

Posted on

Canvas to S3 Buckets with Rails and React

So you have a nice drawing on your beautiful HTML canvas. How can we persist our image? Save it to our database? Save it to our server? Well I'm using Heroku, so saving it to the server is a non starter (they use ephemeral file systems which will periodically wipe all the data that isn't in your slug). I also didn't want to put a blob or large binary file in my database. So I ended up storing the canvas contents as a PNG on Amazon's S3 cloud storage. I wanted to upload directly from the front-end so the basic flow of the app will be:

+User clicks save
+We send a request to the backend for a pre-signed URL that will let us upload to our S3 bucket
+We use an AWS gem to create the pre-signed URL and create a GET URL
+We send both the pre-signed URL and GET URL to the React front end
+Then we turn our canvas into a dataURL png and then into a blob png
+We POST that blob to the presigned URL and it gets uploaded to our S3 bucket
+On success, we POST the GET URL to our backend and create a resource that stores it

To implement client-side uploading to S3, there are 8 steps necessary to get programmatic uploading to S3 to work with a Rails backend and React front end. I'll outline them here, but I strongly recommend heading over to https://betterprogramming.pub/uploading-files-directly-to-aws-in-a-rails-react-app-9188f4eb6f7e for the walk through. I followed the steps closely, and got the process to work. In this example, an uploaded file was being saved, which is a slightly different process than saving from canvas. I'll go over those differences in detail. By the end we will be able to save our canvas contents to S3 and save the URL in our database for later access.

Steps 1-4
Setup an S3 Bucket to receive the images. This requires getting a FREE developer trial account, setting Bucket policies, Access Control Lists, CORS, and and creating a user with Access Key and a secret key for the bucket.

Step 5
Add the aws gem 'aws-sdk' ~3 NOTE: amazon has updated since the original blog post was written. Use version 3.

Add your keys to a .local_env.yml file and load it in applicaiton.rb

Step 6

Create an initializer

Step 7
Make a controller to handle the pre-signed URL creation

Step 8
Get the pre-signed url to the frontend and upload the image to S3, save the url where the image was posted.

This is where I diverged from the original blog.

saveImage

  async function saveImage(){        
        let canvas = ref.current
        let imageURL = canvas.toDataURL()
        const blob = dataURItoBlob(imageURL)
        const getURL = await uploadToAWS(blob, "postcards")
        const response = await fetch("/postcards",
                                    {method: "POST", 
                                    headers: {'Content-Type': 'application/json'}, 
                                    body: JSON.stringify({image_url: getURL, user_id: user.id})})
        const postcard = await response.json()
        setPostcards([...postcards, postcard]) 
}
Enter fullscreen mode Exit fullscreen mode

here we get the data from our canvas into an image/png, base64 string via canvas.toDataURL().
Then we convert it into a blob with a handy function we found on stack overflow. I know that it works to convert the base64 string into a binary blob. How, well, I'm not sure. The code is below.
next we pass the blob to our uploadToAWS function, which does the magic and returns us a GET URL, the place the file is stored.
Then we POST to our backend, creating a postcard with the GET URL.
Lastly, we set state, adding the newly created postcard to our list of postcards.

The postcards_controller.rb

def create
        postcard = Postcard.create(postcard_params)
        render json: postcard
    end

    private
    def postcard_params
       params.permit(:image_url, :user_id)
    end
end
Enter fullscreen mode Exit fullscreen mode

dataURItoBlob:

    function dataURItoBlob(dataURI) {
        var binary = atob(dataURI.split(',')[1]);
        var array = [];
        for(var i = 0; i < binary.length; i++) {
            array.push(binary.charCodeAt(i));
        }
        return new Blob([new Uint8Array(array)], {type: 'image/png'});
    }
Enter fullscreen mode Exit fullscreen mode

here is the uploadToAWS function:

 const uploadToAWS = async(blob, directory) => {
        const  data  = await fetch(`/presign?filename=postcard&fileType=image/png&directory=${directory}`, 
                                    {method: "GET"})
        const json = await data.json();
        const { post_url, get_url } = json;
        const awsResp = await fetch(post_url, 
                                    {method: 'PUT', 
                                    headers: {"Content-Type": 'image/png','acl': 'public-read'}, 
                                    body: blob})
        return get_url;
  }
Enter fullscreen mode Exit fullscreen mode

First we get out pre-signed url from the backend, along with the location where we will upload the file to.
Then we PUT the blob to the pre-signed url post_url
The we return the location of the file in get_url

And just like that, our pretty pictures are in the S3 bucket, waiting to be served, and we have their location saved to our database.

Alt Text

Top comments (2)

Collapse
 
bruzuhq profile image
Image Generation API

Why don't just render the canvas at server side?

Collapse
 
benjaminolmsted profile image
benjaminolmsted

I didn't want to store blobs in the database, and heroku uses an ephemeral file system, so I couldn't store the images there.