Rails + Shrine + DropzoneJS
Direct Uploads to S3
09-25-2017

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](https://groups.google.com/forum/#!msg/ruby-shrine/JZPYbR1NwLs/LuBAKcFjDwAJ), which I will essentially transcribe here with a bit more context to assist anyone dealing with the same issue. # Dependencies ```ruby 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](https://github.com/ncuesta/dropzonejs-rails). 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. ```ruby # 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](shrinerb.com/rdoc/classes/Shrine/Plugins/PresignEndpoint.html)), we'll need to add the route to our `config/routes.rb` file. ```ruby 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. ```ruby 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: ```ruby 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: ```html <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: ```javascript 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 [email protected]