Appendix E. Images in Databases

Table of Contents
E.1. Images in MongoDB from Node.js
E.1.1. The HTML5 Form for Upload
E.1.2. The Model
E.1.3. The Controllers
E.1.4. The Server Side Producing the View

E.1. Images in MongoDB from Node.js

E.1.1. The HTML5 Form for Upload

The HTML5 form must be slightly augmented to allow for sendeing an image from the user's local computer to the server for storing.

Example E.1. The View with the Form, views/register.pug
extends layout

block content
    main.otherpage
        aside
        section
            h2 Please Enter Your Details to #{title}
            include messages.pug
            form(method='post' action='/users/register' enctype='multipart/form-data')
                p
                    label Name
                    br
                    input(type='text' minlength='2' name='name' required)
                p
                    label LoginID
                    br
                    input(type='text' minlength='2' name='uid' required)
                p
                    label Email
                    br
                    input(type='email' minlength='6' name='email' required)
                p
                    label Password
                    br
                    input(type='password' minlength='32' name='password' required)
                p
                    label Repeat Password
                    br
                    input(type='password' minlength='32' name='passwordr' required)
                p
                    label Avatar
                    br
                    input(type='file' name='avatar' placeholder='optional')
                p
                    label
                    input(type='submit', value='Send')
        aside

In code line 16, an enctype, enclosure type, is added. This is necessary to signify that the amount of data to transmit when an image is included cannot be transmitted in one package. Several, if not many, are required.

In line 34 you see the input type file. This provides a browse button in the form through which the user can navigate her file system for the wanted image file.

E.1.2. The Model

To contain an image the User model must contain a field with the proper type. Lines 26 through 29 in the following schema caters for that. The Buffer type will hold the image itself while the field named contentType will hold the mimetype of the image, eg image/png or image/jpeg. The mimetype is necessary for the browser to be able to render the image. All regular image files contain a mimetype for that purpose.

Example E.2. The User Model, models/User.js
const mongoose = require('mongoose');

const UserSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true
  },
  uid: {
    type: String,
    required: true,
    unique: true
  },
  email: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true
  },
  date: {
    type: Date,
    created: Date.now
  },
  avatar: {                             // for image
    data: Buffer,                       // the image itself
    contentType: String                 // the mimetype
  }
});

const User = mongoose.model('User', UserSchema, 'user');

module.exports = User;

E.1.3. The Controllers

Example E.3. The Index Router, routes/index.js
const express = require('express');
const index = require("../controllers/indexController.js");
const router = express.Router();
const { ensureAuthenticated, forwardAuthenticated } = require('../config/auth');

/* GET home page. */
router.get('/', forwardAuthenticated, index.frontpage);
/* dashnord for insiders  */
router.get('/dashboard', ensureAuthenticated, index.dashboard);

module.exports = router;

The index router above contains the route /dashboard with the middleware ensureauthenticated which in this case will allow the dashboard view to be displayed only users that are logged in. This bit has nothing to do with the technology required to show the image. It is purely for the example application.

Example E.4. The Users Router, routes/users.js
const express = require('express');
const router = express.Router();
const auth = require("../controllers/authController.js");
const { ensureAuthenticated, forwardAuthenticated } = require('../config/auth');

/* registration form  */
router.get('/register', forwardAuthenticated, auth.register);
/* receive registration data  */
router.post('/register', auth.postRegister);

/* login form  */
router.get('/login', forwardAuthenticated, auth.login);
/* handle login */
router.post('/login', auth.postLogin)

/* logout, kills session and redirects to frontpage  */
router.get('/logout', auth.logout);

/*
* This is a REST endpoint that GETs an image from the database
* it is requested from an HTML img tag
* re the pug file dashboard.pug, and the endpoint handler 
* in auth.lookupImage
*/
router.get('/getimage/:userid', ensureAuthenticated, auth.lookupImage);

module.exports = router;

The users router hold two aspects essential for handling images in MongoDB. Line 9 routes to the controller function for storing the user with the image in the database. The route in line 25 ins an endpoint for displaying the image. This materializes a REST API that serves a secondary request induced by the browser meeting an img tag requiring the image.

