Это вторая часть из серии постов о 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татьи из оригинальной серии в блоге автора библиотеки:
- Better File Uploads with Shrine: Attachment
- Better File Uploads with Shrine: Processing
- Better File Uploads with Shrine: Metadata
Автор: amerov