Importante

Você está vendo a versão anterior da nova experiência da Alura que estamos preparando para você. Em breve, ela ganha uma identidade visual novinha totalmente pensada em potencializar seus estudos!

17
respostas

Upload de Imagens com Multer - gravar filename

Pessoal, estou fazendo um projeto bem semelhante ao alurapic, mas com cadastro de pessoas. E durante o cadastro, a pessoa pode incluir uma foto dela.

Para isso, estou usando o ngFileUpload (https://github.com/danialfarid/ng-file-upload) e o Multer. Com isso, já consigo fazer o upload da imagem no diretório especificado (no caso, "uploads"), juntamente com os demais dados, que estão sendo gravados no MongoDB.

Porém, não estou conseguindo gravar o nome da imagem (o filename) juntamente com os demais dados do form no Mongo. Meu código está assim:

Trecho HTML do form com o input file:

<form novalidate name="formulario" class="row" ng-submit="submeter()">
    <label class="btn-bs-file btn btn-primary">
        Selecione a imagem
        <input type="file" ngf-select ng-model="worker.file" name="file" ngf-pattern="'image/*'" accept="image/*" ngf-max-size="20MB" file-model="myFile"/>
    </label>
    <label>Image Preview</label>
    <div class="imgArea">
        <img style="width:150px;" ngf-thumbnail="worker.file || '/imagens/thumb.png'" ng-model="worker.file"/>
    </div>
    <button type="submit" class="btn btn-primary" ng-disabled= "formulario.$invalid">
        Save
    </button>
</form>

mydirective.js (para impedir que haja redirect quando dar submit):

.directive('fileModel', ['$parse', function ($parse) {
    return {
        restrict: 'A',
        link: function(scope, element, attrs) {
            var model = $parse(attrs.fileModel);
            var modelSetter = model.assign;

            element.bind('change', function(){
                scope.$apply(function(){
                    modelSetter(scope, element[0].files[0]);
                });
            });
        }
    };
}]);

worker-controller.js:

$scope.submeter = function() {

        if ($scope.formulario.$valid) {
            cadastroDeWorkers.cadastrar($scope.worker)
            .then(function(dados) {
                $scope.mensagem = dados.mensagem;
                if (dados.inclusao) $scope.worker = {};
            })
            .catch(function(erro) {
                $scope.mensagem = erro.mensagem;
            });
        }

        var file = $scope.myFile;
        var uploadUrl = "/upload";
        var fd = new FormData();
        fd.append('file', file);

        $http.post(uploadUrl,fd, {
            transformRequest: angular.identity,
            headers: {'Content-Type': undefined}
        })
        .success(function(){
            console.log("success!!");
        })
        .error(function(){
            console.log("error!!");
        });
    };

express.js (e a tentativa de inserir o filename no Mongo):

var express = require('express');
var app = express();
var consign = require('consign');
var bodyParser = require('body-parser');
var fs = require('fs');
var multer = require('multer');

app.use(function(req, res, next) { //allow cross origin requests
res.setHeader("Access-Control-Allow-Methods", "POST, PUT, OPTIONS, DELETE, GET");
    res.header("Access-Control-Allow-Origin", "http://localhost");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    next();
});

app.use(express.static('./public'));
app.use(bodyParser.json());

var storage = multer.diskStorage({ 
  destination: function (req, url, cb) {
      cb(null, './uploads/')
  },
  filename: function (req, url, cb) {
      var datetimestamp = Date.now();
      cb(null, url.fieldname + '-' + datetimestamp + '.' + url.originalname.split('.')[url.originalname.split('.').length -1]);
  }
});
var upload = multer({ //multer settings
    storage: storage
}).single('file');

/** API path that will upload the files */

app.post('/upload', function(req, res, next) {
    //from this point I tried to insert filename into MongoDB
    upload(req,res,function(err){
        if(err){
            res.json({error_code:1,err_desc:err});
            return;
        }
        res.json({error_code:0,err_desc:null});

        console.log(req.file);
        console.log('TEST: ' + req.file.filename);
        fs.readFile(req.file.path, function (err, data) {
            var newPath = "./uploads/" + req.file.filename;
            fs.writeFile(newPath, data, function (err) {
                res.send("hi");  
            });
            console.log(newPath);
        });

    });
});

module.exports = app;

E por fim, o model worker.js (somente o trecho relevante):

var mongoose = require('mongoose');
var schema = mongoose.Schema({
    (other fields from form)...
    file: {
        data: Buffer, 
        contentType: String,
        required: false
    },
    ...
    //Se eu tentar usar apenas type: String no file, ele exibe um erro tentando inserir a imagem como base64
});
mongoose.model('Worker', schema);

O erro exibido no console é:

{ fieldname: 'file',
  originalname: '1.jpg',
  encoding: '7bit',
  mimetype: 'image/jpeg',
  destination: './uploads/',
  filename: 'file-1493730955783.jpg',
  path: 'uploads\\file-1493730955783.jpg',
  size: 15205 }
TEST: file-1493730955783.jpg
./uploads/file-1493730955783.jpg
_http_outgoing.js:356
    throw new Error('Can\'t set headers after they are sent.');
    ^

Error: Can't set headers after they are sent.
    at ServerResponse.OutgoingMessage.setHeader (_http_outgoing.js:356:11)
    at ServerResponse.header 
(C:\wamp\www\workbr\node_modules\express\lib\response.js:725:10)
    at ServerResponse.send 
(C:\wamp\www\workbr\node_modules\express\lib\response.js:170:12)
    at C:\wamp\www\workbr\config\express.js:49:21
    at FSReqWrap.oncomplete (fs.js:123:15)

npm ERR! Windows_NT 10.0.14393
npm ERR! argv "C:\\Program Files\\nodejs\\node.exe" "C:\\Program 
Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js" "start"
npm ERR! node v6.9.5
npm ERR! npm  v3.10.10
npm ERR! code ELIFECYCLE
npm ERR! workbr@1.0.0 start: `node server.js`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the workbr@1.0.0 start script 'node server.js'.
npm ERR! Make sure you have the latest version of node.js and npm installed.
npm ERR! If you do, this is most likely a problem with the workbr package,
npm ERR! not with npm itself.
npm ERR! Tell the author that this fails on your system:
npm ERR!     node server.js
npm ERR! You can get information on how to open an issue for this project 
with:
npm ERR!     npm bugs workbr
npm ERR! Or if that isn't available, you can get their info via:
npm ERR!     npm owner ls workbr
npm ERR! There is likely additional logging output above.

npm ERR! Please include the following file with any support request:
npm ERR!     C:\wamp\www\workbr\npm-debug.log

Para quem quiser dar uma olhada, o projeto está no GitHub

Qual o erro no meu código e como posso ajustar? Agradeço muito!

17 respostas

Oi Airton!

Pela mensagem você esta tentando modificar o header de uma req/res depois queva resposta já foi enviada e isso é impossível , por isso o erro. Tem que alterar antes.

Veja que vc faz res.json e lá depois faz res.send('hi'). Res.json envia a resposta, por isso não pode fazer o res.send depois.

Oi Flávio,

Mas o simples fato de eu alterar a ordem dos blocos não altera o resultado, certo? Tentei inserir fs.readFile antes do res.json, mantendo dentro da função de callback de upload, mas continua o mesmo erro.

Outra coisa: essa função seria mesmo a responsável por enviar ao Mongo o filename da imagem do upload? É no res.send() que isso ocorre?

Vou te mostrar com detalhes. Veja seu código e os comentários:

app.post('/upload', function(req, res, next) {
    //from this point I tried to insert filename into MongoDB
    upload(req,res,function(err){
        if(err){

          // BELEZA, TA CERTO, VC DA UMA RESPOSTA E O RETURN EVITA QUE O BLOCO CONTINUE
            res.json({error_code:1,err_desc:err});
            return;
        }

      // JÁ ENVIOU UMA RESPOSTA, NÃO PODE MAIS ENVIAR OUTRA, PORQUE ELA JÁ FOI ENVIADA!
        res.json({error_code:0,err_desc:null});

        console.log(req.file);
        console.log('TEST: ' + req.file.filename);
        fs.readFile(req.file.path, function (err, data) {
            var newPath = "./uploads/" + req.file.filename;
            fs.writeFile(newPath, data, function (err) {


              // BOOM! PROBLEMA..VC ESTA ENVIANDO UMA RESPOSTA, MAS UMA JÁ FOI ENVIADA. É COMO SE OS CORREIOS JÁ TIVESSEM SAÍDO PARA ENTREGA E VOCÊ QUER QUE ELE ENTREGUE UM NOVO PACOTE, NÃO VAI ROLAR.
                res.send("hi");  
            });
            console.log(newPath);
        });

    });
});

Sendo assim, fica a lei que, jamais dentro de uma rota do Express que lide com req e res você pode enviar a resposta mais de uma vez, lembrando que res.json() envia como resposta um json e res.send() envia como resposta um texto, mas ambos enviam.

Tipo, viajei total com seu res.send('hi'), não faço a menor ideia do que você deseja com isso, ele é o problema.

Pegou a ideia?

Sim, entendi a lógica da tentativa dupla de envio. E a intenção com o res.send() era fazer um teste para ver se algo imprimia ou se era enviado para o Mongo (nem que fosse um "hi").

Retirando ele, o erro não ocorre, mas o dado (filename da imagem) continua não sendo gravado no Mongo (eu imprimo no console, mas só).

O que está faltando?

Mas em nenhum momento do seu código você esta persistindo o tal dado, como ele poderia ser gravado no banco? Ou eu não entendi sua pergunta.

Deve ser esse o problema. O que ocorre é que eu tenho outros campos no mesmo formulario que são gravados normalmente (como o que é feito no curso), mas este eu me perdi, por que eu crio um nome para ele no express.js, aqui:

filename: function (req, url, cb) {
      var datetimestamp = Date.now();
      cb(null, url.fieldname + '-' + datetimestamp + '.' + url.originalname.split('.')[url.originalname.split('.').length -1]);
  }

Eu tenho então o dado (o novo nome da imagem), consigo imprimir no console, mas não consigo unir este com os demais dados do formulário e gravá-lo no Mongo... Onde eu faço a persistência a partir daí?

Melhor postar o código completo, porque só esse pedaço não consigo entender rapidamente o código.

Eu preciso do código da API que recebe os dados do arquivo...pelo o que eu entend, é o /upload, certo?

Eu inclui todo o codigo que está trabalhando no upload atualmente na minha aplicação. Havia um outro trecho, no arquivo controller da aplicação (arquivo chamado workers-controller.js e que não é mencionado no post aqui), que eu comentei por que não estava fazendo upload do arquivo para a pasta /uploads. Segue este trecho:

.controller('MyImg',['Upload','$window',function(Upload,$window){
        var vm = this;
        vm.submit = function(){ //function to call on form submit
            if (vm.formulario.file.$valid && vm.file) { //check if from is valid
                vm.upload(vm.file); //call upload function
            }
        }
        vm.upload = function (file) {
            Upload.upload({
                url: '/upload', //webAPI exposed to upload the file
                data:{file:file} //pass file as data, should be user ng-model
            }).then(function (resp) { //upload function returns a promise
                if(resp.data.error_code === 0){ //validate success
                    $window.alert('Success ' + resp.config.data.file.name + 'uploaded. Response: ');
                } else {
                    $window.alert('an error occured');
                }
            }, function (resp) { //catch error
                console.log('Error status: ' + resp.status);
                $window.alert('Error status: ' + resp.status);
            }, function (evt) {
                console.log(evt);
                var progressPercentage = parseInt(100.0 * evt.loaded / evt.total);
                console.log('progress: ' + progressPercentage + '% ' + evt.config.data.file.name);
                vm.progress = 'progress: ' + progressPercentage + '% '; // capture upload progress
            });
        };
    }]);

Não sei se era desse código que vc estava falando... Mas também estou com o projeto completo no GitHub, se vc preferir.

Oi Airton!

É o código do servidor, não do Angular que preciso ver. É o código do servidor que interage com o banco, que recebe a imagem, é aquele no qual você esta com dúvidas.

Ah, sim! Por enquanto tenho apenas métodos para adição e lista dos itens.

app/api/workers.js

var mongoose = require('mongoose');

module.exports = function(app) {

    var api = {};
    var model = mongoose.model('Worker');

    api.lista = function(req, res) {
      model.find()
        .then(function(workers) {
            res.json(workers);
        }, function(error) {
            console.log(error);
            res.sendStatus(500);
        });
    };

    api.adiciona = function(req, res) {
      model.create(req.body)
        .then(function(worker) {
            res.json(worker);
        }, function(error) {
            console.log(error);
            res.sendStatus(500);
        });
    };
    return api;
};

Mas e aquele código do express.js que você esta lidando com o recebimento do arquivo? É nele que você fez o teste e esta com problema. Você desativou?

Sim, eu comentei a linha res.send("hi"), mas mantive o metodo fs.writeFile. Assim o erro não ocorre.

Então, na sua API de upload você precisa gravar o arquivo em disco. Quando grava o arquivo em disco, fisicamente, guardará o caminho do arquivo no banco. Veja, você não grava a imagem no banco. apenas o caminho dela.

Para isso você precisa:

1 - receber os dados e gravá-los no file system 2 - Guardar o caminho do arquivo caso tudo tenha sido feito corretamente no MOngoose. Para isso, precisará de algum Esquema. Onde você quer guardar o caminho? No usuário? Então, crie uma propriedade nele que guarde o caminho da foto. 3 - Toda vez que você precisar da foto, buscará a foto no seu filesystem com o endereço que guardou, por exemplo, no seu equema de Usuário e enviará para quem pedi.

Isso envolve: processo de escrita de um arquivo, processo de leitura de um arquivo. Você precisa consolidar essas duas etapas. Vi no seu código que você já ensaiou um teste com fs.readFile e fs.writeFile. É por ai mesmo. Só lembre que quando a escrita do arquivo for bem sucedida, no callback para fs.writeFile você grava o caminho da foto no banco.

Mas é exatamente essa minha dúvida: como gravo o caminho da imagem no banco, no callback para fs.writeFile?

No callback para o writeFile, você precisa acessar um MODEL do Mongoose. Lembre-se que é por esses modelos que você realiza a persistência do seu banco. Ou você cria um novo modelo, ou adiciona uma propriedade string que vai guardar a informação que você quer. Como essa foto é do usuário, acredito que você tenha um modelo Usuario ou User.