Существует множество файлов для загрузки файлов, таких как CarrierWave , Paperclip и Dragonfly . Все они имеют свои особенности, и, возможно, вы уже использовали хотя бы один из этих драгоценных камней.
Однако сегодня я хочу представить относительно новое, но очень крутое решение под названием Shrine , созданное Янко Марохничем. В отличие от некоторых других подобных драгоценных камней, он имеет модульный подход, что означает, что каждая функция упакована как модуль (или плагин в терминологии Shrine). Хотите поддержать проверки? Добавьте плагин. Хотите заняться обработкой файлов? Добавьте плагин! Мне очень нравится этот подход, поскольку он позволяет легко контролировать, какие функции будут доступны для какой модели.
В этой статье я собираюсь показать вам, как:
- интегрировать Shrine в приложение Rails
- настроить его (глобально и для каждого загрузчика)
- добавить возможность загрузки файлов
- обрабатывать файлы
- добавить правила проверки
- хранить дополнительные метаданные и использовать файловое облачное хранилище с Amazon S3
Исходный код этой статьи доступен на GitHub .
Рабочую демоверсию можно найти здесь .
Интегрирующий Храм
Для начала создайте новое приложение Rails без набора тестов по умолчанию:
1
|
rails new FileGuru -T
|
Я буду использовать Rails 5 для этой демонстрации, но большинство концепций применимо и к версиям 3 и 4.
Бросьте драгоценный камень в свой Gemfile:
1
|
gem «shrine»
|
Затем запустите:
1
|
bundle install
|
Теперь нам потребуется модель, которую я собираюсь назвать Photo
. Shrine хранит всю информацию, связанную с файлами, в специальном текстовом столбце, заканчивающемся суффиксом _data
. Создайте и примените соответствующую миграцию:
1
2
|
rails g model Photo title:string image_data:text
rails db:migrate
|
Обратите внимание, что для более старых версий Rails последняя команда должна быть:
1
|
rake db:migrate
|
Параметры конфигурации для Shrine могут быть установлены как глобально, так и для каждой модели. Глобальные настройки выполняются, разумеется, внутри файла инициализатора. Там я собираюсь подключить необходимые файлы и плагины . Плагины используются в Shrine для выделения частей функциональности в отдельные модули, предоставляя вам полный контроль над всеми доступными функциями. Например, есть плагины для проверки, обработки изображений, кэширования вложений и многого другого.
А пока давайте добавим два плагина: один для поддержки ActiveRecord, а другой для настройки ведения журнала . Они будут включены во всем мире. Также настройте хранилище файловой системы :
конфиг / Инициализаторы / shrine.rb
01
02
03
04
05
06
07
08
09
10
|
require «shrine»
require «shrine/storage/file_system»
Shrine.plugin :activerecord
Shrine.plugin :logging, logger: Rails.logger
Shrine.storages = {
cache: Shrine::Storage::FileSystem.new(«public», prefix: «uploads/cache»),
store: Shrine::Storage::FileSystem.new(«public», prefix: «uploads/store»),
}
|
Logger просто выведет некоторую отладочную информацию в консоль, чтобы вы сказали, сколько времени было потрачено на обработку файла. Это может пригодиться.
1
2
3
|
2015-10-09T20:06:06.676Z #25602: STORE[cache] ImageUploader[:avatar] User[29543] 1 file (0.1s)
2015-10-09T20:06:06.854Z #25602: PROCESS[store]: ImageUploader[:avatar] User[29543] 1-3 files (0.22s)
2015-10-09T20:06:07.133Z #25602: DELETE[destroyed]: ImageUploader[:avatar] User[29543] 3 files (0.07s)
|
Все загруженные файлы будут храниться в каталоге public / uploads . Я не хочу отслеживать эти файлы в Git, поэтому исключите эту папку:
.gitignore
1
|
public/uploads
|
Теперь создайте специальный класс «загрузчик», который будет размещать специфичные для модели настройки. На данный момент этот класс будет пустым:
модели / image_uploader.rb
1
2
|
class ImageUploader < Shrine
end
|
Наконец, включите этот класс в модель Photo
:
модели / photo.rb
1
|
include ImageUploader[:image]
|
[:image]
добавляет виртуальный атрибут, который будет использоваться при создании формы. Приведенную выше строку можно переписать так:
1
2
3
|
include ImageUploader.attachment(:image)
# or
include ImageUploader::Attachment.new(:image)
|
Ницца! Теперь модель оснащена функциональностью Shrine, и мы можем перейти к следующему шагу.
Контроллер, Представления и Маршруты
Для целей этой демонстрации нам понадобится только один контроллер для управления фотографиями. Страница index
будет служить корнем:
pages_controller.rb
1
2
3
4
5
|
class PhotosController < ApplicationController
def index
@photos = Photo.all
end
end
|
Вид:
просмотров / фото / index.html.erb
1
2
3
4
5
|
<h1>Photos</h1>
<%= link_to ‘Add Photo’, new_photo_path %>
<%= render @photos %>
|
Для рендеринга массива @photos
требуется частичное:
просмотров / фото / _photo.html.erb
1
2
3
4
5
6
|
<div>
<% if photo.image_data?
<%= image_tag photo.image_url %>
<% end %>
<p><%= photo.title %> |
</div>
|
image_data?
это метод, представленный ActiveRecord, который проверяет, есть ли в записи изображение.
image_url
— это метод Shrine, который просто возвращает путь к исходному изображению. Конечно, гораздо лучше вместо этого отобразить небольшую миниатюру, но об этом мы позаботимся позже.
Добавьте все необходимые маршруты:
конфиг / routes.rb
1
2
3
|
resources :photos, only: [:new, :create, :index, :edit, :update]
root ‘photos#index’
|
На этом все готово, и мы можем перейти к интересной части!
Загрузка файлов
В этом разделе я покажу вам, как добавить функциональность для фактической загрузки файлов. Действия контроллера очень просты:
photos_controller.rb
01
02
03
04
05
06
07
08
09
10
11
12
13
|
def new
@photo = Photo.new
end
def create
@photo = Photo.new(photo_params)
if @photo.save
flash[:success] = ‘Photo added!’
redirect_to photos_path
else
render ‘new’
end
end
|
Единственный недостаток в том, что для сильных параметров вы должны разрешить виртуальный атрибут image
, а не image_data
.
photos_controller.rb
1
2
3
4
5
|
private
def photo_params
params.require(:photo).permit(:title, :image)
end
|
Создайте new
вид:
просмотров / фото / new.html.erb
1
2
3
|
<h1>Add photo</h1>
<%= render ‘form’ %>
|
Частичная форма также тривиальна:
просмотров / фото / _form.html.erb
01
02
03
04
05
06
07
08
09
10
11
|
<%= form_for @photo do |f|
<%= render «shared/errors», object: @photo %>
<%= f.label :title %>
<%= f.text_field :title %>
<%= f.label :image %>
<%= f.file_field :image %>
<%= f.submit %>
<% end %>
|
Еще раз обратите внимание, что мы используем атрибут image
, а не image_data
.
Наконец, добавьте еще один фрагмент для отображения ошибок:
просмотров / общий / _errors.html.erb
1
2
3
4
5
6
7
8
9
|
<% if object.errors.any?
<h3>The following errors were found:</h3>
<ul>
<% object.errors.full_messages.each do |message|
<li><%= message %></li>
<% end %>
</ul>
<% end %>
|
Это почти все — вы можете начать загружать изображения прямо сейчас.
Validations
Конечно, чтобы завершить демонстрационное приложение, нужно проделать гораздо больше работы. Основная проблема заключается в том, что пользователи могут загружать абсолютно любой тип файла любого размера, что не особенно здорово. Поэтому добавьте еще один плагин для поддержки проверок :
конфиг / inititalizers / shrine.rb
1
|
Shrine.plugin :validation_helpers
|
Установите логику проверки для ImageUploader
:
модели / image_uploader.rb
1
2
3
4
|
Attacher.validate do
validate_max_size 1.megabyte, message: «is too large (max is 1 MB)»
validate_mime_type_inclusion [‘image/jpg’, ‘image/jpeg’, ‘image/png’]
end
|
Я разрешаю загружать только изображения JPG и PNG размером менее 1 МБ. Настройте эти правила, как считаете нужным.
MIME Типы
Еще одна важная вещь, которую стоит отметить, это то, что по умолчанию Shrine будет определять MIME-тип файла, используя HTTP-заголовок Content-Type. Этот заголовок передается браузером и устанавливается только в зависимости от расширения файла, что не всегда желательно.
Если вы хотите определить тип MIME на основе содержимого файла, используйте плагин define_mime_type . Я включу его в класс загрузчика, так как другие модели могут не требовать этой функциональности:
модели / image_uploader.rb
1
|
plugin :determine_mime_type
|
Этот плагин будет использовать файловую утилиту Linux по умолчанию.
Кэширование прикрепленных изображений
В настоящее время, когда пользователь отправляет форму с неверными данными, форма будет отображаться снова с ошибками, представленными выше. Проблема, однако, заключается в том, что прикрепленное изображение будет потеряно, и пользователь должен будет выбрать его еще раз. Это очень легко исправить, используя еще один плагин cached_attachment_data :
модели / image_uploader.rb
1
|
plugin :cached_attachment_data
|
Теперь просто добавьте скрытое поле в вашу форму.
просмотров / фото / _form.html.erb
1
2
3
|
<%= f.hidden_field :image, value: @photo.cached_image_data %>
<%= f.label :image %>
<%= f.file_field :image %>
|
Редактирование фото
Теперь изображения можно загружать, но редактировать их невозможно, поэтому давайте исправим это прямо сейчас. Действия соответствующего контроллера несколько тривиальны:
photos_controller.rb
01
02
03
04
05
06
07
08
09
10
11
12
13
|
def edit
@photo = Photo.find(params[:id])
end
def update
@photo = Photo.find(params[:id])
if @photo.update_attributes(photo_params)
flash[:success] = ‘Photo edited!’
redirect_to photos_path
else
render ‘edit’
end
end
|
Будет использована та же часть _form
:
просмотров / фото / edit.html.erb
1
2
3
|
<h1>Edit Photo</h1>
<%= render ‘form’ %>
|
Хорошо, но недостаточно: пользователи по-прежнему не могут удалить загруженное изображение. Для этого нам понадобится — угадайте, что — другой плагин :
модели / image_uploader.rb
1
|
plugin :remove_attachment
|
Он использует виртуальный атрибут с именем :remove_image
, поэтому разрешите его внутри контроллера:
photos_controller.rb
1
2
3
|
def photo_params
params.require(:photo).permit(:title, :image, :remove_image)
end
|
Теперь просто установите флажок, чтобы удалить изображение, если в записи есть вложение:
просмотров / фото / _form.html.erb
1
2
3
|
<% if @photo.image_data?
Remove attachment: <%= f.check_box :remove_image %>
<% end %>
|
Генерация миниатюрного изображения
В настоящее время мы показываем оригинальные изображения, что не является лучшим подходом для предварительного просмотра: фотографии могут быть большими и занимать слишком много места. Конечно, вы можете просто использовать CSS-атрибуты width
и height
, но это тоже плохая идея. Видите ли, даже если для изображения задано маленькое изображение с использованием стилей, пользователю все равно потребуется загрузить исходный файл, который может быть довольно большим.
Поэтому гораздо лучше создать небольшое предварительное изображение на стороне сервера во время начальной загрузки. Это включает в себя два плагина и два дополнительных драгоценных камня. Во-первых, бросьте в драгоценные камни:
1
2
|
gem «image_processing»
gem «mini_magick», «>= 4.3.5»
|
Image_processing — это особая жемчужина, созданная автором Shrine. В нем представлены некоторые высокоуровневые вспомогательные методы для работы с изображениями. Этот драгоценный камень, в свою очередь, использует mini_magick , обертку Ruby для ImageMagick. Как вы уже догадались, вам понадобится ImageMagick в вашей системе, чтобы запустить эту демонстрацию.
Установите эти новые драгоценные камни:
1
|
bundle install
|
Теперь включите плагины вместе с их зависимостями:
модели / image_uploader.rb
1
2
3
4
5
6
7
8
|
require «image_processing/mini_magick»
class ImageUploader < Shrine
include ImageProcessing::MiniMagick
plugin :processing
plugin :versions
# other code…
end
|
Обработка — это плагин, который позволяет нам манипулировать изображением (например, уменьшить его, повернуть, преобразовать в другой формат и т. Д.). Версии , в свою очередь, позволяют нам иметь изображение в разных вариантах. Для этой демонстрации будут сохранены две версии: «оригинал» и «большой палец» (размер до 300x300
).
Вот код для обработки изображения и сохранения его двух версий:
модели / image_uploader.rb
1
2
3
4
5
|
class ImageUploader < Shrine
process(:store) do |io, context|
{ original: io, thumb: resize_to_limit!(io.download, 300, 300) }
end
end
|
resize_to_limit!
это метод, предоставляемый гемом image_processing Он просто сжимает изображение до 300x300
если оно больше, и ничего не делает, если оно меньше. Кроме того, он сохраняет оригинальное соотношение сторон.
Теперь при отображении изображения вам просто нужно предоставить аргумент :original
или :thumb
для метода image_url
:
просмотров / фото / _photo.html.erb
1
2
3
4
5
6
|
<div>
<% if photo.image_data?
<%= image_tag photo.image_url(:thumb) %>
<% end %>
<p><%= photo.title %> |
</div>
|
То же самое можно сделать внутри формы:
просмотров / фото / _form.html.erb
1
2
3
4
|
<% if @photo.image_data?
<%= image_tag @photo.image_url(:thumb) %>
Remove attachment: <%= f.check_box :remove_image %>
<% end %>
|
Чтобы автоматически удалить обработанные файлы после завершения загрузки, вы можете добавить плагин с именем delete_raw :
модели / image_uploader.rb
1
|
plugin :delete_raw
|
Метаданные изображения
Помимо собственно рендеринга изображения, вы также можете получить его метаданные. Давайте, например, отобразим размер оригинальной фотографии и тип MIME:
просмотров / фото / _photo.html.erb
01
02
03
04
05
06
07
08
09
10
|
<div>
<% if photo.image_data?
<%= image_tag photo.image_url(:thumb) %>
<p>
Size <%= photo.image[:original].size %> bytes<br>
MIME type <%= photo.image[:original].mime_type %><br>
</p>
<% end %>
<p><%= photo.title %> |
</div>
|
Как насчет его размеров? К сожалению, они не сохраняются по умолчанию, но это возможно с помощью плагина с именем store_dimensions .
Размеры изображения
Плагин store_dimensions использует гем fastimage , поэтому подключите его сейчас:
1
|
gem ‘fastimage’
|
Не забудьте запустить:
1
|
bundle install
|
Теперь просто включите плагин:
модели / image_uploader.rb
1
|
plugin :store_dimensions
|
И отобразите размеры, используя методы width
и height
:
просмотров / фото / _photo.html.erb
01
02
03
04
05
06
07
08
09
10
11
|
<div>
<% if photo.image_data?
<%= image_tag photo.image_url(:thumb) %>
<p>
Size <%= photo.image[:original].size %> bytes<br>
MIME type <%= photo.image[:original].mime_type %><br>
Dimensions <%= «#{photo.image[:original].width}x#{photo.image[:original].height}» %>
</p>
<% end %>
<p><%= photo.title %> |
</div>
|
Также доступен метод dimensions
который возвращает массив, содержащий ширину и высоту (например, [500, 750]
).
Переезд в Облако
Разработчики часто выбирают облачные сервисы для размещения загруженных файлов, и Shrine предоставляет такую возможность. В этом разделе я покажу вам, как загружать файлы в Amazon S3.
В качестве первого шага добавьте еще два драгоценных камня в Gemfile :
1
2
3
4
|
gem «aws-sdk», «~> 2.1»
group :development do
gem ‘dotenv-rails’
end
|
aws-sdk необходим для работы с SDK S3, тогда как dotenv-rails будут использоваться для управления переменными среды в процессе разработки.
1
|
bundle install
|
Прежде чем продолжить, вы должны получить пару ключей для доступа к S3 через API. Чтобы получить его, войдите (или зарегистрируйтесь) в консоль Amazon Web Services и перейдите в раздел «Учетные данные безопасности»> «Пользователи» . Создайте пользователя с разрешениями для манипулирования файлами на S3. Вот простая политика, представляющая полный доступ к S3:
01
02
03
04
05
06
07
08
09
10
|
{
«Version»: «2016-11-14»,
«Statement»: [
{
«Effect»: «Allow»,
«Action»: «s3:*»,
«Resource»: «*»
}
]
}
|
Загрузите созданную пару ключей пользователя. В качестве альтернативы вы можете использовать корневые ключи доступа, но я настоятельно не рекомендую вам делать это, поскольку это очень небезопасно.
Затем создайте корзину S3 для размещения ваших файлов и добавьте файл в корневой каталог проекта для размещения вашей конфигурации:
.env
1
2
3
4
|
S3_KEY=YOUR_KEY
S3_SECRET=YOUR_SECRET
S3_BUCKET=YOUR_BUCKET
S3_REGION=YOUR_REGION
|
Никогда не открывайте этот файл публично, и убедитесь, что вы исключили его из Git:
.gitignore
1
|
.env
|
Теперь измените глобальную конфигурацию Shrine и представьте новое хранилище:
конфиг / Инициализаторы / shrine.rb
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
require «shrine»
require «shrine/storage/s3»
s3_options = {
access_key_id: ENV[‘S3_KEY’],
secret_access_key: ENV[‘S3_SECRET’],
region: ENV[‘S3_REGION’],
bucket: ENV[‘S3_BUCKET’],
}
Shrine.storages = {
cache: Shrine::Storage::FileSystem.new(«public», prefix: «uploads/cache»),
store: Shrine::Storage::S3.new(prefix: «store», **s3_options),
}
|
Это оно! Никаких изменений не нужно вносить в другие части приложения, и вы можете сразу протестировать это новое хранилище. Если вы получаете ошибки от S3, связанные с неправильными ключами, убедитесь, что вы точно скопировали ключ и секрет, без пробелов и невидимых специальных символов.
Вывод
Мы подошли к концу этой статьи. Надеюсь, к настоящему моменту вы чувствуете себя уверенно в использовании Shrine и хотите использовать его в одном из своих проектов. Мы обсудили многие функции этого драгоценного камня, но есть и другие, такие как возможность хранить дополнительный контекст вместе с файлами и механизм прямой загрузки.
Поэтому просмотрите документацию Shrine и ее официальный сайт , где подробно описаны все доступные плагины. Если у вас есть другие вопросы об этом драгоценном камне, не стесняйтесь их публиковать. Я благодарю вас за то, что вы со мной, и скоро увидимся!