Как программно обрабатывать изображения пользователей с помощью Rails и Amazon S3 (включая тестирование)

kak programmno obrabatyvat izobrazheniya polzovatelej s pomoshhyu rails i amazon

Проблема

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

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

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

Размышления

  • Наши изображения хранятся в облачном хранилище Amazon S3. К счастью, Amazon предлагает относительно простой в использовании API для взаимодействия с их сервисами.
  • Поскольку наши изображения размещены на S3, я подумал, что было бы здорово иметь эту службу как функцию Лямбда, которая запускается, когда пользователь загружает фотографию. К сожалению, я не мог ничего напечатать на консоли CloudWatch (где должны появляться журналы). После того, как я целый день бился об эту стену, я решил вернуть ее в дом.
  • Мы размещаем на Heroku, который предлагает бесплатный и простой планировщик для выполнения задач. Для нас не важно конвертировать эти изображения сразу после загрузки. Мы можем запланировать работу, которая собирает все новое за последние 10 минут и конвертировать его.

Рабочий

Сейчас нужен работник, которого мы можем вызвать так часто, как позволит Heroku (10 минут – кратчайший интервал).

Сбор нужных пользователей

Сначала мы соберем всех пользователей, имеющих изображения, которые необходимо конвертировать. Мы сохраняли пользовательские изображения в определенном шаблоне в нашем сегменте S3, который включает a files папку. Мы можем просто искать пользователей, изображения профиля которых соответствуют регулярным выражениям files:

User.where(profilePictureUrl: { '$regex': %r(\/files\/) })

Здесь ваш пробег может отличаться с точки зрения поиска: мы используем базу данных Mongo.

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

Настройка временного файла

Нам понадобится где-нибудь для хранения данных изображений, которыми мы собираемся манипулировать. Мы можем это сделать с помощью a tmp папку. Мы будем использовать это как место хранения изображения, которое мы хотим загрузить в новое местоположение S3. Мы назовем его так, как хотим, чтобы наше окончательное изображение было названо. Мы хотели упростить и стандартизировать изображение в нашей системе, поэтому мы используем уникальный идентификатор пользователя как название изображения:

@temp_file_location = "./tmp/#{user.id}.png"

Получение необработанного изображения и сохранение его локально

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

key = URI.parse(user.profilePictureUrl).path.gsub(%r(\A\/), '')
s3 = Aws::S3::Client.new
response = s3.get_object(bucket: ENV['AWS_BUCKET'], key: key)

The key код берет строку URL, которую мы сохранили как пользовательскую profilePictureUrl и отрезать все, что не является конечным путём к картине.

Например, вернулся бы whatever/12345/image.png по этому коду. Именно этого S3 хочет от нас найти изображение в нашем ведре. Вот это удобно aws-sdk драгоценный камень, работающий на нас get_object.

Теперь мы можем звонить response.body.read чтобы получить точку изображения (blob – это правильное слово, хотя оно выше моего уровня оплаты, чтобы действительно понять, как изображения посылаются туда-сюда через Интернет). Мы можем записать этот blob локально в нашей папке tmp:

File.open(@temp_file_location, 'wb') { |file| file.write(response.body.read) }

Если мы остановимся на этом, вы увидите, что действительно можете открыть этот файл в своей временной папке (с названием, которое вы указали выше — в нашем случае <user>.png ).

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

Теперь у нас есть изображение, загруженное из Amazon, мы можем делать с ним все, что угодно! ImageMagick – это удивительный инструмент, доступный всем.

Мы использовали уменьшенную версию для Rails под названием MiniMagick. Этот драгоценный камень также имеет отличный API, облегчающий разбивку. Нам даже не нужно ничего особенного делать, чтобы подобрать изображение. The @temp_file_location Мы использовали ранее для сохранения изображения, будет работать нормально, чтобы обратить на него внимание MiniMagick:

image = MiniMagick::Image.new(@temp_file_location)

Вот настройки для наших фотографий, но они есть тонн вариантов для игры:

image.combine_options do |img|
  img.resize '300x300>'
  img.auto_orient
  img.auto_level
  img.auto_gamma
  img.sharpen '0x3'
  image.format 'png'
end

combine_options это удобный способ сделать кучу вещей к изображению в одном блоке. После выхода изображение снова сохраняется там, где было раньше. (Форматирование изображения нельзя выполнить с помощью img от combine_options.) Теперь этот файл с изображением в нашей временной папке подвергается дальнейшей обработке!

