Статьи

Лучшая загрузка файлов с помощью Dragonfly

татуировка либералла

Загрузка файлов является важной частью Интернета. Мы загружаем наши фотографии, видео и музыку, чтобы делиться ими. Мы отправляем электронные письма с кучей вложений; вероятно, каждый активный интернет-пользователь загружает что-то по крайней мере один раз в день (ну, это мое предположение :)).

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

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

  • Интеграция с ImageMagick, обработка и анализ изображений.
  • Как загрузить изображения в Amazon S3 вместо локальной файловой системы.

В качестве бонуса это можно сделать довольно быстро! Итак, начнем, хорошо?

Рабочую демонстрацию можно найти по адресу http://sitepoint-dragonfly.herokuapp.com/ .

Исходный код доступен на GitHub .

Стрекоза и альтернативы

Есть другие решения, похожие на Dragonfly, которые вы можете использовать в своих проектах. Наиболее популярными являются Carrierwave и Paperclip . Это хорошие решения, и они используются на многих сайтах.

Что хорошего в Dragonfly и почему вы должны попробовать его использовать?

  • Это действительно легко начать.
  • Он прекрасно интегрируется с ImageMagick.
  • Возможность работы с «волшебными» столбцами, которые могут автоматически хранить информацию о вложении.
  • Это позволяет спецификацию проверок и обратных вызовов
  • Он работает с широким спектром хранилищ данных или позволяет создавать свои собственные.
  • Он имеет каркас для создания пользовательских анализаторов и генераторов изображений, а также плагинов.
  • Он имеет множество расширений (каждое расширение предоставляется в своем собственном геме).
  • Он работает с Rails 3 и 4, а также с Ruby 1.8.7, 1.9.2, 1.9.3, 2.0.0, jRuby 1.7.8 и Rubinius 2.2.

Если вы не знакомы с этим драгоценным камнем, я надеюсь, что убедил вас (по крайней мере) взглянуть на него 🙂

Обратите внимание, что если вы все еще используете Dragonfly 0.9 или более раннюю версию, есть новая версия Dragonfly 1.0, в которой есть некоторые критические изменения Существует подробное руководство, объясняющее, как перейти на последнюю версию — я смог мигрировать в кратчайшие сроки.

Подготовка проекта

Я буду использовать Rails 4.1.1 для этой демонстрации, но вы можете реализовать то же решение с Rails 3

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

Итак, создайте новое приложение без набора тестов по умолчанию:

rails new uploader -T 

Откройте свой Gemfile и добавьте следующие драгоценные камни:

Gemfile

 gem 'dragonfly' gem 'dragonfly-s3_data_store' gem 'bootstrap-sass' group :production do gem 'rack-cache', :require => 'rack/cache' end 

dragonfly — это настраиваемый драгоценный камень Ruby для обработки изображений и других вложений, созданных Марком Эвансом, а также нашей основной темой сегодня.

dragonfly-s3_data_store — это расширение для dragonfly которое позволяет хранить файлы с помощью облачных сервисов Amazon S3 (об этом мы поговорим позже).

bootstrap-sass драгоценный камень, который приносит в Twitter Bootstrap. Это если для стайлинговых целей и не нужно использовать Dragonfly

rack-cache — это простое решение для кеширования на производстве
среда, которую вы можете использовать для небольших проектов (для больших вы должны реализовать обратный кеширующий прокси).

Не забудьте связать:

 bundle install 

Теперь пришло время подключить некоторые файлы Bootstrap

application.css.scss

 @import 'bootstrap'; 

и создайте PhotosController который пока будет пустым:

photos_controller.rb

 class PhotosController < ApplicationController end 

Интеграция со стрекозой

Создайте базовую конфигурацию Dragonfly:

 rails generate dragonfly 

Он создаст файл инициализатора Dragonfly (который находится в config/initializers/dragonfly.rb ).

Следующее, что нам нужно, это таблица для хранения информации о загруженных фотографиях. На данный момент в этой таблице будет только два столбца (не говоря уже о столбцах по умолчанию — id , created_at и updated_at ):

  • title ( string ) — заголовок, предоставленный пользователем.
  • image_uid ( string ) — путь к загруженной фотографии, созданной Dragonfly.

Эти команды создадут и применят миграцию:

 rails g model Photo image_uid:string title:string rake db:migrate 

Теперь откройте свою вновь созданную модель. Нам нужно интегрировать его с Dragonfly:

photo.rb

 [...] dragonfly_accessor :image [...] 

Обратите внимание, что в вашей таблице должен быть столбец image_uid если в модели указано dragonfly_accessor :image . Например, если вы хотите вызвать avatar вложения, то ваш столбец должен называться avatar_uid . Второе, что следует отметить, это то, что вы можете попросить Dragonfly автоматически сохранить исходное имя файла в так называемом «волшебном» столбце. Для этого добавьте в image_name столбец image_name со string типом.

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

конфиг / Инициализаторы / dragonfly.rb

 [...] plugin :imagemagick [...] 

Если вы работаете с Rails 4, откройте файл рабочей среды и раскомментируйте эту строку, чтобы включить кеш стойки:

