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"
}
]