Making MinIO and Postgresql Work Together

Why and how to store objects using MinIO as an example”

As it was before

I will show code examples in the most simplified form possible so that no difficulties arise.
The project has a functionality for uploading images for restaurants/dishes/category of dishes via REST API. We upload images in the most primitive way.

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
	}

    // ....
}

After all the preparations, we went to the DB and within the transaction we saved a record about the restaurant in the first table, and then made a record or records about the images associated with our restaurant. We have a connection one-to-many, One restaurant can have multiple profile images.

Database schema

Database schema

Everything seems to be fine, the scheme works like a Swiss watch, but we programmers always want to redo something and that is why, when I found about such functionality of MinIO as integration, I immediately went to implement it on my project.

As it is now

Now after uploading images to S3 storage, you no longer need to touch the database yourself, everything will be done for us
To achieve this, you need to set up our MinIO server, a guide to deploying and working with the MinIO server is all in the same article that I indicated above. For the test environment, I will attach docker-compose.yaml, where we have Postgresql and 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

In our database, we will create a table that will store a record of all images.

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

Minio

To send any events, you need a bucket in which images will be stored. For this, we will make these commands.

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

Setting up a connection to the database

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"

After this we will be asked to restart the service

mc admin service restart myminio

And finally, set MinIO to make a record in the database after each image is loaded into the menus bucket.

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

Testing

Of course, you need to test the application, I made a test request to create a restaurant using Swagger

test request

test request

Now in the DB you can see such a large json with data about the object

Object data

Object data

Now for our application, to track the image belonging to a certain restaurant or dish, we will put the metadata when loading the image

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
}

Conclusion

I hope this article will help someone to achieve their goals.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *