Всем привет, в этой статье я хочу рассказать, как у меня получилось интегрировать 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)
olku
26.07.2024 14:08+1Если есть необходимость иметь хранилище для файлов произвольного размера с кучкой метаданных, почему не взяли сразу GridFS?
chechyotka Автор
26.07.2024 14:08потому что во всех своих проектах всегда использовал MinIO как бесплатное решение
chechyotka Автор
26.07.2024 14:08еще был кейс, когда AWS S3 совместимый АPI был большим плюсом на проекте, а про GridFS впервые слышу, спасибо гляну
olku
26.07.2024 14:08+1Странно что Mongo держит GridFS в тени, а компании строят свои велики для совмещения бинарных и мета данных. Загрузка файлов чанками в html5 и отдача назад стримом делается в пару сотен строк. Никакой файловой системы и ее ограничений, поиск по любым полям и тегам с поддержкой индексов. К тому же, это все schema-less. Гибче некуда.
Jsty
А что по надежности, мониторингу, идемпотентности этого решения? Как minio под капотом эти вопросы решает?
chechyotka Автор
Единственное что могу сказать, что MinIO использует SQS (simle queue service) для доставки сообщений в сервисы, но да, надо будет разобраться мне лично как мониторить MinIO
Alexufo
Ну он мониторится и своей панелью внутри и прометеусом