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.
