This seems to be a popular problem without good answers, so I am going to fully answer it here. Before starting, I mentioned that the code is available at https://github.com/mdchaney/multi , but follow on to see how to do this in the easiest way.
Before we go there, in HTML 5 the file input field may have the attribute “multiple”. If it is installed, the result will be the same as having multiple file entries with the same name. In Rails, setting "multiple: true" for the input field of the file created by the form designer will load it as an array of files.
<%= f.file_field :files, :multiple => true %>
becomes
<input id="model_files" multiple="multiple" name="model[files][]" type="file" />
where "model" is the name of your model. This input control (in Chrome, at least) has the label "Select Files" instead of "Select File."
CarrierWave cannot handle this. It uses a single text field to store information about one file and some internal logic to determine where this file is stored (and possibly its derivatives). One could hack it to place information for several files in one text field, choosing an encoding with a given separator. This will require a lot of work and hacking on CarrierWave.
I don’t want to hack CarrierWave, so the problem is that having multiple files attached to one element is actually a one-to-many relationship, or in terms of Rails, “has_many”. Thus, you can add files from the file entry field to several attached records using a simple attribute author.
With this, I present the simplest way to do this, which uses the input field of an HTML 5 file with a set of many attributes. There are ways to do this using jQuery and flash, but I present it to show how to do this with direct HTML 5.
In our example, we will have a simple model for "uploads", each of which will have a name and any number of related files that will be stored in another model called linked_files (which will simplify, right?). The associated_file will contain the original file name, the specified content type, and, of course, a field for CarrierWave to store its information.
Let me create a scaffold for uploads, and then just a model for linked_files:
rails g scaffold Upload name:string rails g model LinkedFile upload:references filename:string mime_type:string file:string
With this, we can set limits if we want in the fields and add a non-null constraint:
class CreateUploads < ActiveRecord::Migration def change create_table :uploads do |t| t.string :name, limit: 100, null: false t.timestamps end end end class CreateLinkedFiles < ActiveRecord::Migration def change create_table :linked_files do |t| t.references :upload, null: false t.string :filename, limit: 255, null: false t.string :mime_type, limit: 255, null: false t.string :file, limit: 255, null: false t.timestamps end add_index :linked_files, :upload_id end end
Now, fix the loading model by adding a new attribute creator called “files”:
class Upload < ActiveRecord::Base has_many :linked_files, inverse_of: :upload, dependent: :destroy accepts_nested_attributes_for :linked_files, reject_if: :all_blank, allow_destroy: true validates_associated :linked_files attr_accessible :name, :files, :linked_files_attributes def files=(raw_files) raw_files.each do |raw_file| self.linked_files.build({filename: raw_file.original_filename, mime_type: raw_file.content_type, file: raw_file}) end end validates :name, presence: true, length: { maximum: 100 } end
In most cases, these are regular declarations for the Rails model. The only real addition here is the "files =" method, which takes a set of uploaded files into an array and creates a "linked_file" for each of them.
We need a CarrierWave bootloader:
class FileUploader < CarrierWave::Uploader::Base storage :file def store_dir "uploads/
This is the easiest bootloader, you might want to limit the type of file you upload or something else. Now the LinkedFile model:
class LinkedFile < ActiveRecord::Base mount_uploader :file, FileUploader belongs_to :upload, inverse_of: :linked_files attr_accessible :file, :filename, :mime_type, :file_cache, :remove_file validates :filename, presence: true, length: { maximum: 255 } validates :mime_type, presence: true, length: { maximum: 255 } end
And this is nothing special, just added: file_cache and: remove_file as available attributes for the file loader.
Now we are finished, except for the submissions. We really need to change the form, but we will also change the “show” to allow access to the downloaded files. Here is the _form.html.erb file:
<%= form_for(@upload, { multipart: true }) do |f| %> <% if @upload.errors.any? %> <div id="error_explanation"> <h2><%= pluralize(@upload.errors.count, "error") %> prohibited this upload from being saved:</h2> <ul> <% @upload.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> <% if f.object.linked_files.size == 0 -%> <div class="field"> <%= f.label :files %><br /> <%= f.file_field :files, :multiple => true %> </div> <% else -%> <fieldset> <legend>Linked Files</legend> <%= f.fields_for :linked_files do |lff| -%> <div class="field"> <%= lff.label :filename %><br /> <%= lff.text_field :filename %> </div> <div class="field"> <%= lff.label :mime_type %><br /> <%= lff.text_field :mime_type %> </div> <div class="field"> <%= lff.label :file, 'File (replaces current selection)' %><br /> <%= lff.file_field :file %> <%= lff.hidden_field :file_cache %> </div> <div class="field"> <%= lff.check_box :_destroy %> Remove this file </div> <hr /> <% end -%> </fieldset> <% end -%> <div class="actions"> <%= f.submit %> </div> <% end %>
I have added two sections of code. If the @upload object does not have any associated_files associated with it, I simply display several input files. Otherwise, I show every linked file with all its information. One could add a “files” method for loading and process it this way, but at the same time lose the mime type for requests.
You can easily test this, since a downloadable "name" is a must. Start the server and go to http://127.0.0.1:3000/uploads : http://127.0.0.1:3000/uploads to see the application. Click the Create link, select some files, and click Create Download without specifying a name. The next page will show all your files waiting now. When you add a name, everything will be saved. Let me change the show action to show related files:
<p id="notice"><%= notice %></p> <p> <b>Name:</b> <%= @upload.name %> </p> <p> <b>Files:</b><br /> </p> <table> <thead><tr><th>Original Filename</th><th>Content Type</th><th>Link</th></tr></thead> <tbody> <% @upload.linked_files.each do |linked_file| -%> <tr> <td><%= linked_file.filename %></td> <td><%= linked_file.mime_type %></td> <td><%= link_to linked_file.file.url, linked_file.file.url %></td> </tr> <% end -%> </tbody> </table> <%= link_to 'Edit', edit_upload_path(@upload) %> | <%= link_to 'Back', uploads_path %>
In this, I simply added a heading for "Files" and a table that shows all of them and provides a link for viewing. Nothing out of the ordinary, but it works.
If I did this in a real application, I would probably also provide a list of files or the minimum number of files on the download index page.
So what is it. Again, the whole test application is available on github if you want to download it, but I put all my Rails instructions and changes in this post.