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.