Статьи

Загрузка с помощью Rails и Carrierwave

Это еще одна статья из серии «Загрузка с Rails». Сегодня мы встретимся с Carrierwave — одним из самых популярных решений для загрузки файлов в Rails. Мне нравится Carrierwave, потому что его легко начать, он имеет множество функций из коробки и предоставляет десятки статей «как сделать», написанных членами сообщества, так что вы не потеряете себя.

В этой статье вы узнаете, как:

  • Интегрируйте Carrierwave в ваше приложение Rails
  • Добавить проверки
  • Сохранять файлы в запросах
  • Удалить файлы
  • Генерировать миниатюры
  • Загружать файлы из удаленных мест
  • Ввести несколько загрузок файлов
  • Добавить поддержку облачного хранилища

Исходный код этой статьи доступен на GitHub . Наслаждайся чтением!

Как всегда, начните с создания нового приложения Rails:

1
rails new UploadingWithCarrierwave -T

Для этой демонстрации я буду использовать Rails 5.0.2. Обратите внимание, что Carrierwave 1 поддерживает только Rails 4+ и Ruby 2. Если вы все еще используете Rails 3, подключите Carrierwave версии 0.11.

Чтобы увидеть Carrierwave в действии, мы собираемся создать очень простое приложение для блогов с единственной моделью Post . Он будет иметь следующие основные атрибуты:

  • title ( string )
  • body ( text )
  • image ( string ) — это поле будет содержать изображение (если быть точным, имя файла), прикрепленное к сообщению

Создайте и примените новую миграцию:

1
2
rails g model Post title:string body:text image:string
rails db:migrate

Установите несколько маршрутов:

1
2
resources :posts
root to: ‘posts#index’

Также создайте очень простой контроллер:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update]
 
  def index
    @posts = Post.order(‘created_at DESC’)
  end
 
  def show
  end
 
  def new
    @post = Post.new
  end
 
  def create
    @post = Post.new(post_params)
    if @post.save
      redirect_to posts_path
    else
      render :new
    end
  end
 
  def edit
  end
 
  def update
    if @post.update_attributes(post_params)
      redirect_to post_path(@post)
    else
      render :edit
    end
  end
 
  private
 
  def post_params
    params.require(:post).permit(:title, :body, :image)
  end
 
  def set_post
    @post = Post.find(params[:id])
  end
end

Теперь давайте создадим индексное представление:

1
2
3
4
5
<h1>Posts</h1>
 
<%= link_to ‘Add post’, new_post_path %>
 
<%= render @posts %>

И соответствующий частичный:

1
2
3
4
5
6
<h2><%= link_to post.title, post_path(post) %></h2>
 
<p><%= truncate(post.body, length: 150) %></p>
 
<p><%= link_to ‘Edit’, edit_post_path(post) %></p>
<hr>

Здесь я использую метод truncate Rails для отображения только первых 150 символов из поста. Прежде чем создавать другие представления и частичную форму, давайте сначала интегрируем Carrierwave в приложение.

Вставьте новый драгоценный камень в Gemfile :

1
gem ‘carrierwave’, ‘~> 1.0’

Бегать:

1
bundle install

Carrierwave хранит свою конфигурацию внутри загрузчиков , которые входят в ваши модели. Чтобы сгенерировать загрузчик, используйте следующую команду:

1
rails generate uploader Image

Теперь внутри app / uploaders вы найдете новый файл с именем image_uploader.rb . Обратите внимание, что в нем есть несколько полезных комментариев и примеров, поэтому вы можете использовать его для начала. В этой демонстрации мы будем использовать ActiveRecord, но Carrierwave также поддерживает Mongoid, Sequel и DataMapper.

Далее нам нужно включить или смонтировать этот загрузчик в модель:

1
mount_uploader :image, ImageUploader

Загрузчик уже имеет нормальные настройки по умолчанию, но по крайней мере нам нужно выбрать, где будут сохранены загруженные файлы. А пока давайте используем хранилище файлов:

1
storage :file

По умолчанию файлы будут помещаться в каталог public / uploads , поэтому лучше всего исключить его из системы контроля версий:

1
public/uploads

Вы также можете изменить метод store_dir внутри вашего загрузчика, чтобы выбрать другое местоположение.

