myfile — file attachment implementation using Dragonfly

Tim Clegg
6 min readFeb 24, 2022

--

Photo by Tiger Lily from Pexels

Background

In case you feel like you’re jumping into the middle of a conversation, you are. This article is part of a series of articles exploring how some common Ruby gems can interact with OCI Object Storage.

The first article set the stage, building the super basic and simplistic myfile app. The second article showed how it could be ported to use Rails Active Storage and OCI Object Storage. This article is yet another permutation of the myfile app, but using a different file handling gem (in this case, Dragonfly).

Why another gem?

That’s a great question! This is another sample implementation using another gem. If you can answer the question “Why are there so many different file handling gems out there?”, then I can provide an answer for why another article in this series. ☺ In all seriousness, each gem will have a loyal following, for various reasons. Rather than just look at a single solution, I wanted to look at a wide variety of options, so I thought it’d be worth showing multiple implementations of the same solution with different gems. You can \choose the solution that resonates with your needs and preferences.

Getting started

First and foremost, you will need an OCI account to follow along. It’s free. It’s awesome. It lasts forever (with Always Free services). Sign-up for an OCI account now if you don’t have one!

This particular solution builds upon the myfile application that we built in a previous article. You’ll want to copy the application that was built, which is where this article starts (assuming you have the base myfile application working and ready-to-go). Whether you want to create a new branch or copy-and-paste a whole new copy of the application, the choice is yours!

You’ll also need an OCI Object Storage bucket created. I’m using the same bucket that was created in the Active Storage implementation. Check out the article if you want more info on the bucket.

If you have any questions, feel free to join us and chime in on Slack.

Adding the gem

Let’s start by adding the Dragonfly gem to our Gemfile:

# Gemfilegem 'dragonfly', '~> 1.4.0'
gem 'dragonfly-s3_data_store'

The above is blatantly plagiarized from the Dragonfly and Dragonfly::S3DataStore documentation. Credit attributed. ☺Go ahead and install the gem with Bundler:

$ bundle

Setup the basic Dragonfly infrastructure:

$ rails g dragonfly

Let’s configure the parameters we’ll be using. A big call-out to Eli for doing a great job of documenting a similar solution(which I re-used here), using another storage provider. Cobbling together the samples in the dragonfly-s3_data_store gemalong with Eli’s great example, we have the following:

# config/initializers/dragonfly.rbrequire 'dragonfly'
require 'dragonfly/s3_data_store'
...
# datastore :file,
# root_path: Rails.root.join('public/system/dragonfly', Rails.env),
# server_root: Rails.root.join('public')

datastore :s3,
bucket_name: "myfile-#{ Rails.env }",
access_key_id: ENV['OCI_KEY'],
secret_access_key: ENV['OCI_SECRET'],
region: ENV['OCI_REGION'],
fog_storage_options: {
endpoint: "https://#{ ENV["OCI_NAMESPACE"] }.compat.objectstorage.#{ ENV["OCI_REGION"] }.oraclecloud.com",
connection_options: {
ssl_verify_peer: false
},
path_style: true,
enable_signature_v4_streaming: false
}
end

It’s credit time, as this was a bit convoluted to figure out. This issue pointed to the parameter to avoid the SSL verification issue and this page pointed me to the path_style parameter.

Updating the app

Dragonfly requires a few columns to store some info about the file. This can be accomplished via a migration! Create a migration which will both eliminate old columns that we no longer need, as well as a couple that Dragonfly will need:

$ rails g migration RemoveThingsColumns

This will create a new file in db/migrate which we can modify to look like:

# db/migrate/<numbers>_remove_things_columns.rbclass RemoveThingsColumns < ActiveRecord::Migration[7.0]
def change
remove_column :things, :file_name, :string
remove_column :things, :content_type, :string
remove_column :things, :file_data, :binary

add_column :things, :document_uid, :string
add_column :things, :document_name, :string
end
end

Apply these changes:

$ rake db:migrate

If you’re getting a lot of weird warnings (warning: already initialized constant OptParse and similar), you might try what I did (thanks to what was posted in this issue):

$ bundle clean --force

That cleaned up the mess of warnings I was getting.

Update the thing model:

# app/models/thing.rbclass Thing < ApplicationRecord
dragonfly_accessor :document
end

Now let’s update the controller, changing this:

# app/controllers/things_controller.rb    def thing_params
ret = params.require(:thing).permit(:description)
ret.merge( { file_data: params[:thing][:file_data].read, file_name: params[:thing][:file_data].original_filename, content_type: params[:thing][:file_data].content_type } )
end

To this:

# app/controllers/things_controller.rb    def thing_params
ret = params.require(:thing).permit(:description, :document)
end

Also update the download method (in the same controller) from this:

# app/controllers/things_controller.rbclass ThingsController < ApplicationController
before_action :set_thing, only: %i[ show edit update destroy download ]

