122. Section Intro
123. Adding Support for File Uploads
install node.js module Multer using npm.
$ npm i multer
Let’s see what is required, at a minimum, to include file uploads in a node app.
const multer = require('multer')
Configure multer to specify which directory to store the files. In VSC, add a directory named images to the root of your project.
const upload = multer({ dest: 'images' })
Create a router to test the upload capability. The router include a middleware function named upload.single() which takes as an argument a string that will be the key (‘upload’) associated with the file that is uploaded by the client.
app.post('/upload', upload.single('upload'), (req, res) -> { res.send(); })
In Postman, create a POST request using the endpoint /upload to test the router. Select form-data and enter upload as the key and select File in the dropdown menu in the key field. Select an image on your laptop that will be sent in the request.
Test the Postman request. If successful, you should see in VSC image in your images directory. If, in VSC, you add the appropriate file extension to the file in the images directory, you will be able to view the image in VSC.
—
In the user router, add an endpoint that allows the user to upload an image as their profile picture using endpoint /users/me/avatar. Send back a 200 status if successfull.
const multer = require('multer') ... const upload = multer({ dest: 'avatars' }) router.post('/users/me/avatar', upload.single('avatar'), (req, res) => { res.send() })
Create Postman request to test the endpoint and test the endpoint.
124. Validating File Uploads
Restrict the file size.
const upload = multer({ dest: 'avatars', limits: { fileSize: 1000000 } })
Restrict the file type.
const upload = multer({ dest: 'avatars', limits: { fileSize: 1000000 }, fileFilter(req, file, callback) { if(!file.originalname.endsWith('pdf')) { return callback(new Error('File must be a PDF')) // when error occurs } callback(undefined, true) // when no error occurs } })
To allow multiple file extensions like .doc or .docx we can pass a regular expression to the string method named match().
if (!file.originalname.match(/\.(doc|docx)$/)) { ... }
125. Validation Challenge
Add validation to the avatar endpoint, allow jpg, jpeg or png files, limit to 1 MB size.
const upload = multer({ dest: 'avatars', limits: { fileSize: 1000000 }, fileFilter(req, file, callback) { if(!file.originalname.match(/\.(jpg|jpeg|png/)) { return callback(new Error('File must be an image')) // when error occurs } callback(undefined, true) // when no error occurs } }) router.post('/users/me/avatar', upload.single('avatar'), (req, res) => { res.send() })
126. Handling Express Errors
Add an express middleware function that returns a JSON object when an error occurs.
router.post('/users/me/avatar', upload.single('avatar'), (req, res) => { res.send() }, (error, req, res, next) => { res.status(400).send({ error: error.message }) })
127. Adding Images to User Profile
We can add authentication by adding the auth middleware before the multer middleware.
router.post('/users/me/avatar', auth, upload.single('avatar'), (req, res) => { res.send() }, (error, req, res, next) => { res.status(400).send({ error: error.message }) })
Add a new field named avatar in the user model to allow us to store the images in the mongodb database.
avatar: { type: Buffer }
Since we no longer want the images saved in the avatars directory, we need to remove the dest property of the
const upload = multer({ limits: { fileSize: 1000000 }, fileFilter(req, file, callback) { if(!file.originalname.match(/\.(jpg|jpeg|png/)) { return callback(new Error('File must be an image')) } callback(undefined, true) // when no error occurs } })
When we remove the dest property multer puts the file information on the request, just like our auth middleware puts the user object on the request.
router.post('/users/me/avatar', auth, upload.single('avatar'), async (req, res) => { req.user.avatar = req.file.buffer await req.user.save() res.send() }, (error, req, res, next) => { res.status(400).send({ error: error.message }) })
Test with Postman and check that the image is in the database using Compass.
You can render the image using the following HTML tag
<img src=”data:image/jpg;base64,<<binary image data>>”>
// Create a router to delete the user avatar
router.delete('/users/me/avatar', auth, async (req, res) => { req.user.avatar = undefined await req.user.save() res.send() })
128. Serving up Files
We’ll create a route to fetch an avatar.
router.get('/users/:id/avatar', async (req, res) => { try { const user = await User.findById(req.params.id) if (!user || !user.avatar) { throw new Error() } res.set('Content-Type': 'image/png') res.send(user.avatar) } catch () { res.status(404).send() } })
We can now access the image in a web page using the server endpoint, replacing <<user id>> with an actual user’s id.
<img src="http://localhost:3000/users/<<user id>>/avatar">
129. Auto-Cropping and Image Formatting
Since there is an endpoint specifically for retrieving avatar images, we will not send the images when we send a user object back to the client like we did when we removed the password and tokens properties. To get this done we modify the toJSON method in the user model.
userSchema.methods.toJSON = function() { const user = this const userObject = user.toObject() delete userObject.password delete userObject.tokens delete userObject.avatar return userObject }
Test with Postman
We’re now going to install the sharp module to resize the image and convert all images to pngs.
npm i sharp
In the user router, load the sharp.
const sharp = require(‘sharp’)
In the POST method we’ll use sharp to convert and resize the image.
router.post('/users/me/avatar', auth, upload.single('avatar'), async (req, res) => { const buffer = await sharp(req.file.buffer) .resize({width: 250, height: 250}) .png() .toBuffer() req.user.avatar = buffer await req.user.save() res.send() }, (error, req, res, next) => { res.status(400).send({ error: error.message }) })
© 2022 – 2024, Eric McGregor. All rights reserved. Unauthorized use, distribution and/or duplication of this material without express and written permission from the author is strictly prohibited.