Статьи

Загрузка файлов с помощью Rails и Dragonfly

Некоторое время назад я написал статью « Загрузка файлов с помощью Rails и Shrine», в которой объяснялось, как внедрить функцию загрузки файлов в приложение Rails с помощью гема Shrine. Однако существует множество подобных решений, и одним из моих любимых является Dragonfly — простое в использовании решение для загрузки Rails и Rack, созданное Марком Эвансом.

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

В этой статье я проведу вас через настройку Dragonfly и объясню, как использовать его основные функции. Вы узнаете, как:

  • Интегрируйте Dragonfly в свое приложение
  • Настройте модели для работы с Dragonfly
  • Введите основной механизм загрузки
  • Ввести проверки
  • Генерация миниатюр изображений
  • Выполнить обработку файла
  • Хранить метаданные для загруженных файлов
  • Подготовить приложение к развертыванию

Чтобы сделать вещи более интересными, мы собираемся создать небольшое музыкальное приложение. Он представит альбомы и связанные с ними песни, которыми можно управлять и воспроизводить на веб-сайте.

Исходный код этой статьи доступен на GitHub . Вы также можете проверить рабочую демонстрацию приложения.

Для начала создайте новое приложение Rails без набора тестов по умолчанию:

1
rails new UploadingWithDragonfly -T

В этой статье я буду использовать Rails 5, но большинство описанных концепций применимо и к более старым версиям.

Наш небольшой музыкальный сайт будет содержать две модели: Album и Song . А пока давайте создадим первое со следующими полями:

  • title ( string ) — содержит название альбома
  • singer ( string ) — исполнитель альбома
  • image_uid ( string ) — специальное поле для хранения изображения предварительного просмотра альбома. Это поле может называться как угодно, но оно должно содержать суффикс _uid как указано в документации по Dragonfly .

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

1
2
rails g model Album title:string singer:string image_uid:string
rails db:migrate

Теперь давайте создадим очень общий контроллер для управления альбомами со всеми действиями по умолчанию:

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
45
46
47
48
49
50
class AlbumsController < ApplicationController
  def index
    @albums = Album.all
  end
 
  def show
    @album = Album.find(params[:id])
  end
 
  def new
    @album = Album.new
  end
 
  def create
    @album = Album.new(album_params)
    if @album.save
      flash[:success] = ‘Album added!’
      redirect_to albums_path
    else
      render :new
    end
  end
 
  def edit
    @album = Album.find(params[:id])
  end
 
  def update
    @album = Album.find(params[:id])
    if @album.update_attributes(album_params)
      flash[:success] = ‘Album updated!’
      redirect_to albums_path
    else
      render :edit
    end
  end
 
  def destroy
    @album = Album.find(params[:id])
    @album.destroy
    flash[:success] = ‘Album removed!’
    redirect_to albums_path
  end
 
  private
 
  def album_params
    params.require(:album).permit(:title, :singer)
  end
end

Наконец, добавьте маршруты:

1
resources :albums

Пришло время Стрекозе выйти в центр внимания. Сначала добавьте драгоценный камень в Gemfile :

1
gem ‘dragonfly’

Бегать:

1
2
bundle install
rails generate dragonfly

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

Следующее, что нужно сделать, это оснастить нашу модель методами Dragonfly. Это делается с помощью dragonfly_accessor :

1
dragonfly_accessor :image

Обратите внимание, что здесь я говорю :image — оно напрямую относится к столбцу image_uid который мы создали в предыдущем разделе. Если вы, например, назвали свой столбец photo_uid , то метод dragonfly_accessor должен получить :photo в качестве аргумента.

Если вы используете Rails 4 или 5, другой важный шаг — пометить поле :image (не :image_uid !) Как разрешено в контроллере:

1
params.require(:album).permit(:title, :singer, :image)

Вот и все — мы готовы создать представления и начать загружать наши файлы!

Начните с представления индекса:

1
2
3
4
5
6
7
<h1>Albums</h1>
 
<%= link_to ‘Add’, new_album_path %>
 
<ul>
  <%= render @albums %>
</ul>

Теперь частичное:

1
2
3
4
5
6
7
<li>
  <%= image_tag(album.image.url, alt: album.title) if album.image_stored?
  <%= link_to album.title, album_path(album) %> by
  <%= album.singer %>
  |
  |
</li>

Здесь нужно отметить два метода Dragonfly:

  • album.image.url возвращает путь к изображению.
  • album.image_stored? говорит, есть ли запись на месте загруженного файла.

Теперь добавьте новые и отредактируйте страницы:

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