def download
# credit to https://piotrmurach.com/articles/streaming-large-zip-files-in-rails/ for the header to set
response.set_header('Content-Disposition', "attachment; filename=\"#{@thing.file_name}\"")
response.content_type = @thing.content_type
response.write(@thing.file_data)
end
...

To this:

# app/controllers/things_controller.rbclass ThingsController < ApplicationController
before_action :set_thing, only: %i[ show edit update destroy download ]

def download
response.set_header('Content-Disposition', "attachment; filename=\"#{@thing.document.name}\"")
response.content_type = @thing.document.mime_type
response.write(@thing.document.data)
end
...

Another credit shout-out is due, for providing an example of some of the headers used for a file download.

Modify the form view for a thing, changing this:

# app/views/things/_form.html.erb...  <div>
<%= form.label :file_data, style: "display: block" %>
<%= form.file_field :file_data %>
</div>
...

To this:

# app/views/things/_form.html.erb...  <div>
<%= form.label 'File', style: "display: block" %>
<%= form.file_field :document %>
</div>
...

Change the view for a thing from this:

# app/views/things/_thing.html.erb<div id="<%= dom_id thing %>">
<p>
<strong>File name:</strong>
<%= link_to thing.file_name, download_thing_path(thing) %>
</p>
<p>
<strong>Content type:</strong>
<%= thing.content_type %>
</p>
<p>
<strong>File data length (bytes):</strong>
<%= thing.file_data.length %>
</p>
<p>
<strong>Description:</strong>
<%= thing.description %>
</p>
</div>

To this:

# app/views/things/_thing.html.erb<div id="<%= dom_id thing %>">
<% if thing.document_stored? %>
<p>
<strong>File name:</strong>
<%= link_to thing.document.name, download_thing_path(thing) %>
</p>
<p>
<strong>Content type:</strong>
<%= thing.document.mime_type %>
</p>
<p>
<strong>File data length (bytes):</strong>
<%= thing.document.length %>
</p>
<% else %>
<p>No document</p>
<% end %>
<p>
<strong>Description:</strong>
<%= thing.description %>
</p>
</div>

I already apologized for the extremely sloppy code in the Active Storage implementation, so will refrain from repeating myself. Suffice it to say that this isn’t model code at all, but serves to show how to get Dragonfly to work with OCI Object Storage.

Experiencing it

We’ve managed to replicate the same behavior as we had before, where the app itself proxies uploading/downloading files to/from OCI Object Storage using the S3-compatibility interface. To see it for yourself, try it out with:

$ export OCI_KEY=<YOUR OCI KEY HERE>
$ export OCI_SECRET=<YOUR OCI SECRET HERE>
$ export OCI_NAMESPACE=<YOUR OCI NAMESPACE HERE>
$ export OCI_REGION=<YOUR OCI REGION HERE>
$ rails s

Have fun uploading and downloading files via the app. It works and from a user perspective, is a transparent change (moving from our own in-house implementation to Dragonfly).

Cleaning up

When you’re all finished, cleaning things up is super easy. Start Rails Console (rails c) and run the following:

Thing.all.each {|t| t.destroy }

That’s it! Any objects created by Dragonfly should be deleted now.

References

Conclusion

Using Dragonfly was certainly not as straight-forward as I envisioned. Dragonfly itself is pretty solid in terms of updates and community activity (lots of downloads, which presumably should indicate usage). One of the primary dependencies, dragonfly-s3_data_store on the other hand is getting really long-in-the-tooth, being ~5+ years stale (at the time of this writing), which isn’t a good sign. Even with this dormancy (and the inherent security risks associated with no ongoing maintenance/updates), it appears to enjoy continued download activity (presumably because this is the primary way of using S3 with Dragonfly).

The dragonfly-s3_data_store gem uses Fog (specifically, Fog::Storage). Fog is also a bit stale, with its most recent release (as of this writing) in 2019. On the plus side, it looks like the Fog AWS module is active (at the time of this writing has recent commit activity).

Because there were so many dependency layers, it took more time and effort to figure out the different parameters (and where to set them). Should they be set at the Dragonfly datastore level (the dragonfly-s3_data_store) or should they be set at the Fog::Storage level? Messy, but we got there!

One more annoyance I found is that the dragonfly-s3_data_store gem makes at least one or more calls to AWS, regardless of which endpoint you use. See it in the code, in the storage method, where it calls storage.sync_clock (line 92 as of the time of this writing), which forces a call to AWS (even when not using an AWS endpoint).

This was an interesting exercise, but not necessarily an approach I’d personally take in any sort of real (production) app. This is largely due to the intertwined dependency levels and the staleness of some of the gems (particularly the dragonfly-s3_data_store gem). My personal opinion aside, there might be really good use-cases for Dragonfly. If you need to use Dragonfly with OCI Object Storage, hopefully this article is of help!

--

--

Tim Clegg

Polyglot skillset: software development, cloud/infrastructure architecture, IaC, DevSecOps and more. Employed at Oracle. Views and opinions are my own.