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


Прошло уже больше года с того времени, как я начал разрабатывать Shrine. За это время Shrine получил много интересного функционала, экосистема значительно выросла и достаточно разработчиков начало использовать Shrine в продакшене.

Прежде чем углубиться в разъяснение преимуществ, нужно сделать шаг назад и рассмотреть подробно, что в первую очередь послужило мотивацией для разработки Shrine.

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

Требования




Требования были следующие:

  1. Файлы на Amazon S3 должны загружаться напрямую
  2. Обработка и удаление файлов должно выполнятся в фоновом режиме
  3. Обработка может выполнятся в процессе загрузки
  4. Интеграция с Sequel
  5. Возможность использовать с фреймворками помимо Rails

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

1. Использование Amazon S3 или аналогов, позволяет оптимизировать процесс загрузки файлов.
Это определенно имеет ряд преимуществ: снижается потребление ресурсов, горизонтальное масштабирование с инкапсуляцией хранилища, работа с облачными решениями типа Heroku, которые не предоставляют возможности записи на диск и имеют ограничение на время выполнения запроса.

2. Обработка и удаление файлов в фоновых задачах дает возможность работать с файлами асинхронно, независимо от того, храните ли вы файлы на локальной файловой системе или на внешнем хранилище, таком как Amazon S3, это значительно улучшит работу пользовательского интерфейса. Использование фоновых задач также необходимо для поддержания высокой пропускной способности вашего приложения, потому что воркеры не будут привязаны медленным запросам.

3. Обработка на в процессе загрузки отлично работает с небольшими файлами, особенно когда создаются несколько версий файлов, например, разные размеры для картинок. с другой стороны обработка при загрузке необходима и для больших файлов, таких как видео. Следовательно, нужна библиотека, которая может работать с любыми типами файлов.

4. Использование с ORM помимо ActiveRecord также является очень важным. Поскольку уже появились более функциональные и производительные ORM для Руби.

5. Наконец, в Ruby сообществе появились достойные альтернативы Rails. Нужна возможность легкой интеграции с любым веб-фреймворком.

Теперь пройдемся по существующим библиотекам и рассмотрим их основные недостатки с учетом требований.

Paperclip




Простое управление вложенными файлами для ActiveRecord

Мы стазу можем сказать — прощай Paperclip, поскольку есть сильная зависимость от ActiveRecord. Так как эта очень распространенная библиотека которая используется с ActiveRecord, давайте все же пройдемся по остальным требованиям.

Прямая загрузка


Paperclip не имеет возможности прямой загрузки. Возможно использование aws-sdk для генерации ссылки и параметров для прямой загрузки на S3 и потом редактировать атрибуты модели, таким же образом, как при загрузке файла через Paperclip.

Однако, Paperclip работает только с одним хранилищем. Для работы необходимо чтобы все закачки происходили напрямую в основное хранилище S3. Это приводит к проблеме безопасности, поскольку атакующий может загружать файлы без прикрепления, и в итоге может создастся множество файлов-сирот. Было бы намного проще, если бы S3 сделал это за вас.

Фоновые задачи


Для фоновых задач используется delayed_papeclip. Однако, delayed_paperclip запускает задачи только после полной загрузки файла. Это означает, что если вы не хотите или не можете делать прямые закачки на S3, ваши пользователи должны будут дважды загружать файл (сначала в приложение, затем в хранилище), прежде чем произойдет какая-либо фоновая обработка. И это очень медленно.

Кроме того, delayed_paperclip не поддерживает удаление файлов в фоновом режиме. Это большой минус, потому что придется выполнять HTTP-запрос для каждой версии файла (если у вас есть несколько версий файлов, хранящихся на S3). Не ожидайте добавления этого функционала, поскольку Paperclip также проверяет существование каждой версии перед удалением. Конечно же, вы можете отключить удаление файлов, но тогда у вас возникнет проблема с файлами-сиротами.

Наконец, delayed_paperclip теперь привязан к ActiveJob, что означает, что теперь не получится напрямую использовать его с библиотеками для фоновых задач.

Ложное срабатывание обнаружения mime-type spoofing атаки


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

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

CarrierWave




Отличное решение для загрузки файлов для Rails, Sinatra и других веб-фреймворков

CarrierWave ответ Paperclip'у который хранил конфигурацию прямо в модели, инкапсуляция в классах.

CarrierWave в есть интеграция с Sequel.

К сожалению, для расширений carrierwave_backgrounder и carrierwave_direct не достаточно интеграции ORM CarrierWave. Нужно много дополнительного ActiveRecord специфичного кода, чтобы все это заработало.