Загрузите обратно на S3 и сохраните как новое изображение пользователя.

Теперь все, что нам нужно сделать, это настроить другое подключение к S3 и осуществить загрузку:

Aws.config.update(
  region: ENV['AWS_REGION'],
  credentials: Aws::Credentials.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY']))

s3 = Aws::S3::Resource.new
name = File.basename(@temp_file_location)
bucket = ENV['AWS_BUCKET'] + '-output'
obj = s3.bucket(bucket).object(name)
obj.upload_file(@temp_file_location, acl: 'public-read')

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

Мы создаем новый объект с именем нового файла в отделе по нашему выбору, а затем мы upload_file. Это должно быть public-read если мы хотим, чтобы это было видно без большой головной боли для наших клиентов (вы можете выбрать другой вариант безопасности).

Если последняя строка вернет true (что будет, если загрузка пройдет гладко), мы можем обновить нашу запись пользователя:

new_url = "https://s3.amazonaws.com/#{ENV['AWS_BUCKET']}-output/#{File.basename(@temp_file_location)}"
user.update(profilePictureUrl: new_url)

И все это! Если мы запустим этого парня, мы автоматически отформатируем и изменим размер всех изображений пользователей в системе. Все оригинальные изображения будут на месте в старом шаблоне (и если что-то пойдет не так), а все ссылки пользователей будут указывать на их новые, отформатированные изображения.

Тестирование

Мы не могли бы добавить новую функцию в программу Rails без тестирования, не правда ли? Совершенно. Вот как выглядят наши тесты для этого:

RSpec.describe Scripts::StandardizeImages, type: :service do
  let!(:user) { User.make!(:student, profilePictureUrl: ' }

  before do
    stub_request(:get, '
      .with(
        headers: {
          'Accept' => '*/*',
          'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
          'Host' => 's3.amazonaws.com',
          'User-Agent' => 'Ruby'
        }
      )
      .to_return(status: 200, body: '', headers: {})
    allow_any_instance_of(MiniMagick::Image).to receive(:combine_options).and_return(true)
    allow_any_instance_of(Aws::S3::Object).to receive(:upload_file).and_return(true)
  end

  describe '.call' do
    it 'finds all users with non-updated profile pictures, downloads, reformats and then uploads new picture' do
      Scripts::StandardizeImages.call

      expect(user.reload.profilePictureUrl)
        .to eq "https://s3.amazonaws.com/#{ENV['AWS_BUCKET']}-output/#{user.to_param}.png"
    end
  end
end

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

Но, конечно, код попытается поговорить с Amazon и развернуть MiniMagick. Мы можем заглушить эти звонки. На всякий случай, если это ново для вас, я расскажу эту часть.

Неприятные звонки

Если вы не высмеиваете вызовы в своих тестах, вам, вероятно, следует немедленно начать это делать. Все что нужно, это драгоценный камень Webmock. Вы нуждаетесь в этом в своем rails_helper и на этом все.

Когда ваш тест попытается совершить вызов к внешнему источнику, вы получите следующее сообщение (я скрыл частные ключи и вещи из …s):

WebMock::NetConnectNotAllowedError:
       Real HTTP connections are disabled. Unregistered request: GET 
You can stub this request with the following snippet:
stub_request(:get, "").
         with(
           headers: {
          'Accept'=>'*/*',
          'Accept-Encoding'=>'',
          'Authorization'=>...}).
         to_return(status: 200, body: "", headers: {})

Просто скопируйте stub_request bit, и вы на хорошем пути к славе. Возможно, вам придется что-то вернуть в этом bodyв зависимости от того, что вы делаете с внешним вызовом API.

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

Как вариант, можно использовать Aws.config[:s3] = { stub_responses: true } в вашем тестовом инициализаторе или, возможно, на вашем rails_helper чтобы закрыть все запросы S3.

И последнее примечание: Travis CI

В зависимости от того, какие параметры вы решите применить к своему изображению, вы можете обнаружить, что версия ImageMagick Тревиса не такая, как ваша. Я попробовал многое, чтобы заставить Тревиса использовать ту же ImageMagick, что и я. В конце концов, я убиваю MiniMagick звоните, так что это спорный вопрос. Но будьте осторожны: если вы не заглушите эту функцию, вы можете обнаружить, что ваш CI не работает, поскольку он не распознает более новую опцию (например intensity).

Спасибо, что прочли!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *