AWS NodeJS App 05 – Upload de imagens para S3

Nesta nova etapa vamos modificar nosso projeto para permitir que uma imagem seja cadastrada juntamente com o formulário do produto. Para isso vamos ter que modificar nosso projeto para suportar o upload de arquivos, modificar o controller para receber esses arquivos e através do SDK da AWS enviar a imagem para um bucket dentro do serviço S3 que iremos criar. O primeiro passo é instalar duas novas bibliotecas em nosso projeto o body-parse e o express-fileupload. Elas permitem receber requisições com arquivos binários no formulário e tratar essas informações.

npm install -s body-parser express-fileupload

O próximo passo é modificar o programa app.js para importar as duas novas bibliotecas e inseri-las no pipeline do express. Observe que ao incluir o fileUpload definimos o tamanho máximo do arquivo aceito pelo servidor.

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var bodyParser = require('body-parser');
const fileUpload = require('express-fileupload');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var productRouter = require('./routes/product');

var app = express();

app.use(fileUpload({
  limits: { fileSize: 50 * 1024 * 1024 },
}));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'hbs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/product', productRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

Agora vamos preparar nosso bucket no S3 para receber os arquivos acesse a ferramenta através do console da AWS, e clique na opção create bucket.

No wizard de criação do bucket exitem diversos parâmetros, vamos nos ater aos mais importantes, informe o nome do bucket como imageawsfaeg<seunome> isso é necessário pois o nome do bucket precisa ser único em toda AWS. Escolha a região e clique em next.

No segundo passo configure options, mantenha todos os parâmetros iguais e selecione next. No terceiro passo onde são definidas as regras de segurança, vamos inicialmente desabilitar a opção Block all public access. Assim vamos ter um bucket com dados públicos para acesso. Conclua o processo até o fim. E o seu bucket será apresentado na tela do S3.

O próximo passo é alterar nosso arquivo views/product/form.hbs para incluir um novo parâmetro na tag FORM para permitir o envio de formulários compostos por diversos tipos de dados, e incluir um novo campo no formulário para permitir ao usuário selecionar o arquivo que será enviado.

<form action = "/product/save" method="POST" enctype="multipart/form-data">
		  <div class="form-group">
		    <label for="inputName">Nome</label>
		    <input type="text" class="form-control" id="inputName" name="name" 
		    	placeholder="Nome do produto">
		  </div>
		  <div class="form-group">
		    <label for="inputDesc">Descrição</label>
		    <textarea rows="5" cols="33" class="form-control" id="inputDesc" 
		    	name="description" placeholder="Descrição do produto"></textarea>
		  </div>
		  <div class="form-group">
		    <label for="inputPrice">Preço</label>
		    <input type="number" min="0" max="1000" step="0.25" class="form-control" 
		    	id="inputPrice"  name="price" placeholder="Preço do produto">
		  </div>
		  <div class="form-group">
		    <label for="imageUpload">Imagem</label>
		    <input id="imageUpload" class="file-add" 
		    	type="file" accept="image/*" name="imageUpload" required/>
		  </div>
		  
		  
		  <button type="submit" class="btn btn-primary">Salvar</button>
		</form>

Agora vamos modificar nossa classe ProductService para criar um novo método que será responsável por fazer o upload da imagem para o bucket S3. Primeiro vamos importar a biblioteca do SDK da AWS, definir duas variáveis com o nome do bucket e a região, e em seguida instanciar o objeto S3 para ter acesso a ele. IMPORTANTE: não inserimos nenhum tipo de chave de acesso pois estamos utilizando politicias e regras do IAM para permitir os acessos da aplicação aos recursos da AWS.

var AWS = require('aws-sdk');
var bucketName = "imageawsfaegwalter";
var bucketRegion = "us-east-2";

AWS.config.update({
  region: bucketRegion
});

var s3 = new AWS.S3({
  apiVersion: "2006-03-01",
  params: { Bucket: bucketName }
});

Agora dentro da classe de serviço vamos incluir um novo método chamado uploadImageS3, este método recebe um objeto contendo os dados da imagem a ser carregada, cria um objeto que define as propriedades de como o objeto será carregado para o S3, e em seguida chama o método putObject para inserir o arquivo. Importante destacar que para que não haja duplicação do nome do arquivo, utilizamos a biblioteca uuidv4() para gerar um valor randômico que é concatenado com o nome original do arquivo.

async uploadImageS3(image){
        var filename = (uuidv4() + image.name);
        const params = {
          Bucket: bucketName,
          Key: filename,
          ACL: 'public-read',
          Body: image.data
        };

        await s3.putObject(params, function (err, data) {
          if (err) {
            console.log("Error: ", err);
          } else {
            console.log("Sucesso: " + filename);
          }
        });
        return filename;
    }

Com este código estamos prontos para inserir o arquivo no S3, porém precisamos armazenar dentro da nossa tabela produto no DynamoDB alguma relação do registro do produto com sua imagem. Para isso vamos modificar o objeto Produto.js para incluir dois novos atributos o filename e o urls3.

var {
  DataMapper, DynamoDbSchema, DynamoDbTable
} = require('@aws/dynamodb-data-mapper');
class Product{
    
}
Object.defineProperties(Product.prototype, {
    [DynamoDbTable]: {
        value: 'product'
    },
    [DynamoDbSchema]: {
        value: {
            id: {
                type: 'String',
                keyType: 'HASH'
            },
            description: {type: 'String'},
            name: {type: 'String'},
            price: {type: 'Number'},
            filename: {type: 'String'},
            urls3: {type: 'String'}
        },
    },
});
module.exports = Product;

Então vamos modificar o código do método save do controlador ProductController. Esse método agora recupera o objeto que representa os dados e conteúdo do arquivo enviado, então chamada a função da classe de serviço uploadImageS3() passando o objeto que presenta o arquivo. Como a função uploadImageS3() é possui o modificador async, seu retorno é uma promisse, utilizamos o método then para aguardar a finalização do processo de upload da imagem e ai chamamos o método do serviço para incluir o registro no DynamoDB e atualizar a nova propriedade filename.

Salve os arquivos alterados no projeto, e execute o nodemon para testar a aplicação.

Ao acessar o DynamoDB, voce vai observar que o novo documento foi inserido contendo o nome do arquivo.

E ao acessar o bucket no s3, observe que o arquivo foi inserido com sucesso.

Agora devemos modificar nossa aplicação para que na tela principal que lista os produtos, a imagem do produto seja carregada. Vamos alterar o código do método getAll() do productservice.js para que ao recuperar os objetos do DynamoDB, faça uma nova consulta ao S3 para recuperar a URL de acesso a imagem. Caso não encontre o objeto ele substitui por uma imagem fixa.

async getAll(){
        var list = [];

        var result = await mapper.scan(Product,{limit:5});
        for await (const item of result) {
            var params = {Bucket: bucketName, Key: item.filename};
            s3.getSignedUrl('getObject', params, function (err, url) {
              if(err){
                  item.urls3 = "images/items/2.jpg"; 
              }else{
                  item.urls3 = url;
              }
              list.push(item);
            });
        }
        return list;

   } 

O mesmo código precisa ser colocado no método de busca getAllBySearch() para também buscar a URL das imagens.

async getAllBySearch(search){
        var list = [];
        var result = await mapper.scan(Product,{limit:5, filter: {
                                                ...contains(search),
                                                subject: 'name'
                                            }});
        for await (const item of result) {
            var params = {Bucket: bucketName, Key: item.filename};
            s3.getSignedUrl('getObject', params, function (err, url) {
              if(err){
                  item.urls3 = "images/items/2.jpg"; 
              }else{
                  item.urls3 = url;
              }
              list.push(item);
            });
        }
        return list;
   }

Por fim basta alterar o arquivo index2.hbs para que o endereço da imagem use o valor do atributo urls3 da lista de produtos.

Ao acessar a aplicação, os novos produtos cadastrados devem apresentar a imagem carregada diretamente do S3.