25 октября 2021 года суданские военные взяли под свой контроль правительство в результате военного переворота. До того, как это произошло, многие суданцы погибли в ходе протестов, часами сидели при 50-градусной жаре, надеясь на лучшее правительство. К сожалению, переворот свел на нет большую часть этой работы.

Я хотел чем-то помочь, поэтому создал веб-приложение для архива революционного суданского искусства https://sudan-art.com. Код с открытым исходным кодом, и это еще рано, поэтому вклад приветствуется 🔥!

В прошлом году я много работал над движением сопротивления перевороту в Мьянме. Я заметил, как быстро протестное движение выпадало из новостей и как трудно архивировать революционное искусство и материалы. Затем я нашел фантастический сайт под названием https://threefingers.org, на котором собраны массы блестящего бирманского искусства, поэтому я решил сделать что-то подобное.

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

По дороге я узнал много интересного. Я собираюсь разделить их на три разных поста в блоге, организованных по домену программирования и стеку:

Python и Django — часть I

  1. Как правильно предварительно обработать изображения для загрузки, уменьшив размер их файлов и удалив метаданные;
  2. Как смириться с тем, что сделать загрузку изображений на 100 % безопасной невозможно, и управлять сопутствующим риском;
  3. Как написать тесты для представлений Django, которые обрабатывают загрузку изображений;

JavaScript/React — Часть II

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

DevOps/Цифровой океан — часть III

Во-первых, если вы хотите 100 долларов на 60 дней, чтобы попробовать Digital Ocean, нажмите на ссылку эта.

  1. Как выполнить развертывание в Digital Ocean с помощью docker compose для MVP;
  2. Как обслуживать Django/React с NGINX в безопасном производственном развертывании с обратным прокси-сервером;
  3. Как настроить простой рабочий процесс действий Github для автоматизации конвейера CI/CD для анализа кода, тестирования и развертывания.

Часть I

Как правильно предварительно обработать изображения для загрузки, уменьшив размер их файлов и удалив метаданные

При создании этого приложения я быстро понял, что очень важно правильно предварительно обрабатывать изображения на бэкэнде. С точки зрения фронтенд-разработчика, изображения занимают очень много места.

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

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

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

Итак, как я решал эти проблемы:

Здесь происходит довольно много вещей! Если вы посмотрите на функцию сохранения в строке 34, она переопределяет метод save, который был частью класса models.Model, от которого унаследован мой класс Artwork.

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

Эта функция создает миниатюру и удаляет все метаданные изображения, читая изображение в поток байтов, а затем снова сохраняя его в файл. Он делает это, создавая новый объект File из основной библиотеки django, поэтому он отлично сочетается с ImageField, который я определил в классе Artwork.

На следующем…

Как смириться с тем, что сделать загрузку изображений на 100 % безопасной невозможно, и справиться с сопутствующим риском

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

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

Я не говорю, что это плохая идея, но это было слишком много работы для этого небольшого личного проекта, который был скорее хобби. Итак, я решил просто положиться на встроенную проверку Django ImageField, а затем хранить изображения в Digital Ocean Space отдельно от остальной части моего приложения. Это важно, поскольку это означает, что злоумышленник не может переключиться с загрузки изображения на среду, в которой запущено мое веб-приложение, — вместо этого он застрянет в пространстве Digital Ocean Space 😜

Итак, это то, что вы можете видеть в строках 21–30 выше. Если я запускаю приложение локально, то settings.DEBUG будет истинным, поэтому мне не нужно этого делать. Но если я запускаю его в производственной среде, я настроил все для использования корзины S3 в Digital Ocean.

Для этого используются хранилища Django, в которых есть некоторые подробности о конфигурации с Digital Ocean здесь. Я использовал это для создания здесь класса хранения, на который ссылается PublicMediaStorage в приведенном выше определении.

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

Поскольку я использовал Django Rest Framework для создания внутреннего API для этой службы, есть несколько вещей, которые вам нужно сделать в ваших представлениях, чтобы убедиться, что ваша загрузка изображения действительно использует ImageField Django. Это очень хорошо работает с остальной структурой.

Если у вас есть views.py, как это:

Затем для вашей модели будет использоваться сериализатор, который вы можете определить следующим образом в serializers.py:

Как написать тесты для представлений Django, которые обрабатывают загрузку изображений

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

Вот так ☺️:

Первое, что я здесь сделал, — это создал базовый класс, от которого я могу наследоваться при написании других тестов. Это просто добавляет объект в базу данных, используя образец изображения, который у меня есть в тестовой папке, а затем удаляет все из значения settings.MEDIA_ROOT, которое является папкой, в которой хранятся все изображения.

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

Одна вещь, которую я люблю делать, чтобы упростить запуск тестов, — это создать отдельный settings.py для тестов, чтобы они запускались в базе данных sql lite в памяти. Это может не сработать, если вы используете в своем приложении много синтаксиса, подобного postgres, но если это так, то это удобно, поскольку это означает, что вам не нужно настраивать какую-либо базу данных для запуска стандартного набора тестов Django.

Вот как это выглядит:

Затем вы должны вызывать тесты с помощью python manage.py test --settings=<path to test_settings.py.

И вот оно!