На этом этапе мы можем создать новый вид и частичную форму для начала загрузки файлов:

1
2
3
<h1>Add post</h1>
 
<%= render ‘form’, post: @post %>
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<%= form_for post do |f|
  <div>
    <%= f.label :title %>
    <%= f.text_field :title %>
  </div>
 
  <div>
    <%= f.label :body %>
    <%= f.text_area :body %>
  </div>
 
  <div>
    <%= f.label :image %>
    <%= f.file_field :image %>
  </div>
 
  <%= f.submit %>
<% end %>

Обратите внимание, что PostsController не нужно изменять, так как мы уже разрешили атрибут image .

Наконец, создайте представление редактирования:

1
2
3
<h1>Edit post</h1>
 
<%= render ‘form’, post: @post %>

Это оно! Вы можете загрузить сервер и попытаться создать пост с изображением. Проблема в том, что это изображение нигде не видно, поэтому давайте перейдем к следующему разделу и добавим страницу показа!

Итак, единственный вид, который мы еще не создали, это шоу . Добавьте это сейчас:

1
2
3
4
5
6
7
8
<%= link_to ‘All posts’, posts_path %>
<h1><%= @post.title %></h1>
 
<%= image_tag(@post.image.url, alt: ‘Image’) if @post.image?
 
<p><%= @post.body %></p>
 
<p><%= link_to ‘Edit’, edit_post_path(@post) %></p>

Как видите, отобразить вложение очень просто: все, что вам нужно сделать, это сказать @post.image.url чтобы получить URL изображения. Чтобы получить путь к файлу, используйте метод current_path . Обратите внимание, что Carrierwave также предоставляет image? метод для нас, чтобы проверить, присутствует ли вложение вообще (сам метод image никогда не вернет nil , даже если файл не присутствует).

Теперь после перехода к сообщению вы должны увидеть изображение, но оно может показаться слишком большим: в конце концов, мы нигде не ограничиваем размеры. Конечно, мы могли бы уменьшить изображение с помощью некоторых правил CSS, но гораздо лучше создать миниатюру после загрузки файла. Это, однако, требует некоторых дополнительных шагов.

Чтобы обрезать и масштабировать изображения, нам нужен отдельный инструмент. «Carrierwave» имеет встроенную поддержку драгоценных камней RMagick и MiniMagick, которые, в свою очередь, используются для управления изображениями с помощью ImageMagick . ImageMagick — это решение с открытым исходным кодом, позволяющее редактировать существующие изображения и создавать новые, поэтому перед тем, как продолжить, вам необходимо скачать и установить его . Далее вы можете выбрать любой из двух драгоценных камней. Я остановлюсь на MiniMagick, потому что он намного проще в установке и имеет лучшую поддержку:

1
gem ‘mini_magick’

Бегать:

1
bundle install

Затем включите MiniMagick в свой загрузчик:

1
include CarrierWave::MiniMagick

Теперь нам просто нужно представить новую версию для нашего загрузчика. Понятие версий (или стилей) используется во многих библиотеках загрузки файлов; это просто означает, что дополнительные файлы на основе исходного вложения будут созданы, например, с разными размерами или форматами. Введите новую версию под названием thumb :

1
2
3
version :thumb do
    process resize_to_fill: [350, 350]
end

У вас может быть столько версий, сколько вам нужно, и, более того, версии могут быть построены поверх других:

1
2
3
version :small_thumb, from_version: :thumb do
    process resize_to_fill: [20, 20]
end

Если вы уже загрузили некоторые изображения, у них не будет доступных миниатюр. Это не проблема, так как вы можете воссоздать их из консоли Rails:

