BLOG

Rails + Shrine + DropzoneJS

Try as I might to find a tutorial on utilizing the Shrine attachment toolkit to perform direct S3 uploads in Rails using the DropzoneJS front end library, I was out of luck at every turn. I ended up locating a solution from a relatively obscure Google Group discussion, which I will essentially transcribe here with a bit more context to assist anyone dealing with the same issue.

Dependencies

gem 'shrine'              # Base attachment toolkit gem
gem 'aws-sdk-s3'          # Facilitate Amazon S3 direct uploads
gem 'dropzonejs-rails'    # Front end helper for file uploads

If you want to use DropzoneJS’ style helpers, you can add that dependency to your application.scss as described in the README. Feel free to ignore this part and write your own styles if you’d prefer.

Configuration

Now that we’ve got the dependencies out of the way, we need to set up our Shrine initializer.

# Require the Shrine S3 plugin to allow it as a storage option
require "shrine/storage/s3"

# Define our S3 options from the application secrets file. These could also come from system environment variables.
s3_options = {
  access_key_id:     Rails.application.secrets.dig(:aws, :s3, :access_key),
  secret_access_key: Rails.application.secrets.dig(:aws, :s3, :secret_key),
  region:            Rails.application.secrets.dig(:aws, :s3, :region),
  bucket:            Rails.application.secrets.dig(:aws, :s3, :bucket),
}

# Set up S3 as the storage mechanism for both the cache and long-term store
Shrine.storages = {
  cache: Shrine::Storage::S3.new(prefix: "cache", **s3_options),
  store: Shrine::Storage::S3.new(prefix: "store", **s3_options),
}

# Inform Shrine that we're using ActiveRecord
Shrine.plugin :activerecord

# Add the capability to generate a route for presigned S3 requests
Shrine.plugin :presign_endpoint

With respect to that last presign_endpoint plugin from Shrine (docs here), we’ll need to add the route to our config/routes.rb file.

Rails.application.routes.draw do
  mount Shrine.presign_endpoint(:cache) => '/presign'
end

Uploader and Model

Now that the setup and configuration is complete, we can get to the meat of the issue! Let’s define our uploader. I’ve created mine in a new app/uploaders directory and called it FileUploader, but you can name the file/class anything you’d like.

class FileUploader < Shrine
end

Then on our model, we just need to let Rails know that we want Shrine to handle an attachment attribute, like so:

class MyModel < ApplicationRecord
  include FileUploader::Attachment.new(:image)
end

HTML

Now in the form for that model, we can hook up our file field. We need a hidden input on the model form_for that we can send the S3 upload information to once the direct upload is complete, and we also need a <form> element outside of our form_for that will house the DropzoneJS file input. Our view will look something like this:

<form action="/" class="dropzone" id="blog-post-image-dropzone"></form>

<hr>

<%= form_for @my_model do |f| %>
  <%= f.hidden_field :image,
    value: @my_model.image_data,
    id: 'image-data__field'
  %>
<% end %>

We set our DropzoneJS form action to / because we are about to override it with our JavaScript, and provide a unique ID to that form so that we can reference it. We add a unique ID to the hidden image field so that we can write our file fields back to the model after the direct upload, and use Shrine’s image_data helper as the value of the field.

JavaScript

The last step is to set up the JavaScript so that everything works together! Here’s what that looks like:

Dropzone.options.blogPostImageDropzone = {
  init: function() {
    // Alias this since it is just my Dropzone object
    myDropzone = this

    // Whenever a file is added, get the presigned S3 URL and attach it
    myDropzone.on('addedfile', function(file) {
      // Set the extension and add a date to avoid any strange caching
      // This options hash is sent to the presign URL to get S3 information
      options = {
        extension: file.name.match(/(\.\w+)?$/)[0],
        _: Date.now()
      };

      // Make the request for the presigned S3 information, then attach it
      $.getJSON('/presign', options, function(result) {
        file.additionalData = result['fields'];
        myDropzone.options.url = result['url'];
        myDropzone.processQueue();
      });
    });

    // Before sending a file, set the formData to the S3 fields stored above
    myDropzone.on('sending', function(file, xhr, formData) {
      $.each(file.additionalData, function(k, v) {
        formData.append(k, v);
      });
    })

    // After a file is successfully uploaded, we need to send it back
    // to the form so that it can be attached to the model
    myDropzone.on('success', function(file) {
      image = {
        id: file.additionalData.key.match(/cache\/(.+)/)[1],
        storage: 'cache',
        metadata: {
          size: file.size,
          filename: file.name,
          mime_type: file.type
        }
      };

      // Set the value of the hidden form field
      $('#image-data__field').val(JSON.stringify(image));
    })
  },
  // Need to override the parameter so that it plays nicely
  paramName: 'file',
  // We are not supporting multiple uploads with this code
  maxFiles: 1,
  // We are only accepting images
  acceptedFiles: 'image/*',
  // We are manually triggering queue processing
  autoProcessQueue: false
};

With all of that in place, your direct uploads to S3 through DropzoneJS and Shrine in Rails should be fully functional. I hope that this walkthrough has saved you some of the time that I spent searching on my own for this information. If you have any feedback on the solution proposed here, please send it to stephen@stephencodes.com

comments powered by Disqus