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:
-
Why am I using
undef
CarrierWave aliases
read_uploader
withread_attribute
when using ActiveRecord,
so you should undefine in order to overwrite the alias. -
What type of data comes in
identifiers
?identifiers
is always an array of strings that represent file names -
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).