1
2
rails c
Post.find_each {|post|

Наконец, отобразите свой эскиз со ссылкой на исходное изображение:

1
<%= link_to(image_tag(@post.image.thumb.url, alt: ‘Image’), @post.image.url, target: ‘_blank’) if @post.image?

Загрузи сервер и наблюдай за результатом!

В настоящее время наша загрузка работает, но мы вообще не проверяем ввод пользователя, что, конечно, плохо. Пока мы хотим работать только с изображениями, давайте добавим в белый список .png, .jpg и .gif расширения:

1
2
3
def extension_whitelist
    %w(jpg jpeg gif png)
end

Вы также можете добавить проверки типов контента, определив метод content_type_whitelist :

1
2
3
def content_type_whitelist
    /image\//
end

В качестве альтернативы, можно занести в черный список некоторые типы файлов, например исполняемые файлы, определив метод content_type_blacklist .

Помимо проверки типа и расширения файла, давайте установим его размер менее 1 мегабайта. Для этого нам понадобится дополнительный валидатор, поддерживающий валидацию файлов для ActiveModel:

1
gem ‘file_validators’

Установите это:

1
bundle install

Теперь введите нужные проверки (обратите внимание, что я также добавляю проверки для атрибутов title и body ):

1
2
3
validates :title, presence: true, length: {minimum: 2}
validates :body, presence: true
validates :image, file_size: { less_than: 1.megabytes }

Следующее, что нужно сделать, это добавить переводы I18n для сообщений об ошибках Carrierwave:

1
2
3
4
5
6
7
8
en:
 errors:
   messages:
     carrierwave_processing_error: «Cannot resize image.»
     carrierwave_integrity_error: «Not an image.»
     carrierwave_download_error: «Couldn’t download image.»
     extension_whitelist_error: «You are not allowed to upload %{extension} files, allowed types: %{allowed_types}»
     extension_blacklist_error: «You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}»

В настоящее время мы нигде не отображаем ошибки проверки, поэтому давайте создадим общую часть:

1
2
3
4
5
6
7
8
<% if object.errors.any?
  <h3>Some errors were found:</h3>
  <ul>
    <% object.errors.full_messages.each do |message|
      <li><%= message %></li>
    <% end %>
  </ul>
<% end %>

Используйте это частичное внутри формы:

1
<%= render ‘shared/errors’, object: post %>

Теперь попробуйте загрузить несколько недействительных файлов и просмотрите результат. Это должно работать, но если вы выберете правильный файл и не заполните заголовок или текст, проверки все равно не пройдут, и появится сообщение об ошибке. Однако поле файла будет очищено, и пользователю потребуется снова выбрать изображение, что не очень удобно. Чтобы это исправить, нам нужно добавить еще одно поле в форму.

Сохранение файлов при повторном отображении формы на самом деле довольно просто. Все, что вам нужно сделать, это добавить новое скрытое поле и разрешить его внутри контроллера:

1
2
3
<%= f.label :image %>
<%= f.file_field :image %><br>
<%= f.hidden_field :image_cache %>
1
params.require(:post).permit(:title, :body, :image, :image_cache)

Теперь image_cache будет заполнен автоматически, и изображение не будет потеряно. Также может быть полезно отобразить миниатюру, чтобы пользователь понял, что изображение было успешно обработано:

1
2
3
<% if post.image?
    <%= image_tag post.image.thumb.url %>
<% end %>

Еще одна очень распространенная функция — это возможность удалять вложенные файлы при редактировании записи. С Carrierwave реализация этой функции не является проблемой. Добавьте новый флажок в форму:

1
2
3
4
5
6
7
8
9
<% if post.image?
    <%= image_tag post.image.thumb.url %>
    <div>
      <%= label_tag :remove_image do %>
        Remove image
        <%= f.check_box :remove_image %>
      <% end %>
    </div>
<% end %>

И разрешить атрибут remove_image :

1
params.require(:post).permit(:title, :body, :image, :remove_image, :image_cache)

Это оно! Чтобы удалить изображение вручную, используйте remove_image! метод:

1
@post.remove_image!

Carrierwave также предоставляет очень полезную функцию из коробки: возможность загружать файлы из удаленных мест по их URL. Давайте сейчас введем эту возможность, добавив новое поле и разрешив соответствующий атрибут:

1
2
<%= f.text_field :remote_image_url %>
<small>Enter URL to an image</small>
1
params.require(:post).permit(:title, :body, :image, :remove_image, :image_cache, :remote_image_url)

Как это круто? Вам не нужно вносить какие-либо изменения, и вы можете проверить эту функцию прямо сейчас!

