Лучший способ загрузки файлов в Ruby с помощью Shrine. Часть 2. Загрузчик

в 10:55, , рубрики: file upload, ruby, ruby on rails, загрузка картинок, загрузка файлов, загрузка файлов на сервер

Это вторая часть из серии постов о Shrine. Цель этой серии статей – показать преимущества Shrine над существующими загрузчиками файлов.


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

Хранилище


«Хранилище» Shrine — это plain Ruby-объект, который инкапсулирует управление файлами в определенном сервисе хранения (файловая система, S3 и.т.д). Хранилище должно содержать следующие 5 методов:

class MyStorage
  def upload(io, id, **options)
    # выгружает `io` в указанный `id`
  end

  def url(id)
    # возвращает URL-адрес файла с `id`
  end

  def open(id)
    # возвращает файл по адресу `id` в качестве объекта типа IO
  end

  def exists?(id)
    # возвращает, существование файла в хранилище
  end

  def delete(id)
    # удаляет соответствующий файл из хранилища 
  end
end

Хранилища Shrine конфигурируются напрямую, передавая опции в конструктор (позаимствовано у Refile) и должны быть зарегистрированы в Shrine.storages:


Shrine.storages[:s3] = Shrine::Storage::S3.new(
  access_key_id: "abc",
  secret_access_key: "xyz",
  region: "eu-west-1",
  bucket: "my-bucket",
)

В настоящее время для Shrine имеются поддержка файловой системы, S3, Fog, Flickr, Cloudary, Transloadit, Uploadcare, Imgix, GridFS и SQL, так что выбирайте.

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

Загрузчик


Загрузчики являются подклассами Shrine, они инкапсулируют логику выгрузки определенного вложения (как у CarrierWave).
class ImageUploader < Shrine
  # image uploading logic goes here
end

Объекты Uploader выступают в качестве оберток вокруг хранилища, в них выполняется вся логика для загрузки, которая является общей для любого хранилища:

  • Обработка
  • Извлечение метаданных
  • Генерация локации для файла
  • Загрузка (на этом этапе происходит обращение к хранилищу)
  • Закрытие загруженного файла

Создание экземпляра загрузчика с установленным параметром хранилища:

Shrine.storages[:disk] = Shrine::Storage::FileSystem.new(...)

uploader = ImageUploader.new(:disk)
uploader.upload(image) #=> #<Shrine::UploadedFile>

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

Загруженные файлы


Когда файл загружается через загрузчик, метод #upload возвращает объект Shrine::UploadedFile. Этот объект является полным представлением файла, который был загружен в хранилище.

uploaded_file = uploader.upload(image) #=> #<Shrine::UploadedFile>

uploaded_file.id       #=> "43ksd9gkafg0dsl.jpg"
uploaded_file.storage  #=> #<Shrine::Storage::FileSystem>
uploaded_file.metadata #=> {...}

Так как этот объект знает, куда он был загружен, он может предоставить много полезных методов:

uploaded_file.url               # generates the URL
uploaded_file.download          # downloads the file to the disk
uploaded_file.exists?           # asks the storage if file exists
uploaded_file.open { |io| ... } # opens the file for reading
uploaded_file.delete            # deletes the file from the storage

Этот объект определяется только хешем. Поскольку на хранилище можно ссылаться по его установленному параметру, этот хэш теперь может быть сериализован в JSON и сохранен в столбце базы данных.

uploaded_file.data #=>
# {
#   "id"       => "df9fk48saflg.jpg",
#   "storage"  => "disk",
#   "metadata" => {...}
# }

uploaded_file.to_json #=> '{"id":"df9fk48saflg.jpg","storage":"disk","metadata":{...}}'

Объекты Shrine::UploadedFile не зависят от загрузчиков. Это значительное отличие от CarrierWave и Paperclip, которые имеют такую зависимость с классами CarrierWave::Uploader::Base и Paperclip::Attachment.

Абстракция IO


