I'm creating a (POST) route to handle file uploads and also store some other properties to MongoDB in addition to the file path. The problem is when input validation fails, the file is still uploaded in the static(uploads) folder.
I'm using the Multer middleware for file uploads.
Setup
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "./uploads/");
},
filename: (req, file, cb) => {
cb(null, Date.now() + "-" + file.originalname);
}
});
const fileFilter = (req, file, cb) => {
if (file.mimetype === "image/jpeg" || file.mimetype === "image/png") {
cb(null, true);
} else {
//rejects storing a file
cb(null, false);
}
};
const upload = multer({
storage: storage,
limits: {
fileSize: 1024 * 1024 * 5
},
fileFilter: fileFilter
});
Fields
const seamUpload = upload.fields([
{ name: "stylePicture", maxCount: 1 },
{ name: "materialPicture", maxCount: 1 }
]);
Route
router.post("/", [auth, seamUpload], async (req, res) => {
const { error } = validateSeam(req.body);
if (error) return res.status(400).send(error.details[0].message);
const seam = new Seam({
stylePicture: req.files["stylePicture"][0].path,
materialPicture: req.files["materialPicture"][0].path,
description: req.body.description,
deliveryDate: req.body.deliveryDate,
customer: req.body.customerId
});
await seam.save();
res.send(seam);
});
Client(PostMan) Screenshot
Me as you faced with this problem.
one solution that I found is when your authentication in all modes seems error, in that case if you have a file from client, you can easily remove it like this:
if(req.file) {
fs.unlink(
path.join(__dirname, "go to root folder that 'req.file.path' leads to the file", req.file.path),
(err) => console.log(err));
}
or then in case that you got multi files, you should do the same way for each of them. If there is, I glad to here that.
I wish there be another way by using multer package for handling that.
good luck
You can have the validation middleware BEFORE the multer middleware.
That way, when the validation fails, all the subsequent middlewares would not be executed -- and thus, the files will not be uploaded.
Separate out the validation like so:
const validateSeamUpload = (req, res, next) => {
const { error } = validateSeam(req.body);
if (error) return res.status(400).send(error.details[0].message);
return next();
};
And then, mount this validateSeamUpload BEFORE the seamUpload middleware like so:
router.post("/", [auth, validateSeamUpload, seamUpload], async (req, res) => {
/** No need for validation here as it was already done in validateSeamUpload */
const seam = new Seam({
stylePicture: req.files["stylePicture"][0].path,
materialPicture: req.files["materialPicture"][0].path,
description: req.body.description,
deliveryDate: req.body.deliveryDate,
customer: req.body.customerId
});
await seam.save();
res.send(seam);
});
By the way, you can pass them as arguments to post() as well. Like so:
router.post("/", /** => See, no need for an array */ auth, validateSeamUpload, seamUpload, async (req, res) => {
/** your controller code */
});
Related
hello I am trying to create a custom middle-ware for file upload with multer with customization (allowed file type, destination folder ...). the upload is working but the req.file is always undefined, below you can find my code
edit : I have changed the code images to code snippets to make it easier to copy
thank you
// route
router.patch('/profile-picture',
[
AsyncErrorWrapper(passport.verifyJwt()),
AsyncErrorWrapper(singleFileUpload('/images/', ['image/png', 'image/jpeg'])),
], AsyncErrorWrapper(userController.updateProfilePicture))
// singleFileUpload
const {uploadConfiguration, multerDelay} = require("../services/multer.service")
/**
*
* #param {*} dest destination folder inside the /tmp
* #param {*} whitelist list of extensions which are allowed to be uploaded
* #returns
*/
module.exports = (dest, whitelist, name = 'file') => {
return async (req, res, next) => {
// get the upload config obj
const upload = uploadConfiguration(dest, whitelist)
// call the upload.single
upload.single(name)(req, res, next)
// make sur we wait for the upload to finish to solve the req.file is undefined
await multerDelay()
next()
}
}
// service
const uploadConfiguration = (dest, whitelist) => {
// storage
const storage = multer.diskStorage({
destination: (req, file, cb) => {
// set the directory
const dir = `${tmpStorage}${dest}`
mkdirp.sync(dir)
cb(null, dir)
},
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname));
},
})
// upload filter
const uploadFilter = (req, file, cb) => {
if (!whitelist.includes(file.mimetype)) {
return cb(new AppError('file type is not allowed', 400), false)
}
// make this to solve problem with req.file is undefined in the following middleware
req.file = file
cb(null, true)
}
return multer({
storage: storage,
fileFilter: uploadFilter
})
}
/**
*
* #returns promise that resolve after the queue is clear
*/
const multerDelay = () => {
console.log('multerDelay');
return new Promise((resolve) => setTimeout(() => resolve(), 0))
}
// controller
const updateProfilePicture = async (req, res, next) => {
console.log(req.file) // undefined
return res.status(200).json({msg: 'hi'})
}
The issue is, you can't run a middleware like that
When you do
upload.single(name)(req, res, next)
You are passing next to the function to run it internally
Running next more than once will always cause errors
You should split it into two functions
// singleFileUpload
module.exports = (dest, whitelist, name = 'file') => {
return async (req, res, next) => {
// get the upload config obj
const upload = uploadConfiguration(dest, whitelist)
// call the upload.single
upload.single(name)(req, res, next)
}
}
// waitForDelay
module.exports = (dest, whitelist, name = 'file') => {
return async (req, res, next) => {
// make sur we wait for the upload to finish to solve the req.file is undefined
await multerDelay()
next()
}
}
router.patch('/profile-picture',
[
AsyncErrorWrapper(passport.verifyJwt()),
AsyncErrorWrapper(singleFileUpload('/images/', ['image/png', 'image/jpeg'])),
AsyncErrorWrapper(waitForDelay()),
], AsyncErrorWrapper(userController.updateProfilePicture))
^ untested code
I am working on a mod site, and I was able to create a method that uploads an image, but I was wondering if it's possible to also upload a zip file in the same method. I tried chaining on code for the file after creating a file model, but I get an unexpected field error. Here's the method I'm trying
router.post('/upload', uploadImage.single('image'), uploadFile.single('file'), IsLoggedIn, (req, res, next) => {
console.log(req.file);
var newImg = new Image({
data: req.file.filename
});
var newFile = new File({
data: req.file.filename
});
Mod.create({
name: req.body.name,
image: newImg.data,
game: req.body.game,
file: newFile.data
}, (err, newMod) => {
if (err) {
console.log(err);
}
else {
res.redirect('/mods');
}
});
});
And here are my storage engines
const imageStorage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, './public/images')
},
filename: (req, file, cb) => {
cb(null, file.originalname)
}
});
const fileStorage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, './public/images')
},
filename: (req, file, cb) => {
cb(null, file.originalname)
}
});
const uploadImage = multer({storage: imageStorage});
const uploadFile = multer({storage: fileStorage});
Is it possible to use 2 storage engines and chain 2 uploads in one method, or am I going about it all wrong? Any clarification or direction would be greatly appreciated.
I can't figure out a simple thing. If I do pass files with the request, I want to save them, and then modify req.body a little bit inside the same multer middleware. My multer middleware:
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, './uploads/')
},
filename: (req, file, cb) => {
cb(null, req.body._id + path.extname(file.originalname))
},
})
const fileFilter = (req: Request, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
if (
file.mimetype === 'audio/wave' ||
file.mimetype === 'image/png' ||
file.mimetype === 'image/jpeg'
)
return cb(null, true)
cb(null, false)
}
const upload = multer({
storage: storage,
limits: {
fileSize: 1024 * 1024 * 3, // up to 3 megabytes
},
fileFilter: fileFilter,
})
export const saveFiles = upload.fields([
{ name: 'audio', maxCount: 1 },
{ name: 'image', maxCount: 1 },
])
Right now I do it in the router:
if (req.files) {
if ((req as any).files.audio)
req.body.data.audio = (req as any).files.audio[0].path.replace('\\', '/')
if ((req as any).files.image)
req.body.data.image = (req as any).files.image[0].path.replace('\\', '/')
}
Which is kinda annoying, I would like to just do it inside the multer somehow before it fires next(). I just can't figure out how.
So, saveFiles is your middleware function. You don't show where you actually use it, but presumably you are registering it in your router as middleware somewhere. Because it's middleware, that means it is a function that expects to be called with the arguments (req, res, next). You can replace that next argument with your own and do your work in their like this:
// multer's middlware function, we will wrap
const saveFilesMiddleware = upload.fields([
{ name: 'audio', maxCount: 1 },
{ name: 'image', maxCount: 1 },
]);
// wrap the multer middleware with our own
export const saveFiles = function(req, res, next) {
saveFilesMiddleware(req, res, err => {
if (err) {
// upon error, just call the real next with the error
next(err);
} else {
// when no error, do our housekeeping in req.body
if (req.files) {
if ((req as any).files.audio)
req.body.data.audio = (req as any).files.audio[0].path.replace('\\', '/');
if ((req as any).files.image)
req.body.data.image = (req as any).files.image[0].path.replace('\\', '/');
}
next();
}
});
};
I'm trying to set user to have one image upload. This is the code I have in the router
const multer = require('multer')
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/')
},
filename: function (req, file, cb) {
cb(null, file.filename + '-' + Date.now() + '.jpg')
}
})
const upload = multer({ storage: storage }).single('image')
appRouter.post('/upload', async (req, res) => {
try {
const uploadImage = await Image.create(req.body)
upload(req, res, () => {
res.json({
uploadImage,
success: true,
message: 'Image uploaded'
})
})
} catch (err) {
console.log(err)
}
})
and heres how I have it in model
module.exports = (db, Sequelize) => {
return db.define('image', {})
}
and this as well.
const Image = imageModel(db, Sequelize)
User.hasOne(Image)
This is a fullstack express app with auth and I'm trying to enable users to upload their own images.
Heres how it looks on postman, userId is still null:
You are never associating the image with an user.
You are creating the image, but don't add a user to the image. You need a way to identify the user and then associate it when creating (or after creating the image).
You'll probably need to implement way for users to identify themselves to do this.
Your model declaration implies that a User has an image, this doesn't mean that all images have an user, there may be images without users.
I'm studying node.js for a school project and I can't figure out why my code won't work. Whenever I upload a form that contains text and a file, the req.body gets populated but the req.files doesn't
server.js
const multer = require('multer')
const bparser = require('body-parser')
app.use(bparser.urlencoded(settings.body_parser))
...
let multer_storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, path.join(__dirname, settings.multer.destination))
},
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname))
}
})
let multer_options = {
storage: multer_storage,
fileFilter: (req, file, cb) => {
if (settings.multer.allowed_files.indexOf(file.mimetype) >= 0)
cb(null, true)
cb(null, false)
}
}
app.use(multer(multer_options).any())
app.use("*", (req, res, next) => {
if (!req.session.user)
if (req.cookies.user)
req.session.user = req.cookies.user
next()
})
for (let i = 0; i < settings.routes.length; i++) {
app.use('/', require("./core/routers/" + settings.routes[i]))
}
...
./core/routers/post.js
const router = require('express').Router()
...
router.post('/post/share/', (req, res) => {
let data = {
title: req.body.title,
user: req.session.user,
post: req.files[0].path,
tags: req.tags.split(" ")
}
post.create(data).then((result) => {
return result
})
})
I keep encountering a "TypeError: Cannot read property 'path' of undefined"
When you call cb(null, false) in your fileFilter method, you tell multer that it shouldn't process the file, but it will still enter your middleware with: req.files being undefined that's why you get that error.
If you don't want it to enter to your middleware if the file wasn't processed, then you should pass an error to the callback instead:
let multer_options = {
storage: multer_storage,
fileFilter: (req, file, cb) => {
if (settings.multer.allowed_files.indexOf(file.mimetype) >= 0)
return cb(null, true); // this return is missing
cb(new Error('Invalid file'));
}
}
In any case, you're missing a return statement before cb(null, true); otherwise you're calling twice the callback, once with true and the other once with false
To sum up, if you don't pass an Error to the fileFilter function, you should check for the presence of req.files in your middleware.
Or you can try the code below:
var tmp_path = req.file.path;