Storing Multiple Image Files in Amazon S3 using Rails Active Storage and React.js

drayeleo

Dylan Raye-Leonard

Posted on July 22, 2022

Storing Multiple Image Files in Amazon S3 using Rails Active Storage and React.js

In conversations with seasoned professionals in the software engineering field I have been told time and again that the cloud is the dominant computer technology of our time. From basic consumer-facing storage services such as Google Drive or OneDrive to lightning-fast, pay-by-the-minute computing services for large dataset analysis, cloud computing has become the central piece of infrastructure in the modern tech world. Its advantages are clear; as Microsoft's Azure website states, "cloud computing is the delivery of computing services—including servers, storage, databases, networking, software, analytics, and intelligence—over the Internet (“the cloud”) to offer faster innovation, flexible resources, and economies of scale. You typically pay only for cloud services you use, helping you lower your operating costs, run your infrastructure more efficiently, and scale as your business needs change."

For my final project with Flatiron School I wanted to engage with this massive aspect of modern software engineering in some way, and using Amazon's S3 (Simple Storage Service) was the perfect introduction. S3 is simple, has a functional free tier, and serves as an entryway into AWS, the current dominant cloud services provider. Below is a walkthrough of how I set up S3 image storage in my react/rails application. Importantly, this guide details storing multiple images associated with a single record, in contrast to storing a single image as is detailed in the majority of the guides that I found (examples here and here).

This guide assumes that you have already set up a web app with React frontend, Rails backend, and a Rails ActiveRecord model to which you would like to attach images (I will be attaching the images to a User model)

A word of warning: I found that the rails "debug" gem did not work well when checking whether ActiveStorage had successfully created a file or when generating a temporary url for a file, likely due to lazy loading or a similar feature. I would recommend using rails console in lieu of debugger wherever possible to avoid these issues.

Uploading images

First, image files need to be uploaded to the front end so that the client can package them to send to the server. To accomplish this, I used code from this Stack Overflow answer but modified it by adding the multiple attribute and saving the images in a stateful array.

# client/src/components/uploadAndDisplayImage.js
import React, { useState } from "react";

const UploadAndDisplayImage = () => {
  const [selectedImages, setSelectedImages] = useState([]);

  function displayUploadedImage() {
    if (selectedImages[0]) {
      return selectedImages.map((image, index) => {
        return (
          <div key={index}>
            <img
              alt="not found"
              width={"250px"}
              src={URL.createObjectURL(image)}
            />
            <br />
            <button
              onClick={() =>
                setSelectedImages((selectedImages) => {
                  return selectedImages.filter((_, i) => i !== index);
                })
              }
            >
              Remove
            </button>
          </div>
        );
      });
    } else {
      return null;
    }
  }

  function handlePhotoSubmit() {
    const formData = new FormData();

    selectedImages.forEach((image, index) =>
      formData.append(`images[]`, image)
    );

    for (const value of formData.values()) {
      console.log(value);
    }

    fetch("/user-image", {
      method: "POST",
      body: formData,
    })
      .then((response) => response.json())
      .then((data) => {
        console.log(data);
      })
      .catch((error) => console.log({ error: error }));
  }

  return (
    <div>
      <h1>Upload and Display Image</h1>
      {displayUploadedImage()}
      <br />
      <br />
      <input
        type="file"
        name="myImage"
        multiple
        onChange={(event) => {
          console.log(event.target.files);
          setSelectedImages(Array.from(event.target.files));
          console.log(Array.from(event.target.files));
        }}
      />
      <br />
      <br />
      <button onClick={handlePhotoSubmit}>Submit</button>
    </div>
  );
};

export default UploadAndDisplayImage;
Enter fullscreen mode Exit fullscreen mode

This code allows for multiple image files to be uploaded, stored in a stateful array, and displayed on the page. When the Submit button is clicked, a formData object is created (see MDN for more info about formData), the images are appended to it, and the object is sent to the back end using a POST request.

Setting up S3

To set up the S3 bucket in which the images will be stored I largely followed this honeybadger.io article. Rather than replicate all of those steps here, I recommend following Jeff's guide, stopping at the "Scoping To a User" header. The second half of his guide involves using the devise gem to quickly create a "User" model in ActiveRecord for example purposes, but is not applicable when building that model from scratch.

Saving images to S3

Once the bucket is set up and Active Storage is configured, actually attaching photos is as simple as:

# app/models/user.rb
has_many_attached :images
Enter fullscreen mode Exit fullscreen mode
# app/controllers/users_controller.rb
def add_images
  user = User.find_by(id: session[:user_id])
  user.images.attach(params[:images])
end
Enter fullscreen mode Exit fullscreen mode
# config/routes.rb
post "/user-image", to: "users#add_images"
Enter fullscreen mode Exit fullscreen mode

Retrieving images from S3

Finally, retrieving images from S3 consists of generating an array of image urls in the image_urls method, then including that array in the data returned from a GET request:

# app/models/user.rb
def image_urls
  images.map do |image|
    Rails.application.routes.url_helpers.rails_blob_path(image, only_path: true)
  end
end
Enter fullscreen mode Exit fullscreen mode
# app/serializers/user_serializer.rb
attributes :id, :first_name, :last_name, :username, :image_urls
Enter fullscreen mode Exit fullscreen mode

The image urls generated in this fashion allow for temporary access to the image files stored in the S3 bucket, and they expire after a short time.

Configuring ActiveStorage/troubleshooting

Most of the configuration for ActiveStorage should be complete after walking through Jeff's guide. However, I added the following code while debugging errors. This may or may not be necessary depending on your system settings, but if in doubt, adding it shouldn't cause any trouble.

adding req.path.exclude?('rails/active_storage') in the following:

# config/routes.rb

# direct all non-backend routes to index.html
get "*path",
  to: "fallback#index",
  constraints: ->(req) { !req.xhr? && req.format.html? && 
  req.path.exclude?('rails/active_storage')}
Enter fullscreen mode Exit fullscreen mode

Make sure that you specify the bucket by name only:

# config/storage.yml
bucket: capstone-2-drayeleo # be sure to delete <%= Rails.env %> from this line
Enter fullscreen mode Exit fullscreen mode

Conclusion

There you have it! Image files should now be able to be saved to your S3 bucket, with ActiveStorage doing most of the heavy lifting for you.

Additional Resources

Rails Edge Guides Active Storage Overview

Note: cover image sourced from BlueBash Blog

💖 💪 🙅 🚩
drayeleo
Dylan Raye-Leonard

Posted on July 22, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related