# Backend

The backend consists of a simple Node + Express application with additional plugins like Nodemon (facilitates the development process while the app is running in background) and Multer, the most used middleware for adding file management features as it is not necessary to use the File System, maintaining the software separated from the hardware, and allowing different configurations (single/multiple file upload, filters, error handling, etc). The backend is deployed and has a index.html file with a form to test the functionality directly from the server side.

Backend Home

Backend running on http://localhost:3000 | Image selection


Backend Sample Image Preview

Image preview and upload


Images are stored in the ./public/img folder and the application is ready for adding a database like MongoDB, MongoDB Cloud or Firebase and store base64 encoded images on it.

Backend Sample Image Upload

Backend image serving


# Middlewares and Frameworks

# Express

Express is a web application framework for NodeJS. It is used for designing and building web applications quickly and easily.

Installation

npm i express
1

Usage

const express = require('express');
const app = express();
1
2

The express.static() function allows to serve files from the ./public folder without installing any other plugin

app.use(express.static('public'));
1

# Cors

Facilitates working locally with Google Chrome browser allowing access to restricted resources from all domains.

Installation

npm i cors
1

Usage

const cors = require('cors');
1

Allow all origins on HTTP request headers

app.use(cors({ origin: '*' }));
1

# Multer

As said before, Multer middleware allows users to upload and manage single/multiple files and includes different features for error handling and filtering.

Installation

npm i multer
1

Usage

const multer = require('multer');
1

Set destination folder for uploaded files and change file name by adding a timestamp to it in order to prevent collisions

const storage = multer.diskStorage({
    destination: (req, file, cb) => {
        cb(null, 'public/img');
    },
    filename: (req, file, cb) => {
        const ext = file.mimetype.split('/')[1];
        cb(null, `pic-${Date.now()}.${ext}`);
    }
});
1
2
3
4
5
6
7
8
9

Use multerFilter to check the mimetype of the uploaded files and allow only images (PNG/JPG/JPEG)

const multerFilter = (req, file, cb) => {
    if (file.mimetype == 'image/jpeg' || file.mimetype == 'image/jpeg' || file.mimetype == 'image/png') {
        cb(null, true);
    } else {
        const error = new Error('The file you are trying to upload is not an image. Only .png/.jpg/.jpeg extensions are allowed.', 400);
        error.code = 'ERROR_FILETYPE';
        return cb(error, false);
    }
};
1
2
3
4
5
6
7
8
9

Include the previous data in a variable to be processed in HTTP requests

const upload = multer({
    storage: storage,
    fileFilter: multerFilter,
    limits: {
        fileSize: 2097152 // Allow images up to 2MB size
    }
});
1
2
3
4
5
6
7

The use of Cookie Parser is recommended to add token-based request verification for each HTTP request.

Installation

npm i cookie-parser csurf
1

Usage

const csrf = require('csurf');
const cookieParser = require('cookie-parser');
1
2

Allow cookies for CSURF with the {cookie: true} option. The order of initialization is important

app.use(cookieParser());
var csrfProtection = csrf({ cookie: true });
1
2

# NodeJS vs NginX vs Others for upload static content

From what I have been able to verify, NginX is the best web server for static content.

# NodeJS

  • Versatile web server
  • Can be integrated with almost all environments and JavaScript frameworks without investing too much time
  • Is more user friendly, and there are a lot of middlewares to solve any problems with the setup
  • The memory usage of a basic NodeJS server is high, but it performs very well for scalable applications

# NginX

  • Is one of the lightests web servers currently (if not the lightest)
  • A basic static server can be deployed in few minutes (similar to NodeJS)
  • Integrations with other coding environments like Python or PHP can be a bit tricky
  • NginX is a C-based web server engine strong focused on using as less memory as possible, so it is perfect for serving static files

# Others

Other possible solutions are Apache (1.x/2.x), LiteSpeed or Lighttpd.

  • Apache servers were the standard and the most extended servers for web pages few years ago, and the community has developed lots of plugins for different third-party integrations, but for static content it is not the best or even a good option over NodeJS or NginX. Years ago I used Apache 2.1 to deploy a server with a Raspberry Pi, and the initial setup was surprisingly easy, but it gave me a lot of trouble every time I changed a part of the code or wanted to add something that was not predefined
  • Both LiteSpeed and Lighttpd are very good options, but after doing some research, I found out that it is better to use NginX than those ones, because despite of being similar in memory usage, NginX is more flexible as it has a very good community support and plugins to fit with different frontends

