这似乎是一个没有好的答案的热门问题,所以我将在这里完全回答它。在开始之前,我会提到代码可在https://github.com/mdchaney/multi获得,但请继续了解如何以最简单的方式执行此操作。
在我们开始之前,在 HTML 5 中,文件输入字段可以设置“多个”属性。如果已设置,则结果与具有多个同名文件输入的结果相同。在 Rails 中,为表单构建器构建的文件输入字段设置“multiple: true”将导致它作为文件数组上传。
<%= f.file_field :files, :multiple => true %>
变成
<input id="model_files" multiple="multiple" name="model[files][]" type="file" />
其中“模型”是您的模型的名称。这个输入控件将(至少在 Chrome 中)具有“选择文件”而不是“选择文件”的标签。
CarrierWave 本身无法处理此问题。它使用单个文本字段来存储有关单个文件的信息,并使用一些内部逻辑来确定该文件(以及可能的派生文件)的存储位置。可以破解它以将多个文件的信息放在单个文本字段中,选择带有设置分隔符的编码。这需要对 CarrierWave 进行大量工作和破解。
不过,我不想破解 CarrierWave,所以问题变成了这样一个事实:将多个文件附加到一个项目实际上是一对多的关系,或者在 Rails 术语中是“has_many”。因此,可以使用简单的属性编写器将文件输入字段中的文件添加到多个附加记录。
有了这个,我提出了最简单的方法,即使用具有多个属性集的 HTML 5 文件输入字段。有很多方法可以使用 jQuery 和 flash 来实现,但我提出这个是为了专门展示如何使用直接的 HTML 5 来实现。
在我们的示例中,我们将有一个简单的“上传”模型,每个模型都有一个名称和任意数量的链接文件,这些文件将存储在另一个名为linked_files 的模型中(让它变得简单,对吗?)。linked_file 将保存原始文件名、提供的内容类型,当然还有 CarrierWave 用于存储其信息的字段。
让我们为上传创建脚手架,然后为linked_files创建模型:
rails g scaffold Upload name:string
rails g model LinkedFile upload:references filename:string mime_type:string file:string
完成后,我们可以在字段上设置限制并添加“非空”约束:
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
现在,让我们通过添加一个名为“files”的新属性编写器来修复 Upload 模型:
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
其中大部分是 Rails 模型的正常声明。这里唯一真正添加的是“files=”方法,它采用数组中的一组上传文件并为每个文件创建一个“linked_file”。
我们需要一个 CarrierWave 上传器:
class FileUploader < CarrierWave::Uploader::Base
storage :file
def store_dir
"uploads/#{model.class.to_s.underscore}/#{model.id}/#{mounted_as}"
end
end
这是最简单的上传器,您可能希望限制上传的文件类型或其他。现在,LinkedFile 模型:
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
这并没有什么特别之处,只是添加了 :file_cache 和 :remove_file 作为文件上传器的可访问属性。
除了视图,我们现在完成了。我们实际上只需要更改表单,但我们还将更改“显示”以允许访问上传的文件。这是 _form.html.erb 文件:
<%= 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 %>
我添加了两段代码。如果@upload 对象没有与之关联的“linked_files”,我只显示多文件输入。否则,我会显示每个linked_file 及其所有信息。可以将“文件”方法添加到 Upload 并以这种方式处理它,但这样做会丢失跨请求的 mime 类型。
您可以轻松地对此进行测试,因为上传的“名称”是必填字段。启动服务器并http://127.0.0.1:3000/uploads
查看应用程序。单击“新建”链接,选择一些文件并点击“创建上传”而不提供名称。下一页将显示您正在等待的所有文件。添加名称时,所有内容都会保存。让我们修改“show”操作以显示linked_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 %>
在此,我只是为“文件”添加了一个标题和一个显示所有文件并提供查看链接的表格。没什么花哨的,但它确实有效。
如果我把它变成一个真正的应用程序,我可能还会在上传索引页面上提供一个文件列表或最少的文件计数。
就是这样了。同样,如果您想下载整个测试应用程序,可以在 github 上找到它,但我已将我的 Rails 生成语句和更改的全部内容放在这篇文章中。