Can't Use Multer Properly In A Controller (Node/Express.js) - node.js

I'm having an issue with file uploading with Multer and unfortunately, I don't have a lot of experience with Multer.
In this project, I'm trying to structure things such that routes call functions (controllers) which run commands, for example creating a product etc.
I'm pretty sure I set up Multer correctly, however when I try to req.file.filename in the controller it returns an undefined.
This is my setup (currently Multer is a helper function, I'm going to move it to middleware after as this is incorrect).
File Storage Helper Func
const multer = require("multer");
//SET Storage
let storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, "public/uploads");
},
filename: function (req, file, cb) {
const fileName = file.originalname.replace(" ", "-");
cb(null, fileName + "-" + Date.now());
},
});
const uploadOptions = multer({ storage });
module.exports = { uploadOptions };
Product Router
const express = require("express");
const router = express.Router();
//#Helpers
const { uploadOptions } = require("./../../Helpers/FileStorage.helper");
//#Categories
const {
getProducts,
postProduct
} = require("./../../Controllers/Products/product.controller");
//GET ALL Products + ADD NEW Product
router
.get("/", getProducts)
.post("/", uploadOptions.single("image"), postProduct);
And finally, the post product controller:
//POST A New Product
const postProduct = expressAsyncHandler(async (req, res) => {
const category = await Categories.findOne(req.body.category);
if (!category) return res.status(400).send("Category is invalid, try again!");
const fileName = req.file.filename;
const basePath = `${req.protocol}://${req.get("host")}/public/upload/`;
const product = new Product({
name: req.body.name,
description: req.body.description,
richDescription: req.body.richDescription,
image: `${basePath}${fileName}`,
brand: req.body.brand,
price: req.body.price,
category,
countInStock: req.body.countInStock,
rating: req.body.rating,
numReviews: req.body.numReviews,
isFeatured: req.body.isFeatured,
});
const productList = await Product.findOne({ name: product.name });
if (productList != null) {
return res.status(404).send("Product already exists! Please try again!");
}
try {
await product.save();
res.status(200).send(product);
} catch (e) {
res.status(500).send("Product was not created! Error: " + e.message);
}
});
I know the traditional way of doing this in routes would be the following (which works!):
router.post("/", uploadOptions.single("image"), async(req,res) => {
//Run function
}
However, as I mentioned above, I'm trying to break the route actions up into controller functions.
When console.log(req.file) it returns an undefined.
I suspect the props aren't being passed to the postProduct function which is what causes the error, but I can't figure out how to resolve this. I've been staring at this too long, perhaps it's an easy thing to resolve and I'm being stupid (highly probable).
If someone could assist me in fixing this and explain where I'm going wrong, I would be eternally grateful.
Edit: This is the ERROR: " TypeError: Cannot read property 'filename' of undefined "

This statement: return console.log(req.file) in postProduct prevents the rest of the code from running and returns void. Console.log always returns void.
If you want to just log the file and continue with the rest of the function, remove the return keyword: console.log(req.file).
What does the console.log output in the terminal when you run the app?

I dont think that you are parsing the form data correctly:
// this is for parsing json data
app.use(express.json());
// this is for parsing form data
app.use(express.urlencoded({ extended: false }));

Related

POSTing from Angular to Node. Getting empty body on node

Im posting two fields in Angular to a NodeJs endpoint.
I usually post on the body and everything is perfect at Node, but this time, I have to post a form to upload a file.
So this is my code for posting the form data (Angular side):
var APIURL = sessionStorage.getItem('endPoint') + "profile/updateeocoverage";
let formData = new FormData();
formData.append("data", JSON.stringify(this.payLoad));
if (this.eofile) {
formData.append("myfile", this.eofile, this.eofile.name);
}
this.httpClient.post(APIURL, formData).subscribe(
result => {
....
My problem is that I always retrieved the body at node as follows:
router.post('/updateeocoverage', async (req, res, next) => {
console.log(req.body)
return;
....
But with the method Im using now in Angular, req.body is retrieving {}
Is the POST wrong, or the router wrong at Node side?
Thanks.
UPDATE ON PABLO'S ANSWER BELOW (SOLUTION PROVIDED) for whoever runs into this issue:
Using Multer solved the problem, but as he said some workaround is needed as to set the file name, but most important it is required to authenticate the user, so:
const multer = require('multer')
const path = require('path')
To authenticate the user, I send the authentication parameters on the header. Sending it as formdata.append didn't work for me. This sets true or false to upload the file, otherwise anyone can upload anything to the route:
async function authenticateUser(req, file, cb) {
let tempcred = JSON.parse(req.headers.data)
let credentials = tempcred.credentials;
let userData = await utils.isValidUser((credentials), false);
if (userData.isValid == false) {
cb(null, false)
return;
}
else {
cb(null, true)
}
}
Then, since Multer uploads the file with a random name, and I need to save it with the user ID name and the file extension, I do the following:
var storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/eofiles/')
},
filename: async function (req, file, cb) {
let tempcred = JSON.parse(req.headers.data)
let credentials = tempcred.credentials;
let userid = await utils.decrypt(credentials.userid, process.env.SECRET);
cb(null, userid + path.extname(file.originalname))
}
});
Finally, I declare the upload variable for using it with Multer:
var upload = multer({ storage: storage, fileFilter: authenticateUser })
And set the router:
router.post('/updateeofile', upload.single("myfile"), async (req, res, next) => {
let filename = req.file.filename //gets the file name
...
...
do my stuff, save on database, etc
...
...
});
For the record, "myfile" is the input file id.
And this is how I upload the file from Angular:
var APIURL = sessionStorage.getItem('endPoint') + "eoset/updateeofile";
const httpOptions = {
headers: new HttpHeaders({
'data': `${JSON.stringify(this.payLoad)}`
})
};
let formData = new FormData();
if (this.eofile) {
formData.append("myfile", this.eofile, this.eofile.name);
}
this.httpClient.post(APIURL, formData, httpOptions).subscribe(
result => {
...
...
...
},
error => {
});
I spent 6 hours on this today. I hope this helps you and saves you some time.
Try using Multer
npm i multer
const multer = require('multer')
const upload = multer({ dest: 'uploads/' })
router.post('/updateeocoverage', upload.single('myfile'), function (req, res, next) {
res.json(JSON.parse(req.body.data))
})
You will need to indicate the destination folder (line 2) and work out the file name, extension, etc.
First thing to do is ensure that the body isn't empty before making the request, so do a console.log before the request:
console.log(formData);
this.httpClient.post(APIURL, formData).subscribe(
result => {
....
After that, if the body is filled correctly, try to put the body like this on the request method:
this.httpClient.post(APIURL, {formData})
or
this.httpClient.post(APIURL, {
"field1": formData.field1,
...
})
and if these two things don't correct your issue, problaby you have something wrong on back-end side

Some properties missing in req.body inside multer middleware

I have a client application that posts some formData to my node backend. The data coming from the client is initially an object that contains a files property among others. Before posting to the back-end, I convert the object to formData.
// client.js file
const person = {
email: 'johndoe#gmail.com',
age: 19,
files: [File, File, File],
occupation: '',
status: 'active',
}
person.occupation = this.getOccupation(); // getOccupation() is defined somewhere in the file
const formData = new FormData();
Object.entries(person).forEach(([key, value]) => {
if (key === "files") {
for (let i = 0; i < person[key].length; i++) {
formData.append("files", person[key][i]);
}
} else {
formData.append(key, value);
}
});
// The following logs output the correct information so we're good until this point
console.log(formData.getAll("files"));
console.log(formData.get("occupation"));
console.log(formData.get("status"));
this.submitData(formData); // Posts to the backend
The data is posted to a route in the node backend with the multer middleware - uploadFiles().
const submitDataOptions = {
storage: multer.diskStorage({
destination: "./public/uploads/users/files",
filename: function (req, file, cb) {
console.log('Request body');
console.log(req.body);
console.log('Occupation');
console.log(req.body.occupation);
console.log('Status');
console.log(req.body.status);
const ext = file.mimetype.split("/")[1];
const datetimestamp = Date.now();
const fname = `${datetimestamp}.${ext}`;
cb(null, fname);
},
}),
};
uploadFiles() {
return multer(submitDataOptions).array("files");
}
The files get uploaded just fine but when I console.log(req.body), I notice that an object with only the properties email and age is returned. Logging occupation and status returns undefined in either case. The multer documentation says that req.body will contain the text fields if there were any but in this case I cannot find those.
What is the issue here?
req.body might not have been fully populated yet. It depends on the order that the client transmits fields and files to the server.
So just put the files last in your object:
const person = {
email: 'johndoe#gmail.com',
age: 19,
occupation: '',
status: 'active',
files: [File, File, File],
}

How do I get "res.fields" to stop coming back as "undefined" in my application?

I am working on a project using multer to upload multiple files. To do this, I am using upload.fields in a controller file, the code looks like this:
tourController.js
const multerStorage = multer.memoryStorage();
const multerFilter = (req, file, cb) => {
if (file.mimetype.startsWith('image')) {
cb(null, true);
} else {
cb(new AppError('Not an image! Please upload images only.', 400), false);
}
};
const upload = multer({
storage: multerStorage,
fileFilter: multerFilter
});
exports.uploadTourImages = upload.fields([
{ name: 'imageCover', maxCount: 1 },
{ name: 'images', maxCount: 3 }
]);
In the next function, where I resize the images, I console.log res.files, but it comes back as undefined in my terminal output. Here is the code where I console.log res.files. It follow the code that I posted directly above:
exports.resizeTourImages = (req, res, next) => {
console.log(res.files);
next();
};
I tried using upload.array, but couldn't get that to work as expected either. I'm not sure if the problem could be with a different part of the application. Here's where the function appears in the middleware stack in the route file:
tourRoutes.js
router
.route('/:id')
.get(tourController.getTour)
.patch(
authController.protect,
authController.restrictTo('admin', 'lead-guide', 'guide'),
tourController.uploadTourImages,
tourController.resizeTourImages,
tourController.updateTour
)
I'm at a loss as to why I can't get this work. If helpful, I have all of the code uploaded to GitHub here:
https://github.com/christopherbclark/mynatours
As mentioned in the documentation, res.files is undefined because multer populates the files under the req-object. You need to change it to:
exports.resizeTourImages = (req, res, next) => {
console.log(req.files);
next();
};

multer and node/express file upload not working with Angular

I'm trying to do single file upload using multer. Although I'm able to upload a file manually in the relevant folder using tools like postman for testing the express route, I cannot upload it using Angular frontend. I created a folder called uploads in the node backend folder where the files are supposed to get uploaded. Also I need to upload the file within a form and pass it to a api where it should take the file along with other parameters also. But unfortunately, it is returning status 500 with Internal Server Error on the browser console while on my node terminal it is returning Cannot read property 'path' of undefined.
My node backend code which is working fine is below:
const multer = require('multer')
const storage = multer.diskStorage({
destination: function(req, file, cb) {
cb(null, './uploads/')
},
filename: function(req, file, cb) {
cb(null, Date.now() + file.originalname)
}
})
const upload = multer({storage: storage})
let baseUrl = appConfig.apiVersion+'/blogs';
app.post(baseUrl+'/create', upload.single('imagePath'), (req, res) => {
var today = time.now()
let blogId = shortid.generate()
let newBlog = new BlogModel({
blogId: blogId,
title: req.body.title,
description: req.body.description,
bodyHtml: req.body.blogBody,
isPublished: true,
category: req.body.category,
author: req.body.fullName,
created: today,
lastModified: today,
imagePath: req.file.path //node console is pointing towards this point
}) // end new blog model
let tags = (req.body.tags != undefined && req.body.tags != null && req.body.tags != '') ? req.body.tags.split(',') : []
newBlog.tags = tags
newBlog.save((err, result) => {
if (err) {
console.log(err)
res.send(err)
} else {
res.send(result)
}
}) // end new blog save
});
Below is my Angular Component code which is not working:
selectImage(event) {
if(event.target.files.length > 0){
const file = event.target.files[0];
this.images = file;
}
}
public createBlog(): any {
const formData = new FormData();
const form = formData.append('imagePath', this.images);
let blogData = {
title: this.blogTitle,
description: this.blogDescription,
blogBody: this.blogBodyHtml,
category: this.blogCategory,
imagePath: form
} //end blogData
console.log(blogData);
this.blogHttpService.createBlog(blogData).subscribe(
data => {
console.log(data);
this.toastr.successToastr('Blog Posted Susseccfully!', 'Success!');
setTimeout(() =>{
this.router.navigate(['blog', data.blogId]);
}, 1000)
},
error => {
console.log(error);
console.log(error.errorMessage);
this.toastr.errorToastr('This is not good!', 'Oops!');
})
}
Angular Service code
public createBlog(blogData): any {
let myResponse = this._http.post(this.baseUrl + '/create', blogData);
return myResponse;
}
Frontend HTML Code:
<div>
<input type="file" name="imagePath" (change)="selectImage($event)" />
</div>
It seems like you created a formData object, but you are not actually doing anything with it. As you can see here, you are building up an object and sending it along with your request, but it does not include your formData
let blogData = {
title: this.blogTitle,
description: this.blogDescription,
blogBody: this.blogBodyHtml,
category: this.blogCategory,
imagePath: this.imagePath
} //end blogData
console.log(blogData);
this.blogHttpService.createBlog(blogData).subscribe(
Not entirely sure what the exact syntax would be in your case, but here you can see some sample code I have in a project of mine which will hopefully give you an idea.
changeHandler(e) {
const fd = new FormData();
fd.append('sample', e.target.files[0]);
axios.post('/save-image', fd).then(i => {
this.setState({img: i.data.filename});
});
}
As you can see, the formData is what I am actually sending to the server.

Req.body is not iterable in node.js

I am building mock restful API to learn better. I am using MongoDB and node.js, and for testing I use postman.
I have a router that sends update request router.patch. In my DB, I have name (string), price (number) and imageProduct (string - I hold the path of the image).
I can update my name and price objects using raw-format on the postman, but I cannot update it with form-data. As I understand, in raw-form, I update the data using the array format. Is there a way to do it in form-data? The purpose of using form-data, I want to upload a new image because I can update the path of productImage, but I cannot upload a new image public folder. How can I handle it?
Example of updating data in raw form
[ {"propName": "name"}, {"value": "test"}]
router.patch
router.patch('/:productId', checkAuth, (req, res, next) => {
const id = req.params.productId;
const updateOps = {};
for (const ops of req.body) {
updateOps[ops.propName] = ops.value;
}
Product.updateMany({_id: id}, {$set: updateOps})
.exec()
.then(result => {
res.status(200).json({
message: 'Product Updated',
request: {
type: 'GET',
url: 'http://localhost:3000/products/' + id
}
});
})
.catch(err => {
console.log(err);
res.status(500).json({
err: err
});
});
});
Using for...of is a great idea, but you can't use it like you are to loop through an object's properties. Thankfully, Javascript has a few new functions that turn 'an object's properties' into an iterable.
Using Object.keys:
const input = {
firstName: 'Evert',
}
for (const key of Object.keys(input)) {
console.log(key, input[key]);
}
You can also use Object.entries to key both the keys and values:
const input = {
firstName: 'Evert',
}
for (const [key, value] of Object.entries(input)) {
console.log(key, value);
}
I know this answer might be too late to help you but it might help someone in 2020 and beyond.
First, comment out this block:
//const updateOps = {};
//for (const ops of req.body) {
//updateOps[ops.propName] = ops.value;
//}
and change this line:
Product.updateMany({_id: id}, {$set: updateOps})
to this:
Product.updateMany({_id: id}, {$set: req.body})
Everything else is fine. I was having similar issues, but this link helped me:
[What is the difference between ( for... in ) and ( for... of ) statements in JavaScript?
To handle multi-part form data, the bodyParser.urlencoded() or app.use(bodyParser.json());body parser will not work.
See the suggested modules here for parsing multipart bodies.
You would be required to use multer in that case
var bodyParser = require('body-parser');
var multer = require('multer');
var upload = multer();
// for parsing application/json
app.use(bodyParser.json());
// for parsing application/xwww-
app.use(bodyParser.urlencoded({ extended: true }));
//form-urlencoded
// for parsing multipart/form-data
app.use(upload.array());
app.use(express.static('public'));

Resources