Example E.5. The Users Controller, controllers/authController.js
const bcrypt = require('bcryptjs');
const formidable = require('formidable');                       // required for image upload
const fs = require('fs');                                       // required for reading temp image file
const mongoose = require('mongoose');
const mongoUtil = require("../models/MongoHandler");
const passport = require('passport');
const User = require('../models/User');

const saltRounds = 10;

exports.register = function (req, res) {
    res.render('register', {
            title: 'Register'
    });
};

exports.postRegister = async function (req, res) {
    let form = new formidable.IncomingForm();
    form.parse(req, async function(err, fields, files) {
      if (err) { console.error(err); }

      let { name, uid, email, password, passwordr } = fields;
      let errors = [];

      if (!name || !uid || !email || !password || !passwordr) {
          errors.push({ msg: 'Please enter all fields' });
      }
      if (password != passwordr) {
          errors.push({ msg: 'Passwords do not match' });
      }
      if (password.length < 32) {
          errors.push({ msg: 'Password must be at least 32 characters' });
      }
      if (errors.length > 0) {            // respond if errors
          res.render('register', {
              errors,
              name,
              uid,
              email,
              password,
              passwordr
          });
      }

      let db = await mongoUtil.mongoConnect();                            // connect
      let user = await User.findOne({ uid: uid });
      if (user) {
          errors.push({ msg: 'users already exists' });
          res.render('register', {                                        // respond if already exists
              errors,
              name,
              uid,
              email,
              password,
              passwordr
          });
      }

      try {
          db = await mongoUtil.mongoConnect();                            // connect
          let hash = await bcrypt.hash(password, saltRounds);             // hash password and create obj
          let newUser = new User({name: name, uid: uid, email: email, password: hash});
          newUser.avatar.data = await fs.readFileSync(files.avatar.path); // add to obj with read uploaded image
          newUser.avatar.contentType = files.avatar.type;                 // get its mimetype
          await newUser.save();
          req.flash('success_msg', 'You are now registered and can log in');
          res.render('login', {title: 'Login'});
      } catch (err) {
          errors.push({ msg: 'an error occurred' });
          res.render('register', {                                        // respond if already exists
              errors,
              name,
              uid,
              email,
              password,
              passwordr
          });
      }
    }) 
};

exports.login = function (req, res) {
    res.render('login', {
        title: 'Login'
    });
};

exports.postLogin = async function (req, res, next) {
    await passport.authenticate('local', {
        successRedirect: '/dashboard',
        failureRedirect: '/users/login',
        failureFlash: true
    })(req, res, next);
};

exports.logout = function (req, res) {
    req.logout();
    res.redirect('/');
};

/*
 *  REST endpoint for serving image
 */
exports.lookupImage = async function (req, res) {
    let query = {uid: req.params.userid};
    const db = await mongoUtil.mongoConnect();
    let user = await User.findOne(query);

    res.contentType(user.avatar.contentType);
    res.send(user.avatar.data);
};

The file above requires a module formidable in line 2. formidable has a more nuanced way of interpreting the form data than we would have without it. It is able to distinguish between regular form fields and those induce by the file input type. This is used in lines 18, and 19.

The fs module is included for reading the uploaded image file which is temporarily stored by the server. This is used in line 63. The user object created in line 62 is then augmented with the image and its mimetype in lines 63 and 64, and being saved as usually. The result is, that apart from the regular text data, the database document for the user now also contains the user's avatar, an image. This may be verified by running mongo from the command line, and displaying the user.

E.1.4. The Server Side Producing the View

When we want to display data about the user, we need to read the data from the database and pass the to the view rendering mechanism. We normally display image data by writing an img element sourcing an image file. We have a problem with the user image. There is no image file because the image is in the document in the database.

Since the src of the img element is a url we make a trick. Instead of the image file we request the image froma REST endpoint. On line 12 below you will see the img element. It results in a request being handled by the REST endpoint in users.js router. That route reads the image data with the lookupImage function in lines 102-109 in authController.js shown above.

Example E.6. The View, views/dashboard.pug
extends layout

block content
    main.otherpage
        aside
        section
            h2 #{title}
            h3 Demo Display of Images from Database
            div.uid
                p= user
                p
                    img(src='/users/getimage/' + user)
        aside

The code for this example is available for cloning from https://bitbucket.org/phidip/passportonewithavatars/src/master/.