Прямая загрузка


Как упоминалось ранее, экосистема CarrierWave имеет решения для прямой загрузки на S3 — carrierwave_direct. Это работает таким образом, что позволяет вам создать форму для прямой загрузки на S3, а затем назначить ключ S3 загруженного файла вашему загрузчику.

<!-- Form submits to "https://my-bucket.s3-eu-west-1.amazonaws.com" -->
<%= direct_upload_form_for @photo.image do |f| %>
  <%= f.file_field :image %>
  <%= f.submit %>
<% end %>

Однако что, если вам нужно сделать несколько загрузок напрямую на S3? в README отмечено, что carrierwave_direct предназначен только для одиночных загрузок. А что на счет JSON API? Это обычная форма, все что онаделает — это генерация URL и параметров для загрузки на S3, Так почему же carrierwave_direct не позволяет получить эту информацию в формате JSON?

А что если, вместо повторной реализации всей логики генерации запроса на S3 используя fog-aws просто полагался на aws-sdk?

# aws-sdk
bucket  = s3.bucket("my-bucket")
object  = bucket.object(SecureRandom.hex)
presign = object.presigned_post

<!-- HTML version -->
<form action="<%= presign.url %>" method="post" enctype="multipart/form-data">
  <input type="file" name="file">
  <% presign.fields.each do |name, value| %>
    <input type="hidden" name="<%= name %>" value="<%= value %>">
  <% end %>
  <input type="submit" value="Upload">
</form>

# JSON version
{ "url": presign.url, "fields": presign.fields }

Этот способ имеет следующие преимущества: Он не привязан к Rails, он работает с JSON API, он поддерживает множественную загрузок файлов (клиент может просто сделать запрос с этими данными для каждого файла), и он более надежен (так как теперь параметры генерируется официально поддерживаемым гемом).

Фоновые задачи


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

Что приводит нас к carrierwave_backgrounder. Эта библиотека поддерживает обработку фоновых задач, но по моему опыту она была нестабильна (1, и 2). Кроме того, она не поддерживает удаление файлов в фоновом режиме, что является решающим фактором при удалении множества файлов.

Даже если мы все это преодолеем, не получится интегрировать carrierwave_backgrounder с carrierwave_direct. Как я уже упоминал, я хочу загружать файлы непосредственно на S3 и обрабатывать и удалять их в фоновых задачах. Но кажется, что эти две библиотеки несовместимы друг с другом, а это означает, что я не могу достичь желаемой производительности с CarrierWave для моих кейсов.

Закрытие не разрешенных issue на Github


Я понимаю, что иногда люди бывают неблагодарны мейнтейнерам популярных open-source библиотек и стоит быть более мягче и уважительными друг к другу. Тем не менее, я не могу понять, почему разработчики CarrierWave закрывают неразрешенные задачи.

Одна из таких закрытых задач является ненужное выполнение обработки CarrierWave перед валидацией. Это серьёзная дыра в безопасности, поскольку атакующий может передать любой файл обработчику изображений, так как валидации размеров файлов/MIME/измерений будут выполняться только после обработки. Это делает ваше приложение уязвимым для атак типа ImageTragick, image bombs или просто загрузка больших изображений.

Refile




Загрузка файлов в Ruby, Попытка №3

Refile был создан Джонасом Никласом, автором CarrierWave, как третья попытка улучшить загрузку файлов в Ruby.Как и Dragonfly, Refile проектировался в возможностью обработки на лету. Помучавшись со сложностью CarrierWave, я обнаружил, что простой и современный дизайн Refile действительно многообещающий, поэтому я начал вносить свой вклад в него, и в итоге я был приглашен в кор-тим.

Refile.attachment_url(@photo, :image, :fit, 400, 500) # resize to 400x500
#=> "/attachments/15058dc712/store/fit/400/500/ed3153b9cb"

Некоторые из новых идей Refile включают в себя: временное и перманентное хранилище в качестве хранилищ первого порядка, чистые абстракции для хранилищ, Абстракция IO, чистый внутренний дизайн (без GOD объектов), и прямая загрузка из коробки. Благодаря чистому дизайну Refile, создание интеграции Sequel было довольно простым делом.

Прямая загрузка


Refile — первая библиотека для загрузки файлов, которая поставляется со встроенной поддержкой прямых загрузок, позволяя вам асинхронно загружать вложенный файл в тот момент, когда пользователь его выбирает. Вы можете загрузить файл через Rack или непосредственно на S3, используя Refile, чтобы сгенерировать параметры запроса S3. есть даже JavaScript библиотека, который делает все за вас.

