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.
