Всем привет, в этой статье я хочу рассказать, как у меня получилось интегрировать MinIO и Postgres, а именно, что после каждой загрузки картинки в объектное хранилище у нас появляется запись в БД.
В рамках данной статьи я не буду рассматривать, как работать с MinIO, про это есть хорошая статья на Хабре "Зачем и как хранить объекты на примере MinIO"

Как было раньше

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

func createRestaurant(ctx *gin.Context) {
    // чтение запроса и валидация его для сохранения ресторана в приложении 
    // ...
  
    // Забираем изображения ресторана из тела запроса	
    form, err := ctx.MultipartForm()
	files := form.File["image"]
	if len(files) == 0 {
		ctx.JSON(http.StatusBadRequest, gin.H{
			"error": "no photos",
		})
		return
	}

    // ....
}

После всех приготовлений мы шли в БД и в рамках транзакции сохряняли запись о ресторане в первой таблице, а потом делали запись или записиь об изображениях связанные с нашим рестораном. Связь у нас один-ко-многим, у одного ресторана может быть несколько изображений в профиле.

Схема базы данных
Схема базы данных

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

Как теперь

Сейчас после загрузки изображений в S3 хранилище больше не нужно трогать БД собственноручно, за нас все сделают
Чтобы достигнуть этого, надо настроить наш сервер MinIO, гайд по развертыванию и работе с MinIO сервером есть все в той же статье, что я указал выше. Для тестового окружения приложу docker-compose.yaml, где у нас есть Postgresql и MinIO

version: '3.8'
services:
  minio:
    container_name: minio
    image: minio/minio
    environment:
      MINIO_ACCESS_KEY: minio
      MINIO_SECRET_KEY: minio123
    command: server /data --console-address ":9001"
    ports:
      - "9000:9000"
      - "9001:9001"
    volumes:
      - minio_data:/data

  postgres:
    container_name: postgres
    image: postgres:alpine
    environment:
      POSTGRES_DB: postgres
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: 5432
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  minio_data:
  postgres_data

Postgesql

В нашей БД создадим таблицу, которая будет хранить запись обо всех изображениях

CREATE TABLE IF NOT EXISTS images (
    key TEXT,
    value JSONB
);

Minio

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

# создания алиаса для нашего сервера, чтобы потом его переиспользовать
mc alias set myminio http://minio:9000 minio minio123
mc mb myminio/menus # создания бакета menus 

Настраиваем подключение к базе данных

mc admin config set myminio notify_postgres:minio-postgres connection_string="user=postgres password=5432 host=postgres dbname=postgres port=5432 sslmode=disable" table="images" format="namespace"

После этого нас попросят перезапустить сервис

mc admin service restart myminio

Ну и напоследок задать, чтобы MinIO делал запись в БД после каждой загрузки изображения в бакет menus

mc event add myminio/menus arn:minio:sqs::minio-postgres:postgresql --event put

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

Само собой нужно протестировать приложение, сделал тестовый запрос на создание ресторана с помощью Swagger

тестовый запрос
тестовый запрос

Теперь в БД можно наблюдать вот такой большой json с данными про объект

Данные объекта
Данные объекта

Теперь для нашего приложения, чтобы отслеживать принадлежность изображения для определённого ресторана или блюда будем класть метадату при загрузке изображения

func makeMetadata(opts *SaveObjectOptions) map[string]string {
	return map[string]string{
		"restaurant_id": opts.RestaurantID,
		"category_id":   opts.CategoryID,
		"dish_id":       opts.DishID,
	}
}

func (c *Client) SaveImage(ctx context.Context, opts *SaveObjectOptions) error {
	info, err := c.minioClient.FPutObject(ctx, menusBucket, opts.FileName, opts.FilePath, minio.PutObjectOptions{
		ContentType:  "image/png",
		UserMetadata: makeMetadata(opts),
	})
	if err != nil {
		c.log.Error("failed to upload image", zap.Error(err))
		return err
	}

	c.log.Info("successfully uploaded of size", zap.String("filename", opts.FileName), zap.Int64("size", info.Size))

	return nil
}

Заключение

Надеюсь кому то поможет данная статья для реализации своих задач

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


  1. Jsty
    26.07.2024 14:08
    +1

    А что по надежности, мониторингу, идемпотентности этого решения? Как minio под капотом эти вопросы решает?


    1. chechyotka Автор
      26.07.2024 14:08

      Единственное что могу сказать, что MinIO использует SQS (simle queue service) для доставки сообщений в сервисы, но да, надо будет разобраться мне лично как мониторить MinIO


      1. Alexufo
        26.07.2024 14:08
        +1

        Ну он мониторится и своей панелью внутри и прометеусом


  1. olku
    26.07.2024 14:08
    +1

    Если есть необходимость иметь хранилище для файлов произвольного размера с кучкой метаданных, почему не взяли сразу GridFS?


    1. chechyotka Автор
      26.07.2024 14:08

      потому что во всех своих проектах всегда использовал MinIO как бесплатное решение


      1. chechyotka Автор
        26.07.2024 14:08

        еще был кейс, когда AWS S3 совместимый АPI был большим плюсом на проекте, а про GridFS впервые слышу, спасибо гляну


        1. olku
          26.07.2024 14:08
          +1

          Странно что Mongo держит GridFS в тени, а компании строят свои велики для совмещения бинарных и мета данных. Загрузка файлов чанками в html5 и отдача назад стримом делается в пару сотен строк. Никакой файловой системы и ее ограничений, поиск по любым полям и тегам с поддержкой индексов. К тому же, это все schema-less. Гибче некуда.


  1. Cryptomania
    26.07.2024 14:08
    +1

    Отлично!


  1. ddruganov
    26.07.2024 14:08
    +1

    То есть отказались от транзакционности и внешних ключей ради ... чего?