Last Updated: June 21, 2016
·
9.859K
· dangaytan

Multiple file uploads with carrierwave

Multiple file uploads with CarrierWave

CarrierWave is a Ruby gem that lets you manage file uploads easily. You can store files locally, Amazon S3, or create your own storage by inheriting from CarrierWave::Storage::Abstract.

On October 17th, they announced in the master branch the possibility of uploading multiple files using a single field, adding the code later (with little changes from then until now).

The main usage of this gem has been for a single file upload. Now I am sharing my experiences with multiple file uploads working on the master branch.

I will assume you are working on a Rails 4.2 project, with ActiveRecord as ORM.

Getting started

How CarrierWave works

When you use mount_uploader :field, UploaderExample within your model, CarrierWave basically saves the filename in the database and use it to instantiate UploaderExample. It adds some methods in order to manage this behavior. This is basically the same code used when you call mount_uploaders (plural).

If your database has a filename 'sample.pdf' in the file field, when you call model_instance.file, it will return an instance of UploaderExample with that filename as the key to find the actual file. You can configure CarrierWave following these steps.

mount_uploaders

We must use the master branch in order to use this feature, so let's specify the dependency in our Gemfile:

gem 'carrierwave', github: 'carrierwaveuploader/carrierwave'

and add the field to the model that needs the files:

add_column :notes, :files, :text

Note I am using :text type, not string. Since you are going to mount many filenames, you will need space for long file names.

CarrierWave created this feature for Postgres’ array field in mind. It serializes an array of strings inside your field. If you are using Mysql, you will need to create your own field type and tell your class to use it:

# app/field_types/array_type.rb
class ArrayType < ActiveRecord::Type::Text
  def type_cast(value)
    Array.wrap(YAML::load(value || YAML.dump([])))
  end
end

# config/application.rb
config.autoload_paths << Rails.root.join('app', 'field_types')

# app/models/note.rb
class Note < ActiveRecord::Base
  attribute :files, ArrayType.new
  mount_uploaders :files, GeneralUploader
end

This will serialize and deserialize as needed in order to create the instances of the specified uploaders. For example, mounting uploaders in Note:

before mount_uploaders:

note.files # => "['vacunas.md', 'vacunas2.md']"

after mount_uploaders:

note.files # => [#<GeneralUploader:0x007fe861d15dc0 @model=#<Note id: 3,...>, #<GeneralUploader...>]

Then you can use your new uploader like this:

note.files.each do |uploader|
  uploader.file.filename
end

This will take into account the extension_whitelist method in each file in the array and make sure they all are valid to be saved.

Upgrading from a single uploader

When you have a single uploader, you will not have a string of files serialized, but a single string that you will want to handle. You can then setup the new file type (it will create the array you need), and carrierwave will handle the reading of the field for you.

In order to write back to the field, you will have to override write_uploader method within your model in order to save it the right way:

class Note < ActiveRecord::Base
  attribute :files, ArrayType.new
  mount_uploaders :files, GeneralUploader

  def write_uploader(column, identifiers)
    Array.wrap(identifiers)
  end
end

Serialized fields

Let's say you have a serialized field. Each file within has other special attributes for you, such as when it was uploaded. You can do that by overwriting read_uploader and write_uploader.

For the following type of element in the array:

class AppFile
  attr_accessor :file_name, :created_at

  def initialize(file_name:, created_at: nil)
    @file_name = file_name
    @created_at = created_at || Time.now
  end
end

, the Note class should look like:

class Note < ActiveRecord::Base
  attribute :files, ArrayType.new
  serialize :files, Array
  mount_uploaders :files, GeneralUploader

  undef read_uploader
  def read_uploader(column)
    read_attribute(column).map(&:file_name)
  end

  undef write_uploader
  def write_uploader(column, identifiers)
    old_files = read_attribute(:files)
    file_cache = {}
    identifiers.compact.map do |identifier|
      if old_file = old_files.find{|doc| doc.try(:file_name) == identifier}
        file_cache[identifier] = old_file
      else
        file_cache[identifier] = AppFile.new(file_name: identifier)
      end
    end
    write_attribute(column, file_cache.values)
  end
end

Some heads up here:

  1. Why am I using undef

    CarrierWave aliases read_uploader with read_attribute when using ActiveRecord,
    so you should undefine in order to overwrite the alias.

  2. What type of data comes in identifiers?

    identifiers is always an array of strings that represent file names

  3. What kind of data should I return in each method?

    read_uploader should return identifiers, whatever you have in your column.
    write_uploader should write what you need to your database, whatever CarrierWave gives you as identifiers.

Heads up for subclasses

If you are using subclasses for a class with files, and want to use a different uploader,
you will have to redefine read_uploader and write_uploader again. The reason is whenever
you write mount_uploaders :files, you are basically writing the class again, so
overrides must be set again.

I hope you found this article interesting and explanatory. Please let me know if
I should do some work in the edition, I might not be the best writer (...yet).