Section 14: File Uploads (Task App)

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.