конфиг / окружающая среда / production.rb

 config.action_dispatch.rack_cache = true 

Здорово! На данный момент мы настроили Dragonfly для работы с нашим приложением и готовы перейти к следующей части.

Загрузка фотографий

Самое время конкретизировать контроллер. Мы добавим только пару простых методов:

photos_controller.rb

 class PhotosController < ApplicationController def index @photos = Photo.all end def new @photo = Photo.new end def create @photo = Photo.new(photo_params) if @photo.save flash[:success] = "Photo saved!" redirect_to photos_path else render 'new' end end private def photo_params params.require(:photo).permit(:image, :title) end end 

Если вы используете Rails 3, у вас не будет метода photo_params . Вместо этого добавьте эту строку в файл модели:

photo.rb

 [...] attr_accessible :image, :title [...] 

Обратите внимание, что вам не нужно указывать здесь image_uid — об этом позаботится Dragonfly.

Не забудьте настроить маршруты:

routes.rb

 [...] resources :photos, only: [:new, :create, :index] root to: 'photos#index' [...] 

Последний пункт в нашем контрольном списке — это просмотры. Начиная с индекса:

index.html.erb

 <h1>List of photos</h1> <%= link_to 'Add your photo', new_photo_path, class: 'btn btn-lg btn-primary' %> 

new.html.erb

 <h1>New photo</h1> <%= form_for @photo do |f| %> <div class="form-group"> <%= f.label :title %> <%= f.text_field :title, class: 'form-control', required: true %> </div> <div class="form-group"> <%= f.label :image %> <%= f.file_field :image, required: true %> </div> <%= f.submit 'Submit', class: 'btn btn-primary btn-lg' %> <% end %> 

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

index.html.erb

 <h1>List of photos</h1> <%= link_to 'Add your photo', new_photo_path, class: 'btn btn-lg btn-primary' %> <ul class="row" id="photos-list"> <% @photos.each do |photo| %> <li class="col-xs-3"> <%= link_to image_tag(photo.image.thumb('180x180#').url, alt: photo.title, class: 'img-thumbnail'), photo.image.url, target: '_blank' %> <p><%= photo.title %></p> </li> <% end %> </ul> 

Примените также некоторые стили:

application.css.scss

 #photos-list { padding: 0; margin: 0; margin-top: 30px; clear: both; li { list-style-type: none; padding: 0; margin: 0; max-height: 250px; margin-bottom: 10px; text-align: center; p { margin: 0; margin-top: 10px; height: 40px; overflow: hidden; } } } 

Обратите внимание на использование метода thumb('180x180#') в представлении. Благодаря ImageMagick мы можем обрезать наши изображения любым удобным для нас способом. Символ # означает, что мы хотим сохранить центральную часть нашего изображения. Если бы мы указали 180x180#ne , то северо-восточная часть изображения осталась бы. Есть много других опций, которые вы можете передать (например, нужно ли сохранять соотношение сторон) — читайте о них здесь .

image.url предоставляет image.url на исходное изображение. В нашем случае это изображение откроется при нажатии на миниатюру.

Добавление проверки

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

Dragonfly оснащает наши модели необходимыми методами в инициализаторе:

 # Add model functionality if defined?(ActiveRecord::Base) ActiveRecord::Base.extend Dragonfly::Model ActiveRecord::Base.extend Dragonfly::Model::Validations end 

Все, что нам нужно сделать, это изменить модель:

photo.rb

 validates :title, presence: true, length: {minimum: 2, maximum: 20} validates :image, presence: true validates :image, presence: true validates_size_of :image, maximum: 500.kilobytes, message: "should be no more than 500 KB", if: :image_changed? validates_property :format, of: :image, in: [:jpeg, :jpg, :png, :bmp], case_sensitive: false, message: "should be either .jpeg, .jpg, .png, .bmp", if: :image_changed? 

Как вы можете видеть, наши проверки подтверждают, что title содержать не менее 2 и не более 20 символов. Кроме того, требуется image и проверяются его размер (не более 500 КБ) и формат (только «.jpg», «.png», «.bmp»). Мы также используем image_changed? способ убедиться, что эти проверки запускаются, только если файл был изменен.

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

 validates_property :width, of: :image, in: (0..400), message: proc{ |actual, model| "Unlucky #{model.title} - was #{actual}" } 

Подробнее об этом читайте здесь .

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

new.html.erb

 <h1>New photo</h1> <%= form_for @photo do |f| %> <%= render 'shared/errors', object: @photo %> [...] 

Я использую частичное здесь, поэтому давайте создадим его:

общий / errors.html.erb

 <% if object.errors.any? %> <div id="errors_full_text" class="bg-warning"> <h2>Found these errors when saving:</h2> <ul> <% object.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> 

Примените также некоторые стили:

application.css.scss

 [...] #errors_full_text { padding: 10px; margin-bottom: 10px; h2 { font-size: 18px; } } 

Попробуйте загрузить некоторые недействительные файлы и посмотреть на отображение ошибок.

Обработка и анализ изображений

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

В нашей модели:

photo.rb

 [...] dragonfly_accessor :image do after_assign { |img| img.encode!('jpg', '-quality 80') } end [...] 

