Executando verificação de segurança...
1

Como fazer upload de arquivos utilizando Go e Object Storage

Atualmente, tive a necessidade de fazer um upload de arquivos, mas não queria salvar em disco e fazer o render da pasta utilizando o static. Sendo assim, futuramente iriamos ter problemas com espaço em disco.
Pesquisando, me de parei com o Min.IO, um serviço de Object Storage que permite criarmos bucket e fazer o upload dos arquivos nele. Esse serviço permite uma integração com a AWS S3, dessa forma é o serviço ideal.
Através do Docker, conseguimos criar um container para que possamos utilizar o localhost, utilizando o NGINX podemos fazer um proxy_pass para o container e expor somente a porta 443 em produção;

Repositório para o código completo:

https://github.com/julianojj/aurora

Como subir um novo container do Min.IO?

Devemos criar alguns arquivos:

docker-compose.yml

version: '3.x'
services:
  minio:
    image: minio/minio
    container_name: minio
    ports:
      - 9000:9000
      - 9001:9001
    env_file:
      - ./minio.env
    command: server /data --console-address ":9001"
    volumes:
      - minio_data:/data
volumes:
  minio_data:

minio.env

MINIO_ROOT_USER=access
MINIO_ROOT_PASSWORD=secretkey

.env

MINIO_ENDPOINT=localhost:9000
MINIO_ACCESS_KEY=access
MINIO_SECRET_KEY=secretkey
MINIO_BUCKET_NAME=aurora

Exemplo de código em go:

Utilizei o gin para fazer o upload, criei uma rota /POST e enviei o arquivo utilizando multipart/form-data

main.go

r := gin.Default()

fileRepository := repository.NewFileRepositoryMemory()

bucket := adapters.NewMinio()
err := bucket.CreateBucket()
if err != nil {
        panic(err)
}

uploadFile := usecases.NewUploadFile(fileRepository, bucket)

uploadFileController := controllers.NewUploadFileController(uploadFile)

routes.NewUploadFileRoute(
        r,
        uploadFileController,
).Register()

r.Run(":8080")

main.go é o aggregate root, aqui temos todas as instâncias que o projeto precisa, dessa forma conseguimos utilizar o Dependency Inversion para que possamos injetar as dependências para fora para dentro.

upload_route.go

type UploadRoute struct {
	r                    *gin.Engine
	uploadFileController *controllers.UploadFileController
}

func NewUploadFileRoute(
	r *gin.Engine,
	uploadFileController *controllers.UploadFileController,
) *UploadRoute {
	return &UploadRoute{
		r,
		uploadFileController,
	}
}

func (u *UploadRoute) Register() {
	u.r.POST("/upload", u.uploadFileController.Handle)
}

upload_route é responsável por registar uma nova rota e chamar o controller handle.

upload_file_controller.go

func (u *UploadFileController) Handle(c *gin.Context) {
	fileHeader, err := c.FormFile("file")
	if err != nil {
		c.JSON(400, gin.H{
			"message": "Error uploading file",
			"code":    400,
		})
	}
        
	file, err := fileHeader.Open()
	if err != nil {
		c.JSON(400, gin.H{
			"message": "Error to open file headers",
			"code":    400,
		})
	}
        
	input := usecases.UploadFileInput{
		Name:     fileHeader.Filename,
		Size:     fileHeader.Size,
		Mimetype: fileHeader.Header.Get("Content-Type"),
		Reader:   file,
	}
        
	err = u.uploadFile.Execute(input)
	if err != nil {
		c.JSON(400, gin.H{
			"message": err.Error(),
			"code":    500,
		})
	}
        
	c.JSON(200, gin.H{
		"message": "Success upload file",
		"code":    200,
	})
}

upload_file_controller é responsável por fazer a leitura do arquivo, preparar o input(dto) e chamar o usecase para fazer o upload.

upload_file.go

type UploadFile struct {
	fileRepository domain.FileRepository
	bucket         adapters.Bucket
}

type UploadFileInput struct {
	Name     string    `json:"name"`
	Mimetype string    `json:"mimetype"`
	Size     int64     `json:"size"`
	Reader   io.Reader `json:"reader"`
}

func NewUploadFile(fileRepository domain.FileRepository, bucket adapters.Bucket) *UploadFile {
	return &UploadFile{
		fileRepository,
		bucket,
	}
}

func (u *UploadFile) Execute(input UploadFileInput) error {
	fileID := uuid.NewString()
	bucketName := os.Getenv("MINIO_BUCKET_NAME")
	ext := path.Ext(input.Name)
	newName := fmt.Sprintf("%s%s", fileID, ext)
	file, err := domain.NewFile(
		fileID,
		newName,
		input.Mimetype,
		fmt.Sprintf("/%s/%s", bucketName, newName),
		input.Size,
		input.Reader,
	)
	if err != nil {
		return err
	}
	err = u.fileRepository.Save(file)
	if err != nil {
		return nil
	}
	err = u.bucket.PutObject(file)
	if err != nil {
		return err
	}
	return nil
}

upload_file é responsável por criar uma nova entidade File, logo após vai salvar em um repositório, e enviar o buffer do arquivo para o serviço de upload no min.io.

minio.go

type Minio struct {
	client     *minio.Client
	ctx        context.Context
	bucketName string
}

func NewMinio() *Minio {
	endpoint := os.Getenv("MINIO_ENDPOINT")
	accessID := os.Getenv("MINIO_ACCESS_KEY")
	accessKey := os.Getenv("MINIO_SECRET_KEY")
	bucketName := os.Getenv("MINIO_BUCKET_NAME")
	client, err := minio.New(endpoint, &minio.Options{
		Creds:  credentials.NewStaticV4(accessID, accessKey, ""),
		Secure: false,
	})
	if err != nil {
		panic(err)
	}
	return &Minio{
		client:     client,
		ctx:        context.Background(),
		bucketName: bucketName,
	}
}

func (m *Minio) CreateBucket() error {
	existingBucket, err := m.client.BucketExists(m.ctx, m.bucketName)
	if err != nil {
		return err
	}
	if existingBucket {
		return nil
	}
	err = m.client.MakeBucket(m.ctx, m.bucketName, minio.MakeBucketOptions{})
	if err != nil {
		return err
	}
	fmt.Printf("Created bucket %s\n", m.bucketName)
	return nil
}

func (m *Minio) PutObject(file *domain.File) error {
	_, err := m.client.PutObject(m.ctx, m.bucketName, file.Name, file.Reader, file.Size, minio.PutObjectOptions{})
	if err != nil {
		return err
	}
	return nil
}

Em minio.go, criei uma nova conexão com o container minio, logo após, verifico se um bucket existe, se caso exista eu ignoro a criação, e depois tenho a função responsável por fazer o upload no bucket.

Retorno da API

[GET] /uploads

[
	{
		"FileID": "268be89e-a71a-4e2b-afe1-17dbc12b0a49",
		"Name": "268be89e-a71a-4e2b-afe1-17dbc12b0a49.jpg",
		"FileType": "image/jpeg",
		"Size": 75384,
		"Reader": {
			"Closer": null
		},
		"Path": "/aurora/268be89e-a71a-4e2b-afe1-17dbc12b0a49.jpg"
	}
]
Carregando publicação patrocinada...