Canvas to S3 Buckets with Rails and React
Published: 2021-09-30 19:38:02

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

```javascript
  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]) 
}
```
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](https://stackoverflow.com/questions/12883871/how-to-upload-base64-encoded-image-data-to-s3-using-javascript-only?rq=1). 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
```ruby
def create
        postcard = Postcard.create(postcard_params)
        render json: postcard
    end

    private
    def postcard_params
       params.permit(:image_url, :user_id)
    end
end
```

dataURItoBlob: 
```javascript
    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'});
    }
```
here is the uploadToAWS function: 

```javascript
 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;
  }
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/umhw3yz4lowgtvj8t1fn.png)