In Part 1 we created a Rails app with ActiveStorage and S3. We also built an uploadzone component that can handle the files and prepare to pass them off to something to upload them. That's where we're going to begin.
The S3 Direct Upload Process
The process of uploading a file to S3 is a little more complicated than just sending a file to S3. The file needs to be signed by AWS, and then sent to S3.
Luckily, Rails makes this way easier by providing you with a URL to use that will give you the presigned URL. In fact Rails provides javascript library that abstracts the entire two step process of getting the signed URL and then sending the file along that URL to s3 into a super easy to use library, @rails/activestorage
.
The ActiveStorage Direct Upload Process
The first step is to add the @rails/activestorage
library to your project. There are instructions in the Direct Upload section of the npm package docs. Since our example application is using importmaps, we'll pin the library. Run the following:
./bin/importmap pin @rails/activestorage
A Quick and Dirty Upload
Let's make sure everything is working and just get uploading working and then we will refactor it and add the ability to see live progress. The API of the library is pretty easy.
const upload = new DirectUpload(file, url);
The file
is the file you want to upload, and the url
is the static URL that Rails gives you to get the presigned URL, as far as I can tell, it's always /rails/active_storage/direct_uploads
.
Instances of this DirectUpload
object have a create that handles the actual uploading and accepts a callback for what you want to do after the upload is complete. Let's just put a debugger in there and put it all together.
In our controller, we're going to add an uploadFile
method that will accept each file and upload it via the DirectUpload process described above.
First, import the library to the controller. Then add the method.
app/javascript/controllers/uploadzone_controller.js
import { Controller } from "@hotwired/stimulus";
import { DirectUpload } from "@rails/activestorage";
export default class extends Controller {
// Rest of class
uploadFile(file) {
const upload = new DirectUpload(file, '/rails/active_storage/direct_uploads');
upload.create((error, blob) => {
if (error) {
// Handle the error
} else {
debugger;
}
});
}
}
Now let's pass each file from acceptFiles
to this method and see what happens.
app/javascript/controllers/uploadzone_controller.js
acceptFiles(event) {
event.preventDefault();
const files = event.dataTransfer ? event.dataTransfer.files : event.target.files;
[...files].forEach((f) => {
this.uploadFile(f);
});
}
With that you should be able to try the application and end up in a debugger with access to the blob
from the upload.
If you look at the blob in console, you'll see.
We did it! The file has been uploaded to S3 and Rails even created the ActiveStorage instance and saved it for us. Which is to say, this file is in our database. All we need to do now is attach it to our track!
Attaching the File to the Track
While we created an attachment, we still have to create the Track that has that attachment.
What we want to do is send a request to our server to attach the blob to the track. We'll do this by sending a POST request to /tracks
with the signed_blob_id
, essentially a unique identifier with which to identify the attachment, along with the filename
and allow our controller action to create the post and attach the file.
Let's get our form data ready to send to the server.
const trackData = {track: {filename: blob.filename}, signed_blob_id: blob.signed_id}};
We're basically setting up the params
object that we want to submit to tracks#create
in our Rails application.
Rails gives you a great library for making requests to your server, @rails/request.js
. We'll use that to make the request.
Let's pin that to our application too.
./bin/importmap pin @rails/request.js
If we import the library, we can use it to make a request to our server.
app/javascript/controllers/uploadzone_controller.js
import { Controller } from "@hotwired/stimulus";
import { DirectUpload } from "@rails/activestorage";
import { post } from "@rails/request.js";
We now have a post
function that we can use to submit post requests via AJAX and not have to worry about CSRF and other rails security stuff.
app/javascript/controllers/uploadzone_controller.js
uploadFile(file) {
const upload = new DirectUpload(file, "/rails/active_storage/direct_uploads");
upload.create((error, blob) => {
if (error) {
// Handle the error
} else {
const trackData = { track: { filename: blob.filename }, signed_blob_id: blob.signed_id };
post("/tracks", {
body: trackData,
contentType: "application/json",
responseKind: "json",
});
}
});
}
We're taking the trackData, submitting it as JSON, and expecting a JSON response. We're not doing anything with the response yet, but we will in a minute. Let's see if this submits the request and then jump to the Rails backend.
That worked! We submitted a POST
request to /tracks
with the trackData
as the body. We can now jump to the Rails backend and handle this request.
Creating the Track
First let's create the correct route as the attempt resulted in a 404 above.
Add to config/routes.rb
post "tracks", to: "tracks#create"
Then, and I'm moving through the rails part a little fast, let's create the controller action.
app/controllers/tracks_controller.rb
def create
@track = Track.new(track_params)
@track.audio_file.attach(params[:signed_blob_id])
if @track.save
render json: @track, status: :created
end
end
private
def track_params
params.require(:track).permit(:title, :artist_name, :filename)
end
The key line here is @track.audio_file.attach(params[:signed_blob_id])
, which will attach the file to the track using the signed_blob_id
that we sent in the request.
If we do all that and then try to upload a file, we'll see that it returned a valid JSON response of the newly created track, which is to say, we did it.
We can even go into our console and get the URL of the track that was just uploaded.
Recap
In this post we focused on uploading the track to S3 and creating track record in our database as well as attaching the uploaded audio file to the track as an attachment.
We did this by:
- Used
DirectUpload
from@rails/activestorage
to upload the file to S3. - Constructed a
trackData
object that we could send to our Rails backend using theblob
object returned from `DirectUpload. - Used
post
from@rails/request.js
to submit aPOST
request to our Rails backend with thetrackData
- Built a
create
action in our Rails controller that takes the data and creates a Track with it. It uses thesigned_blob_id
to attach theaudio_file
to the track.
Wow! Really awesome. The final step that we'll cover in part 3 is adding progress to the uploads and replacing them with an audio player when the track is done uploading.