Image Files

Many applications require:

  1. uploading of files to the server-side
  2. downloading of files from the server-side

In these notes, we shall develop code to upload and download image files. Once we understand this code, dealing with other file types is straight forward.

1. Uploading files

In order to upload a file, we need to write code on both the client-side and the server-side.

client-side

On the client-side, include an input element of type file inside a component's render code. The onChange event is dealt with differently to other input types, as we are selecting a file rather than typing an input. Therefore, we write a specific event handler for the file onChange event. The file handler event is called handleFileChange in the code below.

We do not need a name or a value attribute for input elements of type file. These will be assigned later when we append the file information to the formData.

<input             
    type = "file"                    
    onChange = {this.handleFileChange}
/>

 

When the user selects a file to upload, the handleFileChange method is called. The file that the user selected (i.e. e.target.files[0]) is placed in the state variable this.state.selectedFile.


handleFileChange = (e) => 
{
    this.setState({selectedFile: e.target.files[0]})
}

 

The state variable this.state.selectedFile is initialised to null in the component's constructor method. Later on, we can use the null condition to test if a user has uploaded a file.

constructor(props)
{
   ...
        
    this.state = {
        name:"",
        email:"",
        password:"",
        confirmPassword:"", 
        selectedFile:null, 
        isRegistered:false         
    }

   ...
}

 

When the user submits the form, this.state.selectedFile is appended to a FormData object. This object is posted to the server-side using an axios() method. The content-type of the axios() post must be set to "multipart/form-data".

