Let's say you want to add a picture field to your product model. How do you do it?
Unfortunately, Rails' Active Record doesn't provide a native way to work with binary fields, like pictures. We can easily add a string field, but we can't add a file field.
Dragonfly is a gem that extends ActiveRecord, allowing to have binary fields, like videos, files, or... pictures. It's a good, or even better, alternative to the famous Paperclip gem.
Let's create an example app with these commands:
rails new test_df
cd test_df
rails g scaffold product title
rake db:migrate
rails s
# Gemfile
gem 'dragonfly'
bundle install
rails g dragonfly
This command generates a config/initializers/dragonfly.rb
file, which can be used to configure DragonFly's core settings.
To add a normal string field like color
, we create a migration like this:
rails g migration add_field_to_product color
That's how we create a color
string field. To create a Dragonfly field, you need to add an _uid
at the end of the name. So, in this example, the name would be color_uid
. If the name is photo
, that would be photo_uid
.
Let's use image
as the field name:
rails g migration add_field_to_product image_uid
Commit the changes with rake db:migrate
:
rake db:migrate
Now open the product
model and let Rails know this field isn't a string, but a file:
# app/model/product.rb
dragonfly_accessor :image
Images
folder as 1.jpg
rails c
> i = open('../../Images/1.jpg')
> p = Product.last
> p.image = i
> p.save!
WARNING
We are using ../../Images
, because my project is at ~/projects/test-dragonfly
, i.e., 2 folders from ~/Images
. Change it at your will.
> quit
rails c
> p = Product.last
> p.image
# => <Dragonfly Attachment uid="2015/09/26/4ad07k6hin_1.jpg", app=:default>
If p.image
isn't nil, it has been saved.
url
method> p.image.url
# => "/media/W1siZiIsIjIwMTUvMDkvMjYvNGFkMDdrNmhpbl8xLmpwZyJdXQ?sha=0d7af674c4891620"
http://localhost:3000/media/W1siZiIsIjIwMTUvMDkvMjYvNGFkMDdrNmhpbl8xLmpwZyJdXQ?sha=0d7af674c4891620
thumb
method. It generates a new version with different geometries> p.image.thumb('80x60').url
=> "/media/W1siZiIsIjIwMTUvMDkvMjcvOGNiNHFmbTU0M18xLmpwZyJdLFsicCIsInRodW1iIiwiODB4NjAiXV0?sha=1931e3387c6b54f5"
In the rails server window, we can see the logs showing that DragonFly is generating the file on the fly:
Whenever someone loads the thumb image on a browser, Dragonfly gets the original image and converts it, generating a new image.
To see this process happening, just reload the image on the browser and check the logs.
That's why DragonFly has this name – it's on-the-fly, like a Dragonfly 😃
index.html
and show.html
Now, let's show these images as thumbs in our product index page.
index.html.erb
and create a thumb
columnUse image_tag()
to generate an HTML image tag and the thumb()
method to generate a smaller version of the image.
Simplified example:
<!-- apps/views/products/index.html.erb -->
<table>
<tr>
<th>Title</th>
<th>Thumb</th>
</tr>
<% @products.each do |product| %>
<tr>
<td><%= product.name %></td>
<td><%= image_tag product.image.thumb('80x60').url %></td>
</tr>
<% end %>
</table>
Result:
show.html.erb
<!-- apps/views/products/show.html.erb -->
<p>
<strong>Image:</strong>
<%= @product.image.url %>
</p>
If you run the above code you are going to get an exception. That's because you have some records without any image attached, and you're calling thumb
/url
methods from a nil
value.
To check if the field has an attached file or not, use Rail's core nil?
method or use the _stored?
method:
<!-- apps/views/products/show.html.erb -->
<%= product.image_stored? ? image_tag(product.image.thumb('80x60').url) : "" %>
OR
<!-- apps/views/products/show.html.erb -->
<%= product.image.nil? ? image_tag(product.image.thumb('80x60').url) : "" %>
We've added images using the console. But how to save the image through a form?
With DragonFly, we can do it in the same way that we would do with a normal field.
_form.html.erb
, create a new HTML field with the corresponding name, and use the file_field
method to generate a HTML file field<!-- app/views/products/_form.html.erb -->
<%= form_for(@product) do |f| %>
<div class="field">
<%= f.label :title %><br>
<%= f.text_field :title %>
</div>
<div class="field">
<%= f.label :image %><br>
<%= f.file_field :image %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
# app/controllers/products_controller.rb
...
def product_params
params.require(:product).permit(:title, :image)
end
...
Now we can send and save an image using the form:
As DragonFly gem generates new versions on the fly, it can be very resource-consuming, so we need to use a server cache solution.
Let's add the rack-cache gem
.
Gemfile
# Gemfile
gem 'rack-cache', require: 'rack/cache', group: :production
bundle install
# config/environments/production.rb
config.action_dispatch.rack_cache = true
# config/application.rb
class Application < Rails::Application
...
if defined?(Rack::Cache)
config.middleware.delete(Rack::Cache)
config.middleware.insert 0, Rack::Cache, {
:verbose => true, # log verbosity
:metastore => URI.encode("file:#{Rails.root}/tmp/dragonfly/cache/meta"), # URI encoded in case of spaces
:entitystore => URI.encode("file:#{Rails.root}/tmp/dragonfly/cache/body")
}
end
end
TIP
You don't need to do this if you already use a server cache solution like Varnish.
Use validates_property
method on the model to validate properties. Examples:
# app/models/product.rb
class Product < ActiveRecord::Base
dragonfly_accessor :image
validates_property :width, of: :image, in: (0..400)
validates_property :mime_type, of: :image, in: %w(image/jpeg image/png)
validates_property :size, of: :image, maximum: 500.kilobytes
end
As an example, let's say you want to validate if the attachment is an image or something else. With this you can make sure your users aren't uploading movies or applications:
# app/models/product.rb
class Product < ActiveRecord::Base
dragonfly_accessor :image
validates_property :mime_type, of: :image, in: %w(image)
end
See a full list of validations here: https://markevans.github.io/dragonfly/models#validations (opens new window)
This concludes our basic DragonFly gem tutorial, but DragonFly has also many advanced features: you can use Amazon S3, Dropbox, and other providers as your storage. You can change the image on-the-fly in so many ways: add a watermark, change to black and white, change the file format, and so on. You can also create your own plugins to extend DragonFly.
You can also work with other binary types like movies or apps, but I won't cover these things here. Maybe on other tutorial 😉
ImageMagick
installed. On Ubuntu, run a sudo apt-get install imagemagick
to fix it.