Uploads de vários arquivos com CarrierWave
CarrierWave é uma joia Ruby que permite gerenciar facilmente o upload de arquivos. Você pode armazenar arquivos localmente, Amazon S3, ou criar seu próprio armazenamento herdando de CarrierWave :: Storage :: Abstract.
No dia 17 de outubro, eles anunciaram no branch master a possibilidade de fazer upload de vários arquivos usando um único campo, adicionando o código posteriormente (com poucas alterações de então até agora).
O principal uso desta gema tem sido o upload de um único arquivo. Agora estou compartilhando minhas experiências com múltiplos uploads de arquivos trabalhando no branch master.
Vou assumir que você está trabalhando em um projeto Rails 4.2, com ActiveRecord como ORM.
Começando
Como funciona o CarrierWave
Quando você usa mount_uploader :field, UploaderExample
em seu modelo, CarrierWave basicamente salva o nome do arquivo no banco de dados e o usa para instanciar UploaderExample
. Ele adiciona alguns métodos para gerenciar esse comportamento. Este é basicamente o mesmo código usado quando você liga mount_uploaders
(plural).
Se seu banco de dados tiver um nome de arquivo ‘sample.pdf’ no file
campo, quando você chamar model_instance.file
, ele retornará uma instância de UploaderExample
com esse nome de arquivo como a chave para encontrar o arquivo real. Você pode configurar CarrierWave seguindo estas etapas .
mount_uploaders
Devemos usar o branch master para usar este recurso, então vamos especificar a dependência em nosso Gemfile:
gem 'carrierwave', github: 'carrierwaveuploader/carrierwave'
e adicione o campo ao modelo que precisa dos arquivos:
add_column :notes, :files, :text
Observe que estou usando tipo, não string. Como você montará muitos nomes de arquivo, precisará de espaço para nomes de arquivo longos.:text
CarrierWave criou este recurso para o campo array do Postgres em mente. Ele serializa uma matriz de strings dentro do seu campo. Se estiver usando o Mysql, você precisará criar seu próprio tipo de campo e dizer à sua classe para usá-lo:
# 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
Isso será serializado e desserializado conforme necessário para criar as instâncias dos uploaders especificados. Por exemplo, montagem de uploaders em Nota:
antes de mount_uploaders:
note.files # => "['vacunas.md', 'vacunas2.md']"
após mount_uploaders:
note.files # => [#<GeneralUploader:0x007fe861d15dc0 @model=#<Note id: 3,...>, #<GeneralUploader...>]
Em seguida, você pode usar seu novo uploader desta forma:
note.files.each do |uploader|
uploader.file.filename
end
Isso levará em consideração o método extension_whitelist em cada arquivo no array e garantirá que todos sejam válidos para serem salvos.
Atualizando a partir de um único uploader
Quando você tem um único uploader, não terá uma string de arquivos serializados, mas uma única string com a qual deseja controlar. Você pode então configurar o novo tipo de arquivo (ele criará o array de que você precisa) e o carrierwave cuidará da leitura do campo para você.
Para escrever de volta no campo, você terá que substituir o write_uploader
método dentro do seu modelo para salvá-lo da maneira certa:
class Note < ActiveRecord::Base
attribute :files, ArrayType.new
mount_uploaders :files, GeneralUploader
def write_uploader(column, identifiers)
Array.wrap(identifiers)
end
end
Campos serializados
Digamos que você tenha um campo serializado. Cada arquivo contém outros atributos especiais para você, como quando foi carregado. Você pode fazer isso substituindo read_uploader
e write_uploader
.
Para o seguinte tipo de elemento na matriz:
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
, a classe Note deve ser semelhante a:
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
Algumas dicas aqui:
Porque estou usando
undef
CarrierWave aliases
read_uploader
comread_attribute
ao usar ActiveRecord,
então você deve undefine para sobrescrever o alias.Que tipo de dados entram
identifiers
?identifiers
é sempre um array de strings que representam nomes de arquivosQue tipo de dados devo retornar em cada método?
read_uploader
deve retornar identificadores, o que quer que você tenha em sua coluna.write_uploader
deve escrever o que você precisa em seu banco de dados, o que quer que CarrierWave forneça como identificadores.
Atenção para as subclasses
Se você estiver usando subclasses para uma classe com arquivos e quiser usar um uploader diferente,
terá que redefinir read_uploader
e write_uploader
novamente. O motivo é que sempre que
você escreve mount_uploaders :files
, basicamente está escrevendo a classe novamente, portanto, as
substituições devem ser definidas novamente.
Espero que você tenha achado este artigo interessante e explicativo. Por favor, me diga se
eu devo fazer algum trabalho na edição, posso não ser o melhor escritor (… ainda).