Active Storage File Upload Behind The Scenes
Active Storage is a framework in Ruby that makes it a breeze to upload files and reference them in the cloud (or a local disk). It’s built into Ruby On Rails 6, but it’s also got a JavaScript library. In fact, that’s what I like most about Rails. It’s got your back. It delivers complete packages out of the box. Packages that work well together. Client to server — it’s got everything you need! And it’s got beautiful code. Making it a pleasure to work with.
In this post, you will see how Active Storage really works from the inside. We will track the main flow of the program, and see how it processes a file uploaded by the user through the browser with JavaScript. And then, how the file is uploaded to a local disk with Ruby. Let’s get started.
Autostart
It all starts here, with autostart
.This is the function that starts everything in the Active Storage world. It invokes the start()
function (which we’ll look at shortly) if the ActiveStorage
object is present in the browser’s window
. Here it is:
function autostart() { if (window.ActiveStorage) { start() // Imported from ujs.js }}
Then, Active Storage uses a setTimeout
function to automatically call the autostart
function after one millisecond:
setTimeout(autostart, 1)
Start
So what does start
do? Looking at the start
function, first it’s doing a check to make sure the application hasn’t already started, and if so, it makes a note of it by setting started
to true
. The next thing it does is to attach an event listener to the submit
event with the handler didSubmitForm
. That means that didSubmitForm
will run every time the submit
event is triggered by the user. The event will be triggered after the user has submitted the form with the file they chose to upload. That’s what it looks like:
export function start() { if (!started) { started = true // ... document.addEventListener("submit", didSubmitForm) }}
Did Submit Form
Once a user has uploaded a file, Active Storage will run the handler didSubmitForm
. Which in turn will pass the event to handleFormSubmissionEvent
, and invoke (call) it.
function didSubmitForm(event) { handleFormSubmissionEvent(event)}
Handle Form Submission Event
What’s the handleFormSubmissionEvent
function all about? It’s creating an instance of the DirectUploadsController
with the specific form the user has submitted. And it’s starting it with controller.start()
. This controller’s API is such a delight!
function handleFormSubmissionEvent(event) { // ... const controller = new DirectUploadsController(form)
controller.start()}
Direct Uploads Controller
In the Direct Uploads controller Active Storage creates a new instance of the DirectUpload
JavaScript model, passing it the file the user is uploading, its url, and an instance of the controller class.
export class DirectUploadsController { this.directUpload = new DirectUpload(this.file, this.url, this)}
Direct Upload
It’s in this model that the core of the process is taking place. It does two things: It creates a fresh new blob instance from the BlobRecord
model, which saves the meta data of the file the user is uploading in the database without actually uploading the file to the disk. And then, it calls create()
on the blob instance and passes it a callback. It’s in this callback that the actual file upload happens.
The way it does that is by creating a new upload instance from the BlobUpload
model class passing it the blob in question.
export class DirectUpload { create() { // ... const blob = new BlobRecord(this.file, checksum, this.url) blob.create( => const upload = new BlobUpload(blob) ) }}
Blob Record
The BlobRecord
model actually creates an AJAX POST
request to the server. From here the Rails controller in the back-end handles the request.
export class BlobRecord { constructor {
// ... this.xhr = new XMLHttpRequest this.xhr.open("POST", url, true) } create() { this.xhr.send() }}
Direct Uploads Controller
Active Storage has its own base controller class that inherits directly from ActionController::Base
. It’s called ActiveStorage::BaseController
, which just keeps things clean with one root controller for the back-end of the framework.
The controller calls create_before_direct_upload
which just calls create, and creates a new instance of the ActiveStorage::Blob
back-end model in Ruby. Here it’s essentially writing the meta data of the file uploaded to the database. The server returns some JSON with the url and the service headers for direct upload.
class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController def create blob = ActiveStorage::Blob.create_before_direct_upload! (blob_args) render json: direct_upload_json(blob) end
end
Blob Upload
This is the second AJAX request it makes to the server. This time it’s a PUT
request which will go directly to the DiskController
in the back-end which in turn uploads the file to the service (in this case a local disk).
export class BlobUpload { constructor { this.xhr = new XMLHttpRequest this.xhr.open("PUT", url, true) } create() { this.xhr.send(this.file.slice()) }}
Disk Controller
Finally. That actual file upload to the disk happens here. The controller responds with an update
action which will delegate to ActiveStorage::Blob
and calls upload on the service. At this point the process is finalized and the file is present on the service.
class ActiveStorage::DiskController < ActiveStorage::BaseController def update ActiveStorage::Blob.service.upload endend
Focusing on the main flow
Of course there’s a lot more to the Active Storage file upload process, and I simplified things a little bit. But I just wanted to get a feel of what the main flow of the program looks like. This framework is HIGHLY documented so I suggest you take it for a spin yourself. Enjoy!
—
For more posts, subscribe to That Weekly Tech.