Uploads de vários arquivos com carrierwave

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, UploaderExampleem 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 filecampo, quando você chamar model_instance.file, ele retornará uma instância de UploaderExamplecom 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_uploadermé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_uploadere 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:

  1. Porque estou usando undef

    CarrierWave aliases read_uploadercom read_attributeao usar ActiveRecord,
    então você deve undefine para sobrescrever o alias.

  2. Que tipo de dados entram identifiers?

    identifiers é sempre um array de strings que representam nomes de arquivos

  3. Que tipo de dados devo retornar em cada método?

    read_uploaderdeve retornar identificadores, o que quer que você tenha em sua coluna.
    write_uploaderdeve 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_uploadere write_uploadernovamente. 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).