<%= form.attachment_field :image, presigned: true %>

Здесь также есть отличный прирост производительности. Когда вы загружаете файл прямо на S3, вы загружаете его в каталог bucket, который помечен как «временный». Затем, когда проходит валидация и запись сохраняется, загруженный файл перемещается в перманентное хранилище. Если временное и постоянное хранилище находится на S3, то вместо повторной загрузки Refile просто выдаст запрос S3 COPY.

Нет слов, мои требования для прямых загрузках были удовлетворены.

Фоновые задачи


Одним из ограничений Refile является отсутствие поддержки фоновых заданий. Вы можете подумать, что, поскольку Refile выполняет обработку в процессе загрузки и имеет оптимизацию S3 COPY, фоновые задачи здесь не нужны.

Тем не менее запрос S3 COPY по-прежнему является HTTP-запросом и влияет на продолжительность отправки формы. Кроме того, скорость запроса S3 COPY зависит от размера файла, поэтому чем больше файл, тем медленнее будет запрос S3 COPY.

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

Обработка в процессе загрузки


Я думаю, что обработка в процессе загрузки отлично подходит для изображений, которые хранятся локально и быстро обрабатываются. Однако, если вы храните оригиналы на S3, то Refile будет обслуживать первоначальный запрос к версии гораздо медленнее, так как ему нужно сначала загрузить оригинал с S3. В этом случае нужно подумать о добавлении фоновых задач, которые предварительно обрабатывают все версии.

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

Dragonfly




Ruby gem для обработки в процессе загрузки — подходит для загрузки изображений в Rails, Sinatra

Dragonfly — еще одно решение для обработки в процессе загрузки, которое было на сцене гораздо дольше, чем Refile, и, на мой взгляд, обладает гораздо более продвинутыми и гибкими возможностями обработки в процессе загрузки.

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

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

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

map "/attachments" do
  run Dragonfly.app # doesn't care how the files were uploaded
end

Attache




Еще один подход к загрузке файлов

Attache относительно новая библиотека, которая поддерживает обработку в процессе загрузки. Разница между Dragonfly и Refile заключается в том, что Attache был разработан для запуска в виде отдельной службы, поэтому файлы загружаются и раздаются через сервер Attache.

Attache имеет интеграцию ActiveRecord для связывания загруженных файлов к записям базы данных и имеет поддержку прямых загрузок. Но все еще не хватает возможности создавать резервные копии и удалять файлы в фоновых задачах. К тому же Attache недостаточно гибок.

Обратите внимание, аналогично Dragonfly, Attache не нуждается в интеграции с моделью — для этого можно использовать Shrine. В этом году я побывал в RedDotRubyConf в Сингапуре, где мне довелось встретиться с автором Attache, и после очень интересного обсуждения о проблемах при загрузках файлов, мы пришли к решению, что было бы полезно использовать Shrine для логики вложения файлов, и просто подключать Attache в качестве бекенда.

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

В заключение


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

Поэтому я решил создать новую библиотеку Shrine, основываясь на знаниях из существующих библиотек.

Цель Shrine — не быть топорным, предоставить функционал и гибкость, которые позволят оптимизировать различные задачи при работе с файлами.

Это амбициозная цель, но после года активной разработки и исследований я чувствую, что достиг этого. По крайней мере, возможностей, стало больше, чем в любой другой библиотеке для Ruby. В остальной части этой серии статей я познакомлю вас со всеми классными фичами, которые вы можете использовать со Shrine, так что следите за обновлениями!


Оригинал: Better File Uploads with Shrine: Motivation
Остальные статьи из серии в блоге автора:

Поделиться с друзьями
-->

Комментарии (3)


  1. borisano
    14.05.2017 05:06

    А может ли что-то из описанных гемов организовать загрузку целой папки со всеми файлами, как это сделано, например, в дробпоксе?

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


    1. printercu
      14.05.2017 05:24

      Тут ограничение самого http же — он понятия ни о каких папках не имеет. И вы должны сами решать, как вы их хотите передавать. Каждый файл лучше грузить по-отдельности. Либо соседним параметром имя директории передавать и сохранять файл в эту директорию. Или же разрешить "/" в filename загружаемого файла и отключить их экранирование/замену в либе. В этом случае только надо самим обязательно от path traversal фильтры делать.


  1. printercu
    15.05.2017 07:36

    Наконец-то появился гем для загрузок, в котором все хорошо и все учтено! Читаю ридми и радуюсь, что больше не придется хачить carrierwave: все что сами допиливали тут можно настроить из коробки. Хотя еще не дочитал до конца даже :)