Active Storage and React
Tags: rails, javascript, react
Published: 2023-11-29 23:13:48

Let's say you want your user to upload files from your react application, and have it be saved to a table in your rails back end. Thankfully, there's a rails library just for this, and the process is quite simple. Active Storage allows you to upload files (either locally, or to a cloud service like Amazon S3 or google cloud), and attach them to Active Record objects. Today, we'll be using images as an example, and saving them to a user 'profile' object, but Active Storage's uses go far beyond image uploads.

### Installation
Once your application is ready to implement file uploads, in the terminal, navigate to your application folder and run:

```
rails active_storage:install
```
Then:

```
rails db:migrate
```

The first command will add a few new tables to your migrations. Once you migrate these, you should notice that these tables have been added to your schema. These are join tables that you won't directly interact with, but will assist in connecting uploaded files to your models.

### Make models accept files
In our example, we have a 'Profile' model, which belongs to a 'User' model. Our Profile model contains a bio, and we want to add a profile picture to it. Normally, you'd create a migration and add a new column, perhaps for an image url. Here, all we have to do is navigate to our profile model, and call the **has_one_attached** declaration, which functions similarly to other join table declarations like **has_many**. We'll follow this declaration with the word we intend to use as a reference for our files. This could be something like 'track', 'document', or 'photo', but in our case, we'll call it 'avatar'.

```
class Profile < ApplicationRecord
    belongs_to :user
    has_one_attached :avatar
end
```
We'll add **:avatar** to the permitted parameters in the profile controller, and then move on to the front end. But first...

### Quick aside to discuss validations ###
While we're here, let's also write a validation. If we're paying to host these files on a web service, we don't want users to upload files that are too big, as that would drastically inflate the amount of storage space your app is taking.

```
validate :acceptable_image

def acceptable_image
    return unless avatar.attached?
end
```
We start our acceptable_image method with a guard clause that breaks out of the method if there is no image attached to our POST/PATCH request.

Let's look back at the schema for a moment. You'll find a table called "active storage blobs". Imagine this as a hash that contains all relevant information about the file being uploaded. We can use the information in this hash to create our validations. In fact, once you've added the POST/PATCH request to your front end, try temporarily adding the following to the top of your model:
```
ActiveStorage::Blob
```
and then enter the debugger of your choice within the controller's POST or PATCH method. Call your file with .blob at the end (in our case, profile.avatar.blob), and you should see your upload's filename, content type, byte size, and more.

With this knowledge, we can write the following:
```
def acceptable_image
    return unless avatar.attached?

    unless avatar.blob.byte_size <= 3.megabyte
        errors.add(:avatar, "is too large")
    end
end
```
Ensuring our application won't accept avatar images greater than or equal to 3 megabytes.

With this knowledge, we can make another validation! Upload form we're adding to the front end should only accept images, but we should really limit the file type in case someone finds a way to bypass this and add malicious data to our application somehow (can never be too safe!). Maybe we want to limit the types of image files our users can add too! If you were able to check an upload's blob data, you might see that the **image_type** column denotes the file's type using a string formatted as such:
'category/type'
for example: "image/jpeg" or "audio/mp3". with this knowledge, we can add the following to our acceptable_image method:

```
def acceptable_image
    return unless avatar.attached?

    unless avatar.blob.byte_size <= 3.megabyte
        errors.add(:avatar, "is too large")
    end

    image_types = ["image/jpeg", "image/png"]
    unless image_types.include?(avatar.content_type)
        errors.add(:avatar, 'must be jpeg/png')
    end

end
```

### Front End Form

In the front end, we already have a fetch that uploads the profile's bio as json data, but now that we've added image uploading capability to our back end, we're going to need to adjust some things. Here's what that fetch request might look like beforehand:

```
function handleProfileUpdate(e) {
    e.preventDefault()
    fetch(`/profiles/${id}`, {
        method:'PATCH',
        body: JSON.stringify({
            bio
        }),
    })
}
```

This function would a receive a bio saved in state; one theoretically edited by the user through an HTML input. However, now that we've presumably added an html input that accepts images, and saved that image data to state, we can no longer simply turn it into a json. Instead, we'll send it as Form Data, like so:


```
function handleProfileUpdate(e) {
    e.preventDefault()
    const formData = new  FormData()
    if (imageData) {
        formData.append('avatar', imageData)
    }
    if (bio) {
        formData.append('bio', bio)
    }
    fetch(`/profiles/${id}`, {
        method:'PATCH',
        body: formData
    })
}
```

As you can see, here we create a new, blank form, then check state for our image and our bio, appending them to the form data if they exist. Then, we send the data within the body of our fetch request. This should result in a successful upload! After this, we can implement the validations we defined previously, and pass them as errors to the frontend.