Предположим, мы хотим, чтобы в нашем сообщении было несколько доступных вложений. При текущей настройке это невозможно, но, к счастью, Carrierwave также поддерживает такой сценарий. Чтобы реализовать эту функцию, вам нужно добавить либо сериализованное поле (для SQLite), либо поле JSON (для Postgres или MySQL). Я предпочитаю последний вариант, поэтому давайте теперь перейдем к новому адаптеру базы данных. Удалите драгоценный камень sqlite3 из Gemfile и добавьте вместо него pg:

1
gem ‘pg’

Установите это:

1
bundle install

Измените конфигурацию базы данных следующим образом:

01
02
03
04
05
06
07
08
09
10
11
default: &default
  adapter: postgresql
  pool: 5
  timeout: 5000
 
development:
  <<: *default
  database: upload_carrier_dev
  username: ‘YOUR_USER’
  password: ‘YOUR_PASSWORD’
  host: localhost

Создайте соответствующую базу данных Postgres, а затем сгенерируйте и примените миграцию:

1
2
rails g migration add_attachments_to_posts attachments:json
rails db:migrate

Если вы предпочитаете придерживаться SQLite, следуйте инструкциям, приведенным в документации Carrierwave.

Теперь смонтируйте загрузчики (обратите внимание на форму множественного числа!):

1
mount_uploaders :attachments, ImageUploader

Я использую тот же загрузчик для вложений, но, конечно, вы можете создать новый с другой конфигурацией.

Добавьте поле с несколькими файлами в форму:

1
2
3
4
<div>
    <%= f.label :attachments %>
    <%= f.file_field :attachments, multiple: true %>
</div>

Пока поле attachments будет содержать массив, это должно быть разрешено следующим образом:

1
params.require(:post).permit(:title, :body, :image, :remove_image, :image_cache, :remote_image_url, attachments: [])

Наконец, вы можете перебирать вложения к сообщению и отображать их как обычно:

1
2
3
4
5
6
7
<% if @post.attachments?
  <ul>
    <% @post.attachments.each do |attachment|
      <li><%= link_to(image_tag(attachment.thumb.url, alt: ‘Image’), attachment.url, target: ‘_blank’) %></li>
    <% end %>
  </ul>
<% end %>

Обратите внимание, что каждое вложение будет иметь миниатюру, настроенную в нашем ImageUploader . Ницца!

Придерживаться файлового хранилища не всегда удобно и / или возможно, так как, например, в Heroku невозможно хранить пользовательские файлы. Поэтому вы можете спросить, как объединить Carrierwave с облачным хранилищем Amazon S3? Ну, это тоже довольно простая задача . Carrierwave зависит от драгоценного камня fog-aws для реализации этой функции:

1
gem «fog-aws»

Установите это:

1
bundle install

Давайте создадим инициализатор для Carrierwave и настроим облачное хранилище глобально:

01
02
03
04
05
06
07
08
09
10
CarrierWave.configure do |config|
  config.fog_provider = ‘fog/aws’
  config.fog_credentials = {
      provider: ‘AWS’,
      aws_access_key_id: ENV[‘S3_KEY’],
      aws_secret_access_key: ENV[‘S3_SECRET’],
      region: ENV[‘S3_REGION’],
  }
  config.fog_directory = ENV[‘S3_BUCKET’]
end

Есть несколько других доступных опций, которые можно найти в документации .

Я использую гем dotenv-rails для безопасной установки переменных окружения, но вы можете выбрать любой другой вариант. Тем не менее, убедитесь, что ваша пара ключей S3 не доступна публично, потому что в противном случае любой может загрузить что-либо в ваше ведро!

Затем замените строку storage :file на:

1
storage :fog

Помимо S3, Carrierwave поддерживает загрузку в Google Storage и Rackspace . Эти сервисы также просты в настройке.

Это на сегодня! Мы рассмотрели все основные функции Carrierwave, и теперь вы можете начать использовать его в своих проектах. У него есть некоторые дополнительные опции, так что просмотрите документацию .

Если вы застряли, не стесняйтесь оставлять свои вопросы. Кроме того, может быть полезно взглянуть на вики Carrierwave , где размещены полезные статьи «как делать», отвечающие на многие распространенные вопросы.

Поэтому я благодарю вас за то, что вы остались со мной, и счастливого кодирования!