Redimensionamento e upload de imagens em um bucket do Amazon S3 utilizando GoLang.
Tags: aws, go
Published: 2020-08-05 02:09:13

Recentemente uma demanda, inicialmente desafiadora, apareceu em uma sprint e fiquei encarregado de resolve-lá. Criar um microserviço capaz de redimensionar uma imagem em três tamanhos diferentes e posteriormente realizar o upload das mesmas juntamente com a imagem original em um bucket do Amazon S3.

Anteriormente esse serviço era realizado na nossa própria API desenvolvida em node, o que nos gerou alguns problemas de desempenho e a necessidade de mudança.

Nosso CTO sugeriu a linguagem GoLang, visto que é uma linguagem que possuí duas características fascinantes:
facilidade em programar e sua alta capacidade de performance, diferentemente de linguagens como C e C++ que possuem alta performance porém alta complexidade de desenvolvimento. Após algumas pesquisas resolvi aceitar e desafio e começar a programar.

Após configurar o ambiente de desenvolvimento (utilizei o GoLand da JetBrains), comecei por importar os pacotes que iria utilizar:

#	
    "github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/disintegration/imaging"
	"github.com/julienschmidt/httprouter"
	"github.com/nfnt/resize" #

Definindo os pacotes que precisaria, criei uma função que recebendo alguns parâmetros específicos para uma requisição http:

#
    func handler(w http.ResponseWriter, r *http.Request, ps httprouter.Params)

Com a declaração da função pronta, começamos recebendo a imagem através de um Multipart.File (upload) e a acessamos através de um FormFile de key "file":

#
    file, fileHeader, err := r.FormFile("file")

Feito isso podemos criar uma sessão na AWS utilizando um pacote importado anteriormente:

#
	s, err := session.NewSession(&aws.Config{
		Region: aws.String("região-aws"),
		Credentials: credentials.NewStaticCredentials(
			"id-secreto-aws", // id
			"key-secreta-aws",   // secret
			""),  // token podemos deixar em branco
	})

Agora defino 2 arrays com os tamanhos que quero que as imagens sejam redimensionadas (utilizo o tipo uint pois é o que a função de redimensionamento do pacote aceita) e também transformo meu file do tipo file em tipo imagem.

#
     //defino tamanhos de redimensionamento da imagem
     width := [3]uint{1140, 770, 350}
     height := [3]uint{550, 650, 250}

     //file para formato jpg
     img, err := imaging.Decode(file)

Após definir os tamanhos de redimensionamento e transformar o arquivo em imagem, faço um loop para redimensionar a imagem em cada tamanho definido e chamar outra função para realizar o upload no bucket do Amazon S3, e essa funçao receberá como parâmetros a sessão da aws criada anteriormente, a imagem redimensionada e fileHeader do file original recebido:

#

	for i:= 0; i < 3; i++ {
		image := resize.Resize(width[i], height[i], img, resize.Lanczos3)

		fileName, err := UploadFileToS3(s, image, fileHeader)
		if err != nil {
			fmt.Fprintf(w, "Could not upload file")
		}

		fmt.Fprintln(w, "Image uploaded successfully: ", fileName)

	}

Vou chamar a minha função de upload na amazon de UploadFileToS3 e ela retornará uma string com o nome do arquivo criado ou um erro (note que o retorno da função é armazenado em uma variável fileName no passo anterior):

#
     func UploadFileToS3(s *session.Session, image image.Image, fileHeader *multipart.FileHeader) (string, error)

Enfim chegou a hora de fazer o upload das fotos redimensionadas. Aqui o primeiro passo é coletar os dados codificados na memória e salvar em uma variável e apenas depois disso realizar a codificação. O segundo passo é criar um nome único para o arquivo que servirá como uma key (buscar, remover arquivos) e o terceiro passo é configurar o upload para o aws S3:

#
    // get the file size and read
	// the file content into a buffer
	buf := new(bytes.Buffer)
	err := jpeg.Encode(buf, image, nil)
	if err != nil {
		fmt.Println("failed to create buffer", err)
	}

	// create a unique file name for the file
	tempFileName := "" + bson.NewObjectId().Hex() +                      filepath.Ext(fileHeader.Filename)

	// config settings: this is where you choose the bucket,
	// filename, content-type and storage class of the file
	// you're uploading
	_, err = s3.New(s).PutObject(&s3.PutObjectInput{
		Bucket:               aws.String("nome-do-seu-bucket-no-s3"),
		Key:                  aws.String(tempFileName),
		ACL:                  aws.String("public-read"),// pode ser privado
		Body:                 bytes.NewReader(buf.Bytes()),
		ContentType:        aws.String(http.DetectContentType(buf.Bytes())),
	})
	if err != nil {
		return "", err
	}

	return tempFileName, err

Estamos quase lá! Nossas funções de redimensionamento e upload já estão prontas e agora só precisamos chama-las no nosso main. Como posteriormente terei que utilizar parâmetros na minha rota, tive que usar uma biblioteca específica para instanciar um novo router, definir a minha rota e informar em qual porta meu servidor estará rodando:

#
     func main() {
	   router:= httprouter.New()
	   router.POST("/upload/:id", handler)
	   log.Println("Upload server started")
	   log.Fatal(http.ListenAndServe(":3000", router))
     }

E prontinho... realizamos nosso serviço de redimensionamento e upload de imagens.
Ainda realizarei algumas adições no sistema mas a base está pronta e creio que nada mal para um primeiro contato com a linguagem.