Статьи

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

Существует множество файлов для загрузки файлов, таких как 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, а другой для настройки ведения журнала . Они будут включены во всем мире. Также настройте хранилище файловой системы :

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, поэтому исключите эту папку:

1
public/uploads

Теперь создайте специальный класс «загрузчик», который будет размещать специфичные для модели настройки. На данный момент этот класс будет пустым:

1
2
class ImageUploader < Shrine
end

Наконец, включите этот класс в модель Photo :

1
include ImageUploader[:image]

[:image] добавляет виртуальный атрибут, который будет использоваться при создании формы. Приведенную выше строку можно переписать так:

1
2
3
include ImageUploader.attachment(:image)
 # or
 include ImageUploader::Attachment.new(:image)

Ницца! Теперь модель оснащена функциональностью Shrine, и мы можем перейти к следующему шагу.

Для целей этой демонстрации нам понадобится только один контроллер для управления фотографиями. Страница index будет служить корнем:

1
2
3
4
5
class PhotosController < ApplicationController
  def index
    @photos = Photo.all
  end
end

Вид:

1
2
3
4
5
<h1>Photos</h1>
 
<%= link_to ‘Add Photo’, new_photo_path %>
 
<%= render @photos %>

Для рендеринга массива @photos требуется частичное:

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, который просто возвращает путь к исходному изображению. Конечно, гораздо лучше вместо этого отобразить небольшую миниатюру, но об этом мы позаботимся позже.

Добавьте все необходимые маршруты:

1
2
3
resources :photos, only: [:new, :create, :index, :edit, :update]
 
 root ‘photos#index’

На этом все готово, и мы можем перейти к интересной части!

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

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 .

1
2
3
4
5
private
 
def photo_params
    params.require(:photo).permit(:title, :image)
end

Создайте new вид:

1
2
3
<h1>Add photo</h1>
 
<%= render ‘form’ %>

Частичная форма также тривиальна:

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 .

Наконец, добавьте еще один фрагмент для отображения ошибок:

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 %>

Это почти все — вы можете начать загружать изображения прямо сейчас.

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

1
Shrine.plugin :validation_helpers

Установите логику проверки для ImageUploader :

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 МБ. Настройте эти правила, как считаете нужным.

Еще одна важная вещь, которую стоит отметить, это то, что по умолчанию Shrine будет определять MIME-тип файла, используя HTTP-заголовок Content-Type. Этот заголовок передается браузером и устанавливается только в зависимости от расширения файла, что не всегда желательно.

Если вы хотите определить тип MIME на основе содержимого файла, используйте плагин define_mime_type . Я включу его в класс загрузчика, так как другие модели могут не требовать этой функциональности:

1
plugin :determine_mime_type

Этот плагин будет использовать файловую утилиту Linux по умолчанию.

В настоящее время, когда пользователь отправляет форму с неверными данными, форма будет отображаться снова с ошибками, представленными выше. Проблема, однако, заключается в том, что прикрепленное изображение будет потеряно, и пользователь должен будет выбрать его еще раз. Это очень легко исправить, используя еще один плагин cached_attachment_data :

1
plugin :cached_attachment_data

Теперь просто добавьте скрытое поле в вашу форму.

1
2
3
<%= f.hidden_field :image, value: @photo.cached_image_data %>
<%= f.label :image %>
<%= f.file_field :image %>

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

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 :

1
2
3
<h1>Edit Photo</h1>
 
<%= render ‘form’ %>

Хорошо, но недостаточно: пользователи по-прежнему не могут удалить загруженное изображение. Для этого нам понадобится — угадайте, что — другой плагин :

1
plugin :remove_attachment

Он использует виртуальный атрибут с именем :remove_image , поэтому разрешите его внутри контроллера:

1
2
3
def photo_params
    params.require(:photo).permit(:title, :image, :remove_image)
end

Теперь просто установите флажок, чтобы удалить изображение, если в записи есть вложение:

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

Теперь включите плагины вместе с их зависимостями:

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 ).

Вот код для обработки изображения и сохранения его двух версий:

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 :

1
2
3
4
5
6
<div>
  <% if photo.image_data?
    <%= image_tag photo.image_url(:thumb) %>
  <% end %>
  <p><%= photo.title %> |
</div>

То же самое можно сделать внутри формы:

1
2
3
4
<% if @photo.image_data?
    <%= image_tag @photo.image_url(:thumb) %>
    Remove attachment: <%= f.check_box :remove_image %>
<% end %>

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

1
plugin :delete_raw

Помимо собственно рендеринга изображения, вы также можете получить его метаданные. Давайте, например, отобразим размер оригинальной фотографии и тип MIME:

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

Теперь просто включите плагин:

1
plugin :store_dimensions

И отобразите размеры, используя методы width и height :

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 для размещения ваших файлов и добавьте файл в корневой каталог проекта для размещения вашей конфигурации:

1
2
3
4
S3_KEY=YOUR_KEY
S3_SECRET=YOUR_SECRET
S3_BUCKET=YOUR_BUCKET
S3_REGION=YOUR_REGION

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

1
.env

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

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 и ее официальный сайт , где подробно описаны все доступные плагины. Если у вас есть другие вопросы об этом драгоценном камне, не стесняйтесь их публиковать. Я благодарю вас за то, что вы со мной, и скоро увидимся!