Shrine может загрузить любой объект типа IO, который отвечает на #read, #size, #rewind, #eof? И #close (как у Refile). Определяя этот строгий интерфейс, каждая функция Shrine теперь знает, что они могут полагаться только на эти методы, а это значит, что они будут работать правильно независимо от того, загружаете ли вы типы File, StringIO, ActionDispatch::Http::UploadedFile, Rack или удаленные файлы, которые загружаются стримом.

Кроме того, Shrine::UploadedFile сам по себе является объектом типа IO, обертывая любой загруженный файл под тем же унифицированным интерфейсом. Это делает перемещение файла с одного хранилища на другое действительно удобным. Кроме того, это позволяет оптимизировать некоторые закачки путем пропуска процесса скачивания и повторной загрузки, например, использовать копию S3, если оба файла из S3, или просто отправить запрос URL, если хранилище поддерживает его.

cache = ImageUploader.new(:s3_temporary)
cached_file = cache.upload(image)

store = ImageUploader.new(:s3_permanent)
store.upload(cached_file) #=> performs an S3 COPY request

Система плагинов


Shrine поставляется с небольшим ядром (<500 строк кода), которое обеспечивает необходимую функциональность. Любые дополнительные функции могут быть загружены через плагины. Это дает вам гибкость в выборе именно того, что и как должен делать для вас Shrine, и загружать код только для функционала, который вы используете.

# Loads the processing feature from "shrine/plugins/logging.rb"
Shrine.plugin :logging, logger: Rails.logger

Shrine поставляется с более чем 35 плагинами, и вы легко можете написать свои собственные. Плагиновая система Shrine — это адаптация Roda, о которой я писал в прошлом.

Кроме того, загрузчики Shrine могут наследоваться (в отличие от CarrierWave).

Shrine.plugin :logging # enables logging for all uploaders

class ImageUploader < Shrine
  plugin :backup # stores backups only for this uploader (and its descendants)
end 

Зависимости


Большинство библиотек для загрузки файлов, имеют довольно монструозные зависимости.

CarrierWave

  • ActiveSupport – Я действительно не хочу всех этих манки-патчей
  • ActiveModel – Почему бы не выполнять валидацию без библиотек?
  • MIME::Types – Лучше определить MIME-тип из содержимого файла

Paperclip

  • ActiveSupport – Опять же, я хочу иметь возможность не иметь никаких манки-патчей
  • ActiveModel – Окей, любом случае, как ActiveModel, так и ActiveSupport требуются для ActiveRecord
  • Cocaine – Open3 монструозная библиотека для запуска командной оболочки
  • MIME::Types – Обнаружение подмены MIME-типа имеет проблемы
  • MimeMagic – достаточно утилиты file

Refile

  • RestClient – Монструозное решение для простой загрузки файлов
  • Sinatra – Это нормально, хотя Roda – более легкая альтернатива
  • MIME::Types – Лучше определить MIME-тип из содержимого файла

Shrine, с другой стороны, имеет только одну обязательную но легкую зависимость — Down. Down — это net/http обертка для загрузки файлов, которая улучшает open-uri и поддерживает стриминг, и используется почти всеми хранилищами Shrine.

Кроме того, Shrine в целом загружается очень быстро, потому что вы загружаете код только для функционала, который вы используете. Для других загрузчиков требуется загрузить код для всего функционала, которые может вам не понадобиться. К примеру, Shrine загружается в 35 раз быстрее CarrierWave без загруженных плагинов и в 7 раз быстрее со всеми загруженными (исходник) плагинами.

Итоги

Каждый высокоуровневый интерфейс должен иметь хороший фундамент. Таким образом, c каким бы уровнем абстракции вы не работали, вы всегда сможете понять, что происходит. Основа Shrine состоит из классов Storage, Shrine и Shrine::UploadedFile, каждый из которых имеет четко определенный интерфейс и обязанность.


Оригинал: https://twin.github.io/better-file-uploads-with-shrine-uploader/

Cтатьи из оригинальной серии в блоге автора библиотеки:

Автор: amerov

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js