handleSubmit = (e) => 
{
    e.preventDefault()

    let formData = new FormData()  
    formData.append("profilePhoto", this.state.selectedFile)

    axios.post(`${SERVER_HOST}/users/register/${this.state.name}/${this.state.email}/${this.state.password}`, formData, {headers: {"Content-type": "multipart/form-data"}})
    .then(res =>     

    ...

 

server-side

We must create a server-side folder to hold any uploaded files. In the "Cars" Worked Example, we shall create a folder called server/uploads. We store the folder name as a constant (called UPLOADED_FILES_FOLDER) in the server/config/.env file, as shown below.

# Uploaded images folder, which holds any files that are uploaded by the user, such as their profile photo
UPLOADED_FILES_FOLDER = ./uploads

 

After uploading a file, we need to store its filename in the database. We add the filename to the schema of the collection where we shall store it. In the code below, we add the filename to the user's schema.

const mongoose = require(`mongoose`)

let usersSchema = new mongoose.Schema(
   {
        name: {type: String, required:true},
        email: {type: String, required:true},
        password: {type: String,required:true},        
        accessLevel: {type: Number, default:parseInt(process.env.ACCESS_LEVEL_NORMAL_USER)},
        profilePhotoFilename: {type:String, default:""}
   },
   {
       collection: `users`
   })

module.exports = mongoose.model(`users`, usersSchema)

 

We use the multer package to upload files to the server-side. We set the destination folder on the server-side to be the uploads folder that we defined in process.env.UPLOADED_FILES_FOLDER.

const multer  = require('multer')


const upload = multer({dest: `${process.env.UPLOADED_FILES_FOLDER}`})

Install the multer package.

Using multer, we can upload a single file or multiple files to the server-side.

To upload a single file, we call upload.single("profilePhoto"). The parameter "profilePhoto" must match the name of the file input element in the client-side React form.

router.post(`/users/register/:name/:email/:password`, upload.single("profilePhoto"), (req,res) => 
{

    ...

 

Information about the uploaded file is stored in req.file.

If req.file is empty, it means that the file was not uploaded.

We can detect if the the req.file.mimetype matches png or jpeg.

If a file is successfully uploaded, multer will generate a random filename for the uploaded file that is saved on the server-side. The random filename ensures that different users uploading files with the same filename will never cause a conflict. The randomly generated filename is stored in req.file.filename. The randomly generated filename should be stored in the database's users document along with the other user information.

router.post(`/users/register/:name/:email/:password`, upload.single("profilePhoto"), (req,res) => 
{
    if(!req.file)
    {
        res.json({errorMessage:`No file was selected to be uploaded`})
    }
    else if(req.file.mimetype !== "image/png" && req.file.mimetype !== "image/jpg" && req.file.mimetype !== "image/jpeg")
    { 
        fs.unlink(`${process.env.UPLOADED_FILES_FOLDER}/${req.file.filename}`, (error) => {res.json({errorMessage:`Only .png, .jpg and .jpeg format accepted`})})                
    }
    else // uploaded file is valid
    {

        // req.file is the uploaded file
        // req.file.filename is the name of the uploaded file

        ...

If you look at the random filenames in your server/uploads folder, they do not have a filename extension. Why do you think uploaded files are saved without a filename extension?

Whenever a user resets the database's users collection, we shall need to remove any images that have previously been uploaded to the server/uploads folder. We can use the empty-folder package to do this.

const emptyFolder = require('empty-folder')

We pass the server-side folder that we want to delete as a parameter tot he emptyFolder() method. In the code below, we are deleting all files in the process.env.UPLOADED_FILES_FOLDER folder.

The second parameter is set to true if we also want to delete the folder. In the code below, we do not delete the folder, so we set the flag to false.

emptyFolder(process.env.UPLOADED_FILES_FOLDER, false, (result) =>
{
    // the folder was successfully emptied

    ...
})

Install the empty-folder package.

2. Downloading Files

In order to download a file, we need to write code on both the client-side and the server-side.

client-side

An axios() method will be used to fetch the downloaded file data. The downloaded image file data will be returned to the axios() method inside the res.data JSON object. The downloaded file is then stored on the client-side. In the code below, the downloaded data is stored on the client-side in localStorage.profilePhoto.

localStorage.profilePhoto = res.data.profilePhoto

To display the image that is held in localStorage.profilePhoto, use it as the src for a client-side img element. Note that the src format needs to be set to data:;base64.

render(
    {
         localStorage.profilePhoto !== "null" ?                             
             <img id="profilePhoto" src={`data:;base64,${localStorage.profilePhoto}`} alt="Loading photo"/>
         :
             null
    }

    ...

 

server-side

Image files are downloaded to the client-side by including them as part of the JSON object that is returned in res.json().

In order to be able store an image file inside a JSON object, we need to convert the image file to base64. We can read a server-side file and convert it to base64 using the code below.

The base64 data is included in the JSON object and returned in res.json().

fs.readFile(`${process.env.UPLOADED_FILES_FOLDER}/${profilePhotoFilename}`, 'base64', (err, fileData) => 
{

    // fileData will hold the server-side image file contents in base64 format
    // Because it has been read as base64, the fileData can now be added to a JSON object

    res.json({profilePhoto:fileData,  ...  })  

}

Open the jwt_with_pem_file project from the previous section in these notes. Change the code so that the user can upload a profile picture when they are registering as a new user. The profile picture should be visible whenever the user is logged in, as shown below:

"Cars" Worked Example

The full project code for the "Cars" Worked Example that is described below can be downloaded from this link.

In this example, we allow the user to upload a profile photo. The profile photo will be downloaded and displayed inside the DisplayAllCars component.

The default "admin" account for this example does not have a profile photo. If a user does not have a profile photo file then no image will be shown in DisplayAllCars.

Client-Side

client/src/App.js

import React, {Component} from "react"
import {BrowserRouter, Switch, Route} from "react-router-dom"

import "bootstrap/dist/css/bootstrap.css"
import "./css/App.css"

import Register from "./components/Register"
import ResetDatabase from "./components/ResetDatabase"
import Login from "./components/Login"
import Logout from "./components/Logout"
import AddCar from "./components/AddCar"
import EditCar from "./components/EditCar"
import DeleteCar from "./components/DeleteCar"
import DisplayAllCars from "./components/DisplayAllCars"
import LoggedInRoute from "./components/LoggedInRoute"


import {ACCESS_LEVEL_GUEST} from "./config/global_constants"


if (typeof localStorage.accessLevel === "undefined")
{
    localStorage.name = "GUEST"
    localStorage.accessLevel = ACCESS_LEVEL_GUEST
    localStorage.token = null
    localStorage.profilePhoto = null
}

    
export default class App extends Component 
{
    render() 
    {
        return (
            <BrowserRouter>
                <Switch>
                    <Route exact path="/Register" component={Register} />
                    <Route exact path="/ResetDatabase" component={ResetDatabase} />                    
                    <Route exact path="/" component={DisplayAllCars} />
                    <Route exact path="/Login" component={Login} />
                    <LoggedInRoute exact path="/Logout" component={Logout} />
                    <LoggedInRoute exact path="/AddCar" component={AddCar} />
                    <LoggedInRoute exact path="/EditCar/:id" component={EditCar} />
                    <LoggedInRoute exact path="/DeleteCar/:id" component={DeleteCar} />
                    <Route exact path="/DisplayAllCars" component={DisplayAllCars}/> 
                    <Route path="*" component={DisplayAllCars}/>                            
                </Switch>
            </BrowserRouter>
        )
    }
}

If a user has not been defined, then localStorage.profilePhoto is set null. This can be used in the DisplayAllCars component to flag that a profile photo should not be displayed.

if (typeof localStorage.accessLevel === "undefined")
{
    localStorage.name = "GUEST"
    localStorage.accessLevel = ACCESS_LEVEL_GUEST
    localStorage.token = null
    localStorage.profilePhoto = null
}

client/src/components/DisplayAllCars.js

import React, {Component} from "react"
import {Link} from "react-router-dom"

import axios from "axios"

import CarTable from "./CarTable"
import Logout from "./Logout"

import {ACCESS_LEVEL_GUEST, ACCESS_LEVEL_ADMIN, SERVER_HOST} from "../config/global_constants"


export default class DisplayAllCars extends Component 
{
    constructor(props) 
    {
        super(props)
        
        this.state = {
            cars:[]
        }
    }
    
    
    componentDidMount() 
    {
        axios.get(`${SERVER_HOST}/cars`)
        .then(res => 
        {
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else
                {           
                    console.log("Records read")   
                    this.setState({cars: res.data}) 
                }   
            }
            else
            {
                console.log("Record not found")
            }
        })
    }

  
    render() 
    {   
        return (           
            <div className="form-container">
                {
                    localStorage.accessLevel > ACCESS_LEVEL_GUEST 
                    ? <div className="logout">
                        {
                            localStorage.profilePhoto !== "null" 
                            ? <img id="profilePhoto" src={`data:;base64,${localStorage.profilePhoto}`} alt=""/>
                            : null
                        }                        
                        <Logout/>
                      </div>
                    : <div>
                        <Link className="green-button" to={"/Login"}>Login</Link>
                        <Link className="blue-button" to={"/Register"}>Register</Link>  
                        <Link className="red-button" to={"/ResetDatabase"}>Reset Database</Link>  <br/><br/><br/></div>
                }
                
                <div className="table-container">
                    <CarTable cars={this.state.cars} /> 
                        
                    {
                        localStorage.accessLevel >= ACCESS_LEVEL_ADMIN 
                        ? <div className="add-new-car">
                            <Link className="blue-button" to={"/AddCar"}>Add New Car</Link>
                          </div>
                        : null
                    }
                </div>
            </div> 
        )
    }
}

If localStorage.profilePhoto exists, then render it.

    render() 
    {   
        return (           
                        ...

                        {
                            localStorage.profilePhoto !== "null" 
                            ? <img id="profilePhoto" src={`data:;base64,${localStorage.profilePhoto}`} alt=""/>
                            : null
                        }                        
                        
                        ...
        )
    }

client/src/components/Login.js

import React, {Component} from "react"
import {Redirect, Link} from "react-router-dom"
import axios from "axios"

import Button from "../components/Button"
import {SERVER_HOST} from "../config/global_constants"


export default class Login extends Component
{
    constructor(props)
    {
        super(props)
        
        this.state = {
            email:"",
            password:"",
            isLoggedIn:false
        }
    }
        
    
    handleChange = (e) => 
    {
        this.setState({[e.target.name]: e.target.value})
    }
    
    
    handleSubmit = (e) => 
    {
        axios.post(`${SERVER_HOST}/users/login/${this.state.email}/${this.state.password}`)
        .then(res => 
        {                 
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else // user successfully logged in
                { 
                    console.log("User logged in")
                    
                    localStorage.name = res.data.name
                    localStorage.accessLevel = res.data.accessLevel
                    localStorage.profilePhoto = res.data.profilePhoto                        
                    localStorage.token = res.data.token
                    
                    this.setState({isLoggedIn:true})
                }        
            }
            else
            {
                console.log("Login failed")
            }
        })                
    }


    render()
    {            
        return (
            <form className="form-container" noValidate = {true} id = "loginOrRegistrationForm">
                <h2>Login</h2>
                
                {this.state.isLoggedIn ? <Redirect to="/DisplayAllCars"/> : null} 
                
                <input 
                    type = "email" 
                    name = "email" 
                    placeholder = "Email"
                    autoComplete="email"
                    value={this.state.email} 
                    onChange={this.handleChange}
                /><br/>
                    
                <input 
                    type = "password" 
                    name = "password" 
                    placeholder = "Password"
                    autoComplete="password"
                    value={this.state.password} 
                    onChange={this.handleChange}
                /><br/><br/>
                
                <Button value="Login" className="green-button" onClick={this.handleSubmit}/> 
                <Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link>                                      
            </form>
        )
    }
}

Set localStorage.profilePhoto when the user logs in.

    handleSubmit = (e) => 
    {
        axios.post(`${SERVER_HOST}/users/login/${this.state.email}/${this.state.password}`)
        .then(res => 
        {                
                    ...

                    localStorage.profilePhoto = res.data.profilePhoto                        
                    
                    ...

        })                
    }  

client/src/components/Register.js

import React, {Component} from "react"
import {Redirect, Link} from "react-router-dom"
import axios from "axios"

import Button from "../components/Button"

import {SERVER_HOST} from "../config/global_constants"


export default class Register extends Component
{
    constructor(props)
    {
        super(props)
        
        this.state = {
            name:"",
            email:"",
            password:"",
            confirmPassword:"",  
            selectedFile:null,
            isRegistered:false
        } 
    }
    
    
    handleChange = (e) => 
    {
        this.setState({[e.target.name]: e.target.value})
    }
    
    
    handleFileChange = (e) => 
    {
        this.setState({selectedFile: e.target.files[0]})
    }
    
    
    handleSubmit = (e) => 
    {
        e.preventDefault()

        let formData = new FormData()  
        formData.append("profilePhoto", this.state.selectedFile)
                
        axios.post(`${SERVER_HOST}/users/register/${this.state.name}/${this.state.email}/${this.state.password}`, formData, {headers: {"Content-type": "multipart/form-data"}})
        .then(res => 
        {     
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else // user successfully registered
                { 
                    console.log("User registered and logged in")
                    
                    localStorage.name = res.data.name
                    localStorage.accessLevel = res.data.accessLevel
                    localStorage.profilePhoto = res.data.profilePhoto                    
                    localStorage.token = res.data.token
                    
                    this.setState({isRegistered:true})
                }        
            }
            else
            {
                console.log("Registration failed")
            }
        })   
    }


    render() 
    {     
        return (
            <form className="form-container" noValidate = {true} id = "loginOrRegistrationForm">
           
                {this.state.isRegistered ? <Redirect to="/DisplayAllCars"/> : null} 
            
                <h2>New User Registration</h2>
           
                <input  
                    name = "name"              
                    type = "text"
                    placeholder = "Name"
                    autoComplete="name"
                    value = {this.state.name}
                    onChange = {this.handleChange}
                    ref = {(input) => { this.inputToFocus = input }} 
                /><br/>           

	              <input  
                    name = "email"              
                    type = "email"
                    placeholder = "Email"
                    autoComplete="email"
                    value = {this.state.email}
                    onChange = {this.handleChange}
                /><br/>              

	              <input  
                    name = "password"           
                    type = "password"
                    placeholder = "Password"
                    autoComplete="password"
                    title = "Password must be at least ten-digits long and contains at least one lowercase letter, one uppercase letter, one digit and one of the following characters (£!#€$%^&*)"
                    value = {this.state.password}
                    onChange = {this.handleChange}
                /><br/>           

                <input          
                    name = "confirmPassword"    
                    type = "password"
                    placeholder = "Confirm password"
                    autoComplete="confirmPassword"
                    value = {this.state.confirmPassword}
                    onChange = {this.handleChange}
                /><br/>
                
                <input            
                    type = "file"                    
                    onChange = {this.handleFileChange}
                /><br/><br/>
                
                <Button value="Register New User" className="green-button" onClick={this.handleSubmit} />
                <Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link>   
            </form>
        )
    }
}

this.state.selectedFile is used to hold the file that is selected to be uploaded by the user. When the user selects a file to upload, the handleFileChange() will be called. This method sets the value of this.state.selectedFile

export default class Register extends Component
{
    constructor(props)
    {
        super(props)
        
        this.state = {
            name:"",
            email:"",
            password:"",
            confirmPassword:"",  
            selectedFile:null,
            isRegistered:false
        } 
    }
    
    
    handleChange = (e) => 
    {
        this.setState({[e.target.name]: e.target.value})
    }
    
    
    handleFileChange = (e) => 
    {
        this.setState({selectedFile: e.target.files[0]})
    }
  

When the form is submitted, a FormData object is created and this.state.selectedFile is appended to this FormData object. The object (formData) is included as a parameter in the axios() method.

Because we are including a file in our upload, we need to add the header parameter {headers: {"Content-type": "multipart/form-data"}} to the axios() method.

If the new user is successfully registered, then we log the user in. We need to set the user's profile photo (i.e. localStorage.profilePhoto = res.data.profilePhoto) as part of the login.

"profilePhoto" must match the server-side route's upload.single("profilePhoto") parameter name.

    handleSubmit = (e) => 
    {
        e.preventDefault()

        let formData = new FormData()  
        formData.append("profilePhoto", this.state.selectedFile)
                
        axios.post(`${SERVER_HOST}/users/register/${this.state.name}/${this.state.email}/${this.state.password}`, formData, {headers: {"Content-type": "multipart/form-data"}})
        .then(res => 
        {     
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else // user successfully registered
                { 
                    console.log("User registered and logged in")
                    
                    localStorage.name = res.data.name
                    localStorage.accessLevel = res.data.accessLevel
                    localStorage.profilePhoto = res.data.profilePhoto
                    
                    localStorage.token = res.data.token
                    
                    this.setState({isRegistered:true})
                }        
            }
            else
            {
                console.log("Registration failed")
            }
        })   
    }

A file upload input is added to the render() method, so that the user is provided with an interface for uploading their profile photo file.

    render()
    {

        ...

                <input            
                    type = "file"                    
                    onChange = {this.handleFileChange}
                />
  
        ...

Server-Side

server/uploads

We need to create an uploaded files folder, which will hold any files that are uploaded by the user, such as their profile photo.

This folder name is held in the server/config/.env file and is accessed via the environment variable process.env.UPLOADED_FILES_FOLDER

server/config/.env

# This file holds global constants that are visible on the Server-side

# Database
DB_NAME = D01234567
DB_HOST = localhost
DB_USER = root
DB_PASS = yourDBpassword


# Access Levels
ACCESS_LEVEL_GUEST = 0
ACCESS_LEVEL_NORMAL_USER = 1
ACCESS_LEVEL_ADMIN = 2


# Keys
JWT_PRIVATE_KEY_FILE = ./config/jwt_private_key.pem
JWT_EXPIRY = "7d" # Salt length of encryption of user passwords # The salt length should be 16 or higher for commercially released code # It has been set to 3 here, so that the password will be generated faster PASSWORD_HASH_SALT_ROUNDS = 3 # Uploaded images folder, which holds any files that are uploaded by the user, such as their profile photo UPLOADED_FILES_FOLDER = ./uploads # Port SERVER_PORT = 4000 # Local Host LOCAL_HOST = http://localhost:3000

Include the path of the uploaded files folder. This is the new folder that we created to hold any files that are uploaded by the user, such as their profile photo.

server/config/models/users.js

const mongoose = require(`mongoose`)

let usersSchema = new mongoose.Schema(
   {
        name: {type: String, required:true},
        email: {type: String, required:true},
        password: {type: String,required:true},        
        accessLevel: {type: Number, default:parseInt(process.env.ACCESS_LEVEL_NORMAL_USER)},
        profilePhotoFilename: {type:String, default:""}
   },
   {
       collection: `users`
   })

module.exports = mongoose.model(`users`, usersSchema)

Store the filename of the server/uploads folder file that contains the user's profile photo.

server/config/routes/users.js

const router = require(`express`).Router()

const usersModel = require(`../models/users`)

const bcrypt = require('bcrypt')  // needed for password encryption

const jwt = require('jsonwebtoken')
const fs = require('fs')
const JWT_PRIVATE_KEY = fs.readFileSync(process.env.JWT_PRIVATE_KEY_FILE, 'utf8')

const multer  = require('multer')
const upload = multer({dest: `${process.env.UPLOADED_FILES_FOLDER}`})

const emptyFolder = require('empty-folder')



// IMPORTANT
// Obviously, in a production release, you should never have the code below, as it allows a user to delete a database collection
// The code below is for development testing purposes only 
router.post(`/users/reset_user_collection`, (req,res) => 
{
    usersModel.deleteMany({}, (error, data) => 
    {
        if(data)
        {
            const adminPassword = `123!"£qweQWE`
            bcrypt.hash(adminPassword, parseInt(process.env.PASSWORD_HASH_SALT_ROUNDS), (err, hash) =>  
            {
                usersModel.create({name:"Administrator",email:"admin@admin.com",password:hash,accessLevel:parseInt(process.env.ACCESS_LEVEL_ADMIN)}, (createError, createData) => 
                {
                    if(createData)
                    {
                        emptyFolder(process.env.UPLOADED_FILES_FOLDER, false, (result) =>
                        {
                            res.json(createData)
                        })
                    }
                    else
                    {
                        res.json({errorMessage:`Failed to create Admin user for testing purposes`})
                    }
                })
            })
        }
        else
        {
            res.json({errorMessage:`User is not logged in`})
        }
    })                
})


router.post(`/users/register/:name/:email/:password`, upload.single("profilePhoto"), (req,res) => 
{
    if(!req.file)
    {
        res.json({errorMessage:`No file was selected to be uploaded`})
    }
    else if(req.file.mimetype !== "image/png" && req.file.mimetype !== "image/jpg" && req.file.mimetype !== "image/jpeg")
    {
        fs.unlink(`${process.env.UPLOADED_FILES_FOLDER}/${req.file.filename}`, (error) => {res.json({errorMessage:`Only .png, .jpg and .jpeg format accepted`})})                
    }
    else // uploaded file is valid
    { 
        // If a user with this email does not already exist, then create new user
        usersModel.findOne({email:req.params.email}, (uniqueError, uniqueData) => 
        {
            if(uniqueData)
            {
                res.json({errorMessage:`User already exists`})
            }
            else
            {
                bcrypt.hash(req.params.password, parseInt(process.env.PASSWORD_HASH_SALT_ROUNDS), (err, hash) =>  
                {
                    usersModel.create({name:req.params.name, email:req.params.email, password:hash, profilePhotoFilename:req.file.filename}, (error, data) => 
                    {
                        if(data)
                        {
                            const token = jwt.sign({email: data.email, accessLevel:data.accessLevel}, JWT_PRIVATE_KEY, {algorithm: 'HS256', expiresIn:process.env.JWT_EXPIRY})     
                            
                            fs.readFile(`${process.env.UPLOADED_FILES_FOLDER}/${req.file.filename}`, 'base64', (err, fileData) => 
                            {
                                res.json({name: data.name, accessLevel:data.accessLevel, profilePhoto:fileData, token:token})
                            })
                        }
                        else
                        {
                            res.json({errorMessage:`User was not registered`})
                        }
                    }) 
                })
            }
        })         
    }
})


router.post(`/users/login/:email/:password`, (req,res) => 
{
    usersModel.findOne({email:req.params.email}, (error, data) => 
    {
        if(data)
        {
            bcrypt.compare(req.params.password, data.password, (err, result) =>
            {
                if(result)
                {                    
                    const token = jwt.sign({email: data.email, accessLevel:data.accessLevel}, JWT_PRIVATE_KEY, {algorithm: 'HS256', expiresIn:process.env.JWT_EXPIRY})     

                    fs.readFile(`${process.env.UPLOADED_FILES_FOLDER}/${data.profilePhotoFilename}`, 'base64', (err, fileData) => 
                    {        
                        if(fileData)
                        {  
                            res.json({name: data.name, accessLevel:data.accessLevel, profilePhoto:fileData, token:token})                           
                        }   
                        else
                        {
                            res.json({name: data.name, accessLevel:data.accessLevel, profilePhoto:null, token:token})  
                        }
                    })                                                             
                }
                else
                {
                    res.json({errorMessage:`User is not logged in`})
                }
            })
        }
        else
        {
            console.log("not found in db")
            res.json({errorMessage:`User is not logged in`})
        } 
    })
})


router.post(`/users/logout`, (req,res) => {       
    res.json({})
})


module.exports = router

We shall use the multer package to upload the profile photo file.

The uploaded file will be stored in the the uploads folder (ie ${process.env.UPLOADED_FILES_FOLDER})

const multer  = require('multer')
const upload = multer({dest: `${process.env.UPLOADED_FILES_FOLDER}`})

Whenever we reset the users collection, we shall empty out the uploads folder. We shall use the empty-folder package to do this.


const emptyFolder = require('empty-folder')

The upload.single("profilePhoto") method is part of the multer package. It allows us to upload one file. The uploaded file will be stored in the uploads folder (ie ${process.env.UPLOADED_FILES_FOLDER}).

The upload.single() parameter "profilePhoto" must match the first parameter of the client-side Register component's formData.append("profilePhoto", ... ) method.

If the file is successfully uploaded, then req.file will contain information about the uploaded file.

By checking that req.file is not empty, the first of the two if statements checks that file was successfully uploaded

By checking req.file.mimetype, the second of the two if statements checks that the file is an image (png, jpg or jpeg). If it is not an image, then fs.unlink() is called to deleted the uploaded file from the server.

The name of the uploaded file on the server-side is held in req.file.filename. This filename is stored with the rest of the user's details in the mongoDB document.

router.post(`/users/register/:name/:email/:password`, upload.single("profilePhoto"), (req,res) => 
{
    if(!req.file)
    {
        res.json({errorMessage:`No file was selected to be uploaded`})
    }
    else if(req.file.mimetype !== "image/png" && req.file.mimetype !== "image/jpg" && req.file.mimetype !== "image/jpeg")
    {
        fs.unlink(`${process.env.UPLOADED_FILES_FOLDER}/${req.file.filename}`, (error) => {res.json({errorMessage:`Only .png, .jpg and .jpeg format accepted`})})                
    }
    else // uploaded file is valid
    { 
        // If a user with this email does not already exist, then create new user
        usersModel.findOne({email:req.params.email}, (uniqueError, uniqueData) => 
        {
            if(uniqueData)
            {
                res.json({errorMessage:`User already exists`})
            }
            else
            {
                bcrypt.hash(req.params.password, parseInt(process.env.PASSWORD_HASH_SALT_ROUNDS), (err, hash) =>  
                {
                    usersModel.create({name:req.params.name, email:req.params.email, password:hash, profilePhotoFilename:req.file.filename}, (error, data) => 
                    {                        

                        ...

The randomly allocated filename of the user's profile photo file that has been uploaded and saved in the server-side uploads folder is held in req.file.filename.

The user's profile photo needs to be made available on the client-side. Along with their name and accessLevel, the user's profile photo will be returned to the calling client-side axios() method. The name, accessLevel and profilePhoto are returned as a JSON object in the res.json() method. The profile photo file must be converted to base64, so that it can be stored in the res.json() JSON object.

The method fs.readFile() is used to read the data in the profile photo file `${process.env.UPLOADED_FILES_FOLDER}/${req.file.filename}`. The file is then converted into base64 and saved as fileData, which can then be included in the res.json() JSON object that will be returned to the client-side axios() method that called this route.

router.post(`/users/register/:name/:email/:password`, upload.single("profilePhoto"), (req,res) => 
{
                            ...

                            fs.readFile(`${process.env.UPLOADED_FILES_FOLDER}/${req.file.filename}`, 'base64', (err, fileData) => 
                            {
                                res.json({name: data.name, accessLevel:data.accessLevel, profilePhoto:fileData, token:token})
                            })

                            ...  

The empty-folder package that we included in this file has an emptyFolder() function. We use the emptyFolder() function to empty the uploads folder when we reset the user collection.

router.post(`/users/reset_user_collection`, (req,res) => 
{
                        ...

                        emptyFolder(process.env.UPLOADED_FILES_FOLDER, false, (result) =>
                        {
                            res.json(createData)
                        })
                    
                        ...

Amend the above code so that the uploaded file size is <= 100000 bytes.

In the above example, the profile photo is displayed with a width and height of 50px. Amend the above code so that the uploaded image is resized on the server-side.

Instead of resizing the image, we can resize the image data that we send back as the response to the axios call. Write code to resize response.data.profilePhoto before it is returned to the calling axios method.

Amend the above code so that the user does not have to include a profile photo.

Write code to allow the user to add a photo of each car. The user should be able to change or delete the car photo.

 
<div align="center"><a href="../versionC/index.html" title="DKIT Lecture notes homepage for Derek O&#39; Reilly, Dundalk Institute of Technology (DKIT), Dundalk, County Louth, Ireland. Copyright Derek O&#39; Reilly, DKIT." target="_parent" style='font-size:0;color:white;background-color:white'>&nbsp;</a></div>