Некоторое время назад я написал статью « Загрузка файлов с помощью 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
|
Теперь давайте создадим очень общий контроллер для управления альбомами со всеми действиями по умолчанию:
albums_controller.rb
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
|
Наконец, добавьте маршруты:
конфиг / routes.rb
1
|
resources :albums
|
Интеграция Стрекоза
Пришло время Стрекозе выйти в центр внимания. Сначала добавьте драгоценный камень в Gemfile :
Gemfile
1
|
gem ‘dragonfly’
|
Бегать:
1
2
|
bundle install
rails generate dragonfly
|
Последняя команда создаст инициализатор с именем dragonfly.rb с конфигурацией по умолчанию. Пока отложим это, но вы можете прочитать о различных вариантах на официальном сайте Dragonfly .
Следующее, что нужно сделать, это оснастить нашу модель методами Dragonfly. Это делается с помощью dragonfly_accessor
:
модели / album.rb
1
|
dragonfly_accessor :image
|
Обратите внимание, что здесь я говорю :image
— оно напрямую относится к столбцу image_uid
который мы создали в предыдущем разделе. Если вы, например, назвали свой столбец photo_uid
, то метод dragonfly_accessor
должен получить :photo
в качестве аргумента.
Если вы используете Rails 4 или 5, другой важный шаг — пометить поле :image
(не :image_uid
!) Как разрешено в контроллере:
albums_controller.rb
1
|
params.require(:album).permit(:title, :singer, :image)
|
Вот и все — мы готовы создать представления и начать загружать наши файлы!
Создание видов
Начните с представления индекса:
просмотров / альбомы / index.html.erb
1
2
3
4
5
6
7
|
<h1>Albums</h1>
<%= link_to ‘Add’, new_album_path %>
<ul>
<%= render @albums %>
</ul>
|
Теперь частичное:
просмотров / альбомы / _album.html.erb
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?
говорит, есть ли запись на месте загруженного файла.
Теперь добавьте новые и отредактируйте страницы:
просмотров / альбомы / new.html.erb
1
2
3
|
<h1>Add album</h1>
<%= render ‘form’ %>
|
просмотров / альбомы / edit.html.erb
1
2
3
|
<h1>Edit <%= @album.title %></h1>
<%= render ‘form’ %>
|
просмотров / альбомы / _form.html.erb
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
, при рендеринге файла ввода.
Теперь вы можете загрузить сервер и протестировать функцию загрузки!
Удаление изображений
Таким образом, пользователи могут создавать и редактировать альбомы, но есть проблема: у них нет возможности удалить изображение, только заменить его другим. К счастью, это очень легко исправить, установив флажок «удалить изображение»:
просмотров / альбомы / _form.html.erb
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
в вашем контроллере:
albums_controller.rb
1
|
params.require(:album).permit(:title, :singer, :image, :remove_image)
|
Добавление проверок
На этом этапе все работает нормально, но мы вообще не проверяем ввод пользователя, что не особенно хорошо. Поэтому давайте добавим проверки для модели Album:
модели / album.rb
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, размер и т. д.
Теперь давайте создадим общий фрагмент для визуализации найденных ошибок:
просмотров / общий / _errors.html.erb
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 %>
|
Используйте это частичное внутри формы:
просмотров / альбомы / _form.html.erb
1
2
3
4
|
<%= form_for @album do |f|
<%= render ‘shared/errors’, object: @album %>
<%# … %>
<% end %>
|
Немного стилизуйте поля с ошибками, чтобы визуально изобразить их:
таблицы стилей / application.scss
1
2
3
4
5
6
7
8
9
|
.field_with_errors {
display: inline;
label {
color: red;
}
input {
background-color: lightpink;
}
}
|
Сохранение изображения между запросами
Введя проверки, мы столкнулись с еще одной проблемой (довольно типичный сценарий, а?): Если пользователь допустил ошибки при заполнении формы, ему или ей нужно будет снова выбрать файл после нажатия кнопки « Отправить» .
Dragonfly также может помочь вам решить эту проблему, используя скрытое поле retained_*
:
просмотров / альбомы / _form.html.erb
1
|
<%= f.hidden_field :retained_image %>
|
Не забудьте также разрешить это поле:
albums_controller.rb
1
|
params.require(:album).permit(:title, :singer, :image, :remove_image, :retained_image)
|
Теперь изображение будет сохраняться между запросами! Единственная небольшая проблема, однако, заключается в том, что при загрузке файла по-прежнему будет отображаться сообщение «выберите файл», но это можно исправить с помощью некоторого стиля и небольшого количества JavaScript.
Обработка изображений
Генерация миниатюр
Изображения, загруженные нашими пользователями, могут иметь очень разные размеры, что может (и, вероятно, будет) оказывать негативное влияние на дизайн сайта. Вы, вероятно, хотели бы уменьшить изображения до некоторых фиксированных размеров, и, конечно, это возможно, используя стили width
и height
. Это, однако, не оптимальный подход: браузеру все равно придется загружать полноразмерные изображения, а затем уменьшать их.
Другим вариантом (который обычно намного лучше) является создание миниатюр изображений с некоторыми предварительно заданными размерами на сервере. Это действительно просто сделать с помощью Dragonfly:
просмотров / альбомы / _album.html.erb
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:
конфиг / Инициализаторы / dragonfly.rb
1
|
plugin :imagemagick
|
Теперь миниатюры создаются, но они нигде не хранятся. Это означает, что каждый раз, когда пользователь посещает страницу альбомов, эскизы будут обновляться. Есть два способа преодолеть эту проблему: генерируя их после сохранения записи или выполняя генерацию на лету.
Первый вариант включает введение нового столбца для хранения миниатюр и настройку метода dragonfly_accessor
. Создайте и примените новую миграцию:
1
2
|
rails g migration add_image_thumb_uid_to_albums image_thumb_uid:string
rails db:migrate
|
Теперь измените модель:
модели / album.rb
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
в ваших представлениях:
просмотров / альбомы / _album.html.erb
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:
конфиг / Инициализаторы / dragonfly.rb
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
:
модели / song.rb
1
|
dragonfly_accessor :track
|
Не забудьте установить отношение has_many
:
модели / album.rb
1
|
has_many :songs, dependent: :destroy
|
Добавить новые маршруты. Песня всегда существует в рамках альбома, поэтому я сделаю эти маршруты вложенными:
конфиг / routes.rb
1
2
3
|
resources :albums do
resources :songs, only: [:new, :create]
end
|
Создайте очень простой контроллер (еще раз, не забудьте разрешить поле track
):
songs_controller.rb
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
|
Показать песни и ссылку, чтобы добавить новую:
просмотров / альбомы / show.html.erb
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>
|
Код формы:
просмотров / песни / new.html.erb
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 :
просмотров / песни / _song.html.erb
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 :
Gemfile
1
|
gem ‘streamio-ffmpeg’
|
Установите это:
1
|
bundle install
|
Теперь мы можем использовать тот же after_assign
вызов after_assign
уже видели в предыдущих разделах:
модели / song.rb
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
, потому что на данный момент мы работаем с временным файлом. Затем мы просто извлекаем длительность песни (конвертируя ее в минуты и секунды с ведущими нулями) и битрейт (конвертируя ее в килобайты в секунду).
Наконец, отобразите метаданные в представлении:
просмотров / песни / _song.html.erb
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. Основная проблема заключается в том, что Heroku не позволяет хранить пользовательские файлы (например, загружаемые файлы), поэтому мы должны полагаться на службу облачного хранения, такую как Amazon S3. К счастью, Dragonfly легко интегрируется с ним.
Все, что вам нужно сделать, это зарегистрировать новую учетную запись в AWS (если у вас ее еще нет), создать пользователя с правами доступа к корзинам S3 и записать пару ключей пользователя в безопасном месте. Вы можете использовать пару корневых ключей, но это действительно не рекомендуется . Наконец, создайте ведро S3.
Возвращаясь к нашему Rails-приложению, добавьте новый гем:
Gemfile
1
2
3
|
group :production do
gem ‘dragonfly-s3_data_store’
end
|
Установите это:
1
|
bundle install
|
Затем настройте конфигурацию Dragonfly для использования S3 в производственной среде:
конфиг / Инициализаторы / dragonfly.rb
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 в этой статье, поэтому обязательно зайдите на его официальный сайт, чтобы найти обширную документацию и полезные примеры. Если у вас есть другие вопросы или вы застряли с некоторыми примерами кода, не стесняйтесь обращаться ко мне.
Спасибо, что остаетесь со мной, и до скорой встречи!