I recently came across a situation where I needed to be able to upload a file to a Rails server with Active Admin. I did a quick search on Google and found this post by Job, a fellow Atom.
Our use cases were a little bit different, though. He was storing the file contents directly in the database, whereas I needed to be able to uplaod a firmware image file to the server’s filesystem, parse the file name, and perform some validations on the file. I decided to use the Paperclip gem to manage the file processing and storage. Using Job’s advice on Active Admin file uploads, I expanded his example to incorporate Paperclip.
What is Paperclip?
Paperclip, as its name suggests, is a file attachment library for ActiveRecord. It is designed to treat files much like other attributes, and it provides a whole slew of built-in extensions (e.g. validations, pre/post processing callback hooks, security enhancements, etc.). The setup is very simple, and getting the basics up and running takes only a few minutes. For the sake of this example, we will set up the ActiveRecord model, create a migration, and get rolling with the Active Admin file upload.
Install the Paperclip Gem
Installing the gem is easy. Just add
gem 'paperclip'
to your Gemfile
and run
bundle install
Create a Migration
If you have an existing model where you want to add Paperclip support, then you simply need to create a migration. In this case, I already had a table and a corresponding model for my firmware image, so I just needed to add a migration to add the columns that Paperclip requires. The easiest way to do this is with the Rails migration generator:
rails generate paperclip firmware image
This will automatically create a migration file that looks like this:
class AddImageColumnsToFirmware < ActiveRecord::Migration
def up
add_attachment :firmware, :image
end
def down
remove_attachment :firmware, :image
end
end
The add_attachment
helper will automatically create the following columns in your table:
- image_file_name
- image_content_type
- image_file_size
- image_updated_at
Technically, only the *_file_name
column is required for Paperclip to operate, so you could throw away the others if you don’t need them.
Paperclip Your Models
The true beauty of Paperclip is how well it integrates with ActiveModel. To add Paperclip support to your model, start by adding the following line to your model class:
class Firmware < ActiveRecord::Base
has_attached_file :image
# more stuff to come
end
With that one line, Paperclip is now integrated with your Rails model, and it will automatically handle your file “attachments” just like any other Rails attribute! There are, of course, plenty of other options that you can add to the has_attached_file
attribute (e.g. specifying a file path, style type, etc.), but I won’t go into that right now. For our purposes, the defaults should be just fine.
Validations!
Paperclip also makes it really easy to perform powerful validations on the file. If any validation fails, the file will not be saved to disk and the associon will be rolled back, just like any other ActiveRecord validation. There are built-in helpers to validate attachment presence, size, content type, and file name.
In our case, we really just need to validate the file name to ensure that it had the proper format and also that it was a unique file name. The following validation did the trick:
validates_attachment_file_name :image, :matches => [/_\d+_\d+_\d+\.bin$/]
validates_uniqueness_of :image_file_name # this is a standard ActiveRecord validator
Additional Processing
I also wanted to be able to grab the firmware version out of the file name. The best way to do this is with a before_post_process
callback in the model, like this:
before_post_process :parse_file_name
def parse_file_name
version_match = /_(?\d+)_(?\d+)_(?\d+)\.bin$/.match(image_file_name)
if version_match.present? and version_match[:major] and version_match[:minor] and version_match[:patch]
self.major_version = version_match[:major]
self.minor_version = version_match[:minor]
self.patch_version = version_match[:patch]
end
end
Before the file is saved, but after the filename is validated (so we can be sure it has the proper formatting), we extract the major, minor, and patch numbers from the filename and save them in our database.
Configure Active Admin for File Upload
Now that Paperclip is all set up and wired into our Rails model, we need to actually set up the file upload piece in Active Admin. I won’t go into much detail, since I relied on Job’s post as a reference. Basically, all we need to do is:
- Define the Index page contents
Enumerate which columns will be displayed. If any special decoration is required, such as customized column title, sort properties, or row formatting, they can be specified easily. For example, I wanted a link to download the firmware image, so I added a
link_to
in the “Image” column. Note that the file path is stored in theimage.url
attribute. - Specify which parameters may be changed
Use the
permit_params
method to whitelist any attributes. - Create the upload form
Use the
f.input :image, as: :file
syntax to automatically create a file upload field in Active Admin.
The code snippet below is the Active Admin page, which allows the user to create, view, edit, and delete a firmware image.
ActiveAdmin.register Firmware do
permit_params :image
index do
selectable_column
id_column
column 'Image', sortable: :image_file_name do |firmware| link_to firmware.image_file_name, firmware.image.url end
column :image_file_size, sortable: :image_file_size do |firmware| "#{firmware.image_file_size / 1024} KB" end
column "Version" do |firmware| "#{firmware.major_version}.#{firmware.minor_version}.#{firmware.patch_version}" end
column :created_at
actions
end
form do |f|
f.inputs "Upload" do
f.input :image, required: true, as: :file
end
f.actions
end
end
And that’s it! We now have a fully functional file-upload implementation using Active Admin backed by Paperclip. It’s really quite simple, and it only took a few minutes to get it set up and running.
Thanks! Great, succinct writeup and precisely what I needed. Will give this a go tonight – appreciate the pointers!
Hey!
How would I make it so that I have the choice to either upload a file OR give a link to an exisiting file already online so that I don’t have to take up my space on my personal server.
Hi Keith,
Thanks for the comment. In Paperclip, you can set the attachment as a URL with the following syntax:
image = URI.parse(“http://example.com/some_image.bin”).
So you could have the option to enter a URL instead of uploading a file, and set the “image” attribute with the URI.parse’d URL. I’m actually not sure if this will instantly go download the file or if it will lazily fetch the file when needed, so you might have to test that. You can also store the downloaded file in a temp folder on your server so it doesn’t get persisted if that’s what you want. I hope this helps!
and if I want to load several images How can you do?