Форма ничего необычного, но еще раз обратите внимание, что мы говорим :image , а не :image_uid , при рендеринге файла ввода.

Теперь вы можете загрузить сервер и протестировать функцию загрузки!

Таким образом, пользователи могут создавать и редактировать альбомы, но есть проблема: у них нет возможности удалить изображение, только заменить его другим. К счастью, это очень легко исправить, установив флажок «удалить изображение»:

1
2
3
4
5
<% if @album.image_thumb_stored?
    <%= image_tag(@album.image.url, alt: @album.title) %>
    <%= f.label :remove_image %>
    <%= f.check_box :remove_image %>
<% end %>

Если с альбомом связано изображение, мы отображаем его и отображаем флажок. Если этот флажок установлен, изображение будет удалено. Обратите внимание, что если ваше поле называется photo_uid , то соответствующим методом для удаления вложения будет remove_photo . Просто, не правда ли?

Единственное, что нужно сделать, это разрешить атрибут remove_image в вашем контроллере:

1
params.require(:album).permit(:title, :singer, :image, :remove_image)

На этом этапе все работает нормально, но мы вообще не проверяем ввод пользователя, что не особенно хорошо. Поэтому давайте добавим проверки для модели Album:

1
2
3
4
validates :title, presence: true
validates :singer, presence: true
validates :image, presence: true
validates_property :width, of: :image, in: (0..900)

validates_property — метод Dragonfly, который может проверять различные аспекты вашего вложения: вы можете проверить расширение файла, тип MIME, размер и т. д.

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

01
02
03
04
05
06
07
08
09
10
11
<% if object.errors.any?
  <div>
    <h4>The following errors were found:</h4>
 
    <ul>
      <% object.errors.full_messages.each do |msg|
        <li><%= msg %></li>
      <% end %>
    </ul>
  </div>
<% end %>

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

1
2
3
4
<%= form_for @album do |f|
    <%= render ‘shared/errors’, object: @album %>
    <%# … %>
<% end %>

Немного стилизуйте поля с ошибками, чтобы визуально изобразить их:

1
2
3
4
5
6
7
8
9
.field_with_errors {
  display: inline;
  label {
    color: red;
  }
  input {
    background-color: lightpink;
  }
}

Введя проверки, мы столкнулись с еще одной проблемой (довольно типичный сценарий, а?): Если пользователь допустил ошибки при заполнении формы, ему или ей нужно будет снова выбрать файл после нажатия кнопки « Отправить» .

Dragonfly также может помочь вам решить эту проблему, используя скрытое поле retained_* :

1
<%= f.hidden_field :retained_image %>

Не забудьте также разрешить это поле:

1
params.require(:album).permit(:title, :singer, :image, :remove_image, :retained_image)

Теперь изображение будет сохраняться между запросами! Единственная небольшая проблема, однако, заключается в том, что при загрузке файла по-прежнему будет отображаться сообщение «выберите файл», но это можно исправить с помощью некоторого стиля и небольшого количества JavaScript.

Изображения, загруженные нашими пользователями, могут иметь очень разные размеры, что может (и, вероятно, будет) оказывать негативное влияние на дизайн сайта. Вы, вероятно, хотели бы уменьшить изображения до некоторых фиксированных размеров, и, конечно, это возможно, используя стили width и height . Это, однако, не оптимальный подход: браузеру все равно придется загружать полноразмерные изображения, а затем уменьшать их.

Другим вариантом (который обычно намного лучше) является создание миниатюр изображений с некоторыми предварительно заданными размерами на сервере. Это действительно просто сделать с помощью Dragonfly:

