Storing Multiple Image Files in Amazon S3 using Rails Active Storage and React.js
Dylan Raye-Leonard
Posted on July 22, 2022
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;
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
# app/controllers/users_controller.rb
def add_images
user = User.find_by(id: session[:user_id])
user.images.attach(params[:images])
end
# config/routes.rb
post "/user-image", to: "users#add_images"
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
# app/serializers/user_serializer.rb
attributes :id, :first_name, :last_name, :username, :image_urls
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')}
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
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
Posted on July 22, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.