# Security

  • Multer filter functionality has been used to prevent users to upload files that are not images from the server form
  • Multer property to change the image name and add a timestamp to prevent uploading images with the same name
  • No security method has been implemented to prevent the direct access to the API from the server itself because the idea is to have the client and the server working together but the assignment specifies that the server has to be capable of serving images by itself. In other case, the use of JWT/CSURF to authenticate requests also from the server should be the best option

# Frontend

Third assignment was started as soon as a first version of the backend was running. A Vue app was created including some plugins like Vuetify (styling) or Vuex (notification system) to it. The frontend application consists of one component for the notifications and two components for the image upload system. The first of those is a Home screen with a Upload an image button that deploys the second floating component called Upload.

Frontend Home

Frontend (Home)


The second component is a Dialog box created using the v-card component and adding the Drag&Drop functionality to it. When Drag&Drop was working, a button for uploading files manually was added to the Dialog box.

Frontend Dialog Box

Floating Dialog box to upload images


Added security on the frontend to prevent users to upload files that are not images from the button (used the input accept property). The application supports uploading multiple files at the same time.

Frontend Sample Image Preview

Image name and size preview


In order to display the image, all files are passed to the parent component using the emit function.

Also the application has a notification system that alerts the user of the different errors, warnings or when files have been uploaded successfully.

Frontend Sample Image Uploaded

Image serving on Home component


# Connection and data validation

Axios and VueRouter have been used to connect the frontend with the server.

# Backend POST

app.post('/uploadmultiple', upload.array('picture'), (req, res, next) => {
    const imgArray = req.files;
    if (!imgArray) {
        const error = new Error('Invalid files.');
        error.httpStatusCode = 400;
        return next(error);
    }
    res.status(201).json({ files: imgArray });
});
1
2
3
4
5
6
7
8
9

# Frontend Routing

Installation

npm i axios vue-router
1

Axios Usage

const axios = require('axios');
axios.defaults.baseURL = 'http://localhost:3000/';
1
2

To use VueRouter, it is recommended to create a separated folder, in this case is ./src/router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter);

const routes = [{
    path: '/',
    name: 'Home',
    component: () =>
        import ('@/components/Home.vue')
}]

const router = new VueRouter({
    mode: 'history',
    base: process.env.BASE_URL,
    routes
});

export default router;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

HTTP Requests with Axios example

axios.post("/uploadimagecli", formData)
    .then((res) => {
        this.sendFileToParent(res.data.file.filename);
        this.$store.dispatch("addNotification", {
            message: "Files uploaded successfully",
            color: "success",
        });
    }).catch((err) => {
        console.log("ERR: " + err.response.data.error);
        this.$store.dispatch("addNotification", {
            message: err.response.data.error,
            color: "error",
        });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Flow Diagram

Image Uploader App Flow Diagram

Flow diagram Backend & Frontend


# Image Resize

In order to resize the uploaded images, there are two ways:

# Image resizing on client side

Resizing images on client side is the common way to do it because this method does not affect the original image dimensions, but it can be difficult depending on the framework and elements containing the images.

If the images are shown within a Vue <v-img> or HTML <img> component, there is no problem fitting the image to the needed dimensions

<v-img src="src_url" max-height="max_height_in_px" max-width="max_width_in_px" contain></v-img>
1

The contain property prevents the component to crop the image if the dimensions are higher than the specified ones


Using elements like the Vuetify <v-carousel>, the images will not be completely responsive. If images of different dimensions are uploaded, the carousel will be as large as the larger of the uploaded images, if the max-width/max-height properties allow it.

# Image resizing on server side

Due to the problems with some client-side elements for showing images of different sizes in the proper way, there is a Node module that can work with Multer to store the images with custom dimensions.

The module is multer-sharp-resizer (opens new window)

# Image crop

There are different Vue plugins to modify the uploaded files before storing them in the server-side. This plugins allow to crop, rotate and resize the image while previewing it and have different properties to fix the maximum size or aspect ratio of an uploaded image.

# Image crop tools

# Using vue-advanced-cropper

Vue Advanced Cropper is the chosen tool to apply this functionality to the application.

New ImageCropper component has been created and the functionality to upload the images has been passed from the Upload component to this new component. The image is uploaded and sent to the parent component (Home) using a FileReader instead of using the FileSystem or a Vue Loader plugin in order to avoid adding too much charge to the application.

The cropper use is simple but has a lot of properties to set it up in the way that it is needed. Here is an example of the cropper required to accomplish the mentioned task

<cropper
    class="cropper-background"
    :src="img"
    ref="cropper"
    @change="change"
    :stencil-props="{
        handlers: {},
        movable: false,
        scalable: false,
    }"
    :stencil-size="{
        width: 600,
        height: 400,
    }"
    image-restriction="stencil"