1
2
3
4
<li>
  <%= image_tag(album.image.thumb(‘250×250#’).url, alt: album.title) if album.image_stored?
  <%# … %>
</li>

250x250 — это, конечно, размеры, тогда как # — это геометрия, которая означает «изменить размер и обрезать, если необходимо, чтобы сохранить соотношение сторон с центральной силой тяжести». Вы можете найти информацию о других геометриях на сайте Dragonfly .

Метод thumb основан на ImageMagick — отличном решении для создания и управления изображениями. Поэтому, чтобы увидеть демонстрацию работы локально, вам нужно установить ImageMagick (поддерживаются все основные платформы).

Поддержка ImageMagick включена по умолчанию в инициализаторе Dragonfly:

1
plugin :imagemagick

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

Первый вариант включает введение нового столбца для хранения миниатюр и настройку метода dragonfly_accessor . Создайте и примените новую миграцию:

1
2
rails g migration add_image_thumb_uid_to_albums image_thumb_uid:string
rails db:migrate

Теперь измените модель:

1
2
3
4
5
dragonfly_accessor :image do
    copy_to(:image_thumb){|a|
end
 
dragonfly_accessor :image_thumb

Обратите внимание, что теперь первый вызов dragonfly_accessor отправляет блок, который фактически генерирует для нас миниатюру, и копирует его в image_thumb . Теперь просто используйте метод image_thumb в ваших представлениях:

1
<%= image_tag(album.image_thumb.url, alt: album.title) if album.image_thumb_stored?

Это простейшее решение, но оно не рекомендуется официальными документами и, что еще хуже, на момент написания не работает с полями retained_* .

Поэтому позвольте мне показать вам другой вариант: создание миниатюр на лету. Это включает в себя создание новой модели и настройку конфигурационного файла Dragonfly. Во-первых, модель:

1
2
rails g model Thumb uid:string job:string
rake db:migrate

Таблица thumbs будет содержать ваши миниатюры, но они будут созданы по требованию. Чтобы это произошло, нам нужно переопределить метод url внутри инициализатора Dragonfly:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
Dragonfly.app.configure do
    define_url do |app, job, opts|
        thumb = Thumb.find_by_job(job.signature)
        if thumb
          app.datastore.url_for(thumb.uid, :scheme => ‘https’)
        else
          app.server.url_for(job)
        end
    end
     
    before_serve do |job, env|
        uid = job.store
         
        Thumb.create!(
            :uid => uid,
            :job => job.signature
        )
    end
    # …
end

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

1
DRAGONFLY: shell command: «convert» «some_path/public/system/dragonfly/development/2017/02/08/3z5p5nvbmx_Folder.jpg» «-resize» «250×250^^» «-gravity» «Center» «-crop» «250×250+0+0» «+repage» «some_path/20170208-1692-1xrqzc9.jpg»

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

Вы можете выполнять практически любые манипуляции с изображениями после их загрузки. Это можно сделать внутри обратного вызова after_assign . Давайте, например, преобразуем все наши изображения в формат JPEG с качеством 90%:

1
2
3
dragonfly_accessor :image do
    after_assign {|a|
end

Есть еще много действий, которые вы можете выполнять: вращать и обрезать изображения, кодировать в другом формате, писать на них текст, смешивать с другими изображениями (например, для размещения водяного знака) и т. Д. Чтобы увидеть другие примеры, обратитесь к раздел ImageMagick на сайте Dragonfly.

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

1
2
rails g model Song album:belongs_to title:string track_uid:string
rails db:migrate

Подключите методы Dragonfly, как мы делали для модели Album :

1
dragonfly_accessor :track

Не забудьте установить отношение has_many :

1
has_many :songs, dependent: :destroy

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

1
2
3
resources :albums do
    resources :songs, only: [:new, :create]
end

Создайте очень простой контроллер (еще раз, не забудьте разрешить поле track ):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class SongsController < ApplicationController
  def new
    @album = Album.find(params[:album_id])
    @song = @album.songs.build
  end
 
  def create
    @album = Album.find(params[:album_id])
    @song = @album.songs.build(song_params)
    if @song.save
      flash[:success] = «Song added!»
      redirect_to album_path(@album)
    else
      render :new
    end
  end
 
  private
 
  def song_params
    params.require(:song).permit(:title, :track)
  end
end

Показать песни и ссылку, чтобы добавить новую:

1
2
3
4
5
6
7
8
<h1><%= @album.title %></h1>
<h2>by <%= @album.singer %></h2>
 
<%= link_to ‘Add song’, new_album_song_path(@album) %>
 
<ol>
  <%= render @album.songs %>
</ol>

Код формы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
<h1>Add song to <%= @album.title %></h1>
 
<%= form_for [@album, @song] do |f|
  <div>
    <%= f.label :title %>
    <%= f.text_field :title %>
  </div>
 
  <div>
    <%= f.label :track %>
    <%= f.file_field :track %>
  </div>
 
  <%= f.submit %>
<% end %>

Наконец, добавьте частичку _song :

1
2
3
4
<li>
  <%= audio_tag song.track.url, controls: true %>
  <%= song.title %>
</li>

Здесь я использую audio тег HTML5, который не будет работать для старых браузеров. Так что, если вы хотите поддерживать такие браузеры, используйте полифилл .

Как видите, весь процесс очень прост. Dragonfly на самом деле не волнует, какой тип файла вы хотите загрузить; все, что вам нужно сделать, это предоставить метод dragonfly_accessor , добавить правильное поле, разрешить его и визуализировать тег ввода файла.

Когда я открываю плейлист, я ожидаю увидеть дополнительную информацию о каждой песне, такую ​​как ее продолжительность или битрейт. Конечно, по умолчанию эта информация нигде не хранится, но мы можем это легко исправить. Dragonfly позволяет нам предоставлять дополнительные данные о каждом загруженном файле и получать его позже, используя meta метод.

Однако, когда мы работаем с аудио или видео, все немного сложнее, потому что для получения их метаданных необходим специальный гем streamio-ffmpeg . Этот драгоценный камень, в свою очередь, полагается на FFmpeg , поэтому для продолжения вам потребуется установить его на свой ПК.

Добавьте streamio-ffmpeg в Gemfile :

1
gem ‘streamio-ffmpeg’

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

1
bundle install

Теперь мы можем использовать тот же after_assign вызов after_assign уже видели в предыдущих разделах:

1
2
3
4
5
6
7
8
dragonfly_accessor :track do
    after_assign do |a|
      song = FFMPEG::Movie.new(a.path)
      mm, ss = song.duration.divmod(60).map {|n|
      a.meta[‘duration’] = «#{mm}:#{ss}»
      a.meta[‘bitrate’] = song.bitrate ?
    end
end

Обратите внимание, что здесь я использую метод path , а не url , потому что на данный момент мы работаем с временным файлом. Затем мы просто извлекаем длительность песни (конвертируя ее в минуты и секунды с ведущими нулями) и битрейт (конвертируя ее в килобайты в секунду).

Наконец, отобразите метаданные в представлении:

1
2
3
4
<li>
  <%= audio_tag song.track.url, controls: true %>
  <%= song.title %> (<%= song.track.meta[‘duration’] %>, <%= song.track.meta[‘bitrate’] %>Kb/s)
</li>

Если вы проверите содержимое в папке public / system / dragonfly (расположение по умолчанию для размещения загрузок), вы заметите некоторые файлы .yml — они хранят всю метаинформацию в формате YAML.

Последняя тема, которую мы рассмотрим сегодня, — это как подготовить ваше приложение перед развертыванием на облачной платформе Heroku. Основная проблема заключается в том, что Heroku не позволяет хранить пользовательские файлы (например, загружаемые файлы), поэтому мы должны полагаться на службу облачного хранения, такую ​​как Amazon S3. К счастью, Dragonfly легко интегрируется с ним.

Все, что вам нужно сделать, это зарегистрировать новую учетную запись в AWS (если у вас ее еще нет), создать пользователя с правами доступа к корзинам S3 и записать пару ключей пользователя в безопасном месте. Вы можете использовать пару корневых ключей, но это действительно не рекомендуется . Наконец, создайте ведро S3.

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

1
2
3
group :production do
  gem ‘dragonfly-s3_data_store’
end

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

1
bundle install

Затем настройте конфигурацию Dragonfly для использования S3 в производственной среде:

01
02
03
04
05
06
07
08
09
10
11
12
if Rails.env.production?
    datastore :s3,
              bucket_name: ENV[‘S3_BUCKET’],
              access_key_id: ENV[‘S3_KEY’],
              secret_access_key: ENV[‘S3_SECRET’],
              region: ENV[‘S3_REGION’],
              url_scheme: ‘https’
else
    datastore :file,
        root_path: Rails.root.join(‘public/system/dragonfly’, Rails.env),
        server_root: Rails.root.join(‘public’)
end

Чтобы предоставить переменные ENV в Heroku, используйте эту команду:

1
heroku config:add SOME_KEY=SOME_VALUE

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

Еще одна небольшая проблема, с которой я столкнулся при развертывании в Heroku, это отсутствие FFmpeg. Дело в том, что при создании нового приложения Heroku у него есть набор служб, которые обычно используются (например, ImageMagick доступен по умолчанию). Другие сервисы могут быть установлены как дополнения Heroku или в виде buildpack-пакетов . Чтобы добавить пакет сборки FFmpeg, выполните следующую команду:

1
heroku buildpacks:add https://github.com/HYPERHYPER/heroku-buildpack-ffmpeg.git

Теперь все готово, и вы можете поделиться своим музыкальным приложением со всем миром!

Это был долгий путь, не так ли? Сегодня мы обсудили Dragonfly — решение для загрузки файлов в Rails. Мы видели его базовую настройку, некоторые параметры конфигурации, генерацию миниатюр, обработку и хранение метаданных. Также мы интегрировали Dragonfly с сервисом Amazon S3 и подготовили наше приложение к развертыванию на производстве.

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

Спасибо, что остаетесь со мной, и до скорой встречи!