Приведенный выше код преобразует все изображения в JPEG с качеством 80 (обратите внимание, что если кто-то попытается загрузить файл PNG с прозрачностью, эта прозрачность будет потеряна).

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

 dragonfly_accessor :image do after_assign do |img| img.encode!('jpg', '-quality 80') if img.image? end end 

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

 dragonfly_accessor :image do after_assign do |img| img.rotate!(90) # 90 is the amount of degrees to rotate end end 

Узнайте больше о колбэках Dragonfly здесь .

Есть несколько других способов получить информацию об изображении. Например: width , height , aspect_ratio и другие (полный список здесь ).

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

 rails g migration add_image_size_to_photos image_size:integer rake db:migrate 

Затем немного откорректируйте вид:

index.html.erb

 [...] <p><%= photo.title %></p> <small><%= number_to_human_size photo.image.size %></small> [...] 

Теперь загрузите новое изображение и наблюдайте за результатом. Его размер будет автоматически сохранен в столбце image_size ! Довольно круто, не правда ли?

Использование другого хранилища данных

Dragonfly будет хранить все загруженные изображения на вашем локальном жестком диске в каталоге public/system/dragonfly . Кстати, не забудьте добавить этот каталог в файл .gitignore , иначе при развертывании все ваши локальные образы будут отправлены на сервер (если это не то, что вам нужно).

Чтобы изменить это поведение, измените файл config/initializers/dragonfly.rb . Для этой демонстрации мы используем облачное хранилище файлов Amazon S3, но есть и другие доступные варианты:

Есть и другие, но они кажутся устаревшими и не работают с Dragonfly 1+. Вы даже можете создать собственное хранилище данных .

Чтобы интегрироваться с Amazon S3, добавьте dragonfly-s3_data_store в Gemfile (мы это уже сделали). Крутая вещь в Dragonfly заключается в том, что изменить хранилище данных очень легко. Все, что вам нужно сделать, это изменить файл инициализатора. Вам, конечно, нужна учетная запись Amazon AWS, которую вы можете создать здесь . Когда вы закончите, откройте консоль управления AWS , найдите S3 в списке и нажмите на нее.

Создайте новое ведро (подробнее о наименовании и производительности ведра здесь ) и запомните его название.

Теперь откройте « YourAccount — учетные данные безопасности» (в правом верхнем углу) и разверните раздел «Ключи доступа». Создайте новую пару ключей и загрузите ее (у вас не будет возможности загрузить ее позже!)

Измените файл инициализатора (полный список доступных опций можно найти здесь ):

конфиг / Инициализаторы / dragonfly.rb

 require 'dragonfly/s3_data_store' [...] url_format "/media/:job/:name" if Rails.env.development? || Rails.env.test? datastore :file, root_path: Rails.root.join('public/system/dragonfly', Rails.env), server_root: Rails.root.join('public') else datastore :s3, bucket_name: YOUR_BUCKET_NAME, access_key_id: YOUR_S3_KEY, secret_access_key: YOUR_S3_SECRET, url_scheme: 'https' end [...] 

Будьте очень осторожны с вашей ключевой парой и никогда никому ее не подвергайте. Если вы развертываете Heroku, используйте ее переменные окружения для хранения пары ключей. Он не должен присутствовать в управлении версиями!

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

index.html.erb

 <%= link_to image_tag(photo.image.thumb('180x180#').url, alt: photo.title, class: 'img-thumbnail'), photo.image.remote_url, target: '_blank' %> 

Обратите внимание на использование remote_url вместо просто url . Это приведет к файлу на Amazon.

Немного оптимизации

Внутри index.html.erb мы используем метод thumb для отображения миниатюры. Это нормально, но генерация миниатюр происходит на лету. У нас может быть много изображений на одной странице, и это поколение происходит для каждого изображения. Как насчет того, чтобы попытаться оптимизировать это?

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

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

Теперь добавьте этот код в ваш инициализатор:

конфиг / Инициализаторы / dragonfly.rb

 [...] # Override the .url method... define_url do |app, job, opts| thumb = Thumb.find_by_job(job.signature) # If (fetch 'some_uid' then resize to '180x180') has been stored already, give the datastore's remote url ... if thumb app.datastore.url_for(thumb.uid, :scheme => 'https') # ...otherwise give the local Dragonfly server url else app.server.url_for(job) end end # Before serving from the local Dragonfly server... before_serve do |job, env| # ...store the thumbnail in the datastore... uid = job.store # ...keep track of its uid so next time we can serve directly from the datastore Thumb.create!( :uid => uid, :job => job.signature # 'BAhbBls...' - holds all the job info ) # eg fetch 'some_uid' then resize to '180x180' end [...] 

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

Теперь попробуйте перезагрузить главную страницу и проверить атрибут src миниатюрного изображения — он должен указывать на Amazon (если, конечно, вы находитесь в производственной среде).

Вывод

Я надеюсь, что вы узнали несколько вещей и получили удовольствие от чтения. Какой драгоценный камень вы используете для управления вложениями в ваших приложениях? Поделитесь своим опытом в комментариях!