/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Vue Advanced Cropper has also another modules included like for example the preview component, that allows to preview the image before cropping it.

<preview
    style="margin-top: 10px"
    :width="150"
    :height="100"
    :image="result.image"
    :coordinates="result.coordinates"
/>
1
2
3
4
5
6
7
export default {
data() {
    return {
      result: {
        coordinates: null,
        image: null,
      },
      coordinates: {
        width: 0,
        height: 0,
        left: 0,
        top: 0,
      },
      // ...
    };
  },
  methods: {
    change({ coordinates, image }) {
      this.result = {
        coordinates,
        image,
      };
    }, 
    // ...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

The image received by the ImageCropper is base64 encoded, so there is a difference when sending the image to the server-side, having to change the img object and convert it into a blob object. Here is the submit function

const { canvas } = this.$refs.cropper.getResult(); // Get result from the cropper plugin

if (canvas) {
const form = new FormData();
// Create a new blob object
canvas.toBlob(
    (blob) => {
    console.log("blob: ", blob);
    form.append("picture", blob);
    axios
        .create({ withCredentials: true })
        .post("/uploadmultiple", form, {
        headers: { crossdomain: true, "Content-Type": "undefined" },
        })
        .then((res) => {
        this.sendFilesToParent(res.data.files);
        this.$store.dispatch("addNotification", {
            message: this.$t("alert.filesUploaded"),
            color: "success",
        });
        })
        .catch((err) => {
        console.log("ERR: " + err.response.data.error);
        this.$store.dispatch("addNotification", {
            message: err.response.data.error,
            color: "error",
        });
        });
    } /*Second parameter can be used here to specify file forma e.g. "image/jpeg". Default: png */
);
}
this.closeDialog();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

The files are stored in the server-side once cropped and then passed to the client-side using the $emit function.

Vue Advanced Cropper

When the image is uploaded, a cropper Dialog Box is displayed for the user to crop the image and then send it to the server

# Storing images with fixed dimensions

In order to store the images with a fixed dimensions, there are different ways to do it, but it generally depends on the requirements of the application. For this purpose, there are no file restriction when uploading, which means that users can upload images with smaller dimensions than the minimum allowed. In this case it is necessary to fill the area with blank space until the minimum height or/and width allowed.

<cropper
...
    :min-width="600"
    :min-height="400"
    :max-width="600"
    :max-height="400"
...
/>
1
2
3
4
5
6
7
8

The code above makes the stencil sized that the saved images will have dimensions of 600x400 pixels

This also add the need of moving the stencil around the blank space to center the image.

<cropper
...
    imageRestriction="none"
...
/>
1
2
3
4
5

This property add blank space around the image to fill the minimum dimensions set previously (600x400px)

# Advanced recipes

There are a lot of different options to customize the cropper and make it fit with all application requirements. It is recommended to visit the website and read the documentation, especially the Concepts section, which explains the core concepts of image cropping in a general way and the properties that this module offers to adjust or modify the default behavior.

# Future work

  • The use of Vue Advanced Cropper makes the application perform slower when showing and modifying the preview and the cropper component
  • Even with fixed size of images, the quality of the cropped images is very low. This could be fixed choosing a higher image dimensions, but this also could be a problem for smaller images. The image dimensions chosen have to be in balance so that bigger images do not lost much quality and smaller images do not seem to be very small inside the complete frame.