Node JS Image Delivery Microservice Challenge [Docker, Node JS & MongoDB]

 

Continuation from http://justcodesnippets.durlut.ro/index.php/2019/12/08/node-js-image-delivery-microservice-challenge-mongo-db-stats-page/

We have to make the app self reliant.

We install Docker. We create the docker files.

FROM node:12
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

version: '3'
services:
  app:
    container_name: docker-node-mongo
    restart: always
    build: .
    ports:
      - '3000:3000'
    links:
      - mongo
  mongo:
    container_name: mongo
    image: mongo
    ports:
      - '27017:27017'

 

We make the app run on port 3000 to avoid conflicts.

We un-install mongodb and install mongoose client npm install mongoose

We re-write the DB client according to here:

// https://know-thy-code.com/mongoose-schemas-models-typescript/
import { connect, connection, Connection } from "mongoose";
import { IImageModel, Image } from "./image-schema";

declare interface IModels {
    Image: IImageModel;

}

export class DbClient {

    public static get Models() {
        if (!DbClient.instance) {
            DbClient.instance = new DbClient();
        }
        return DbClient.instance.models;
    }

    public static incrementImageValues(imageName: string, incrementTimesServed: boolean, incrementTimesResized: boolean, incrementTimesDirectlyServed: boolean) {
        DbClient.Models.Image.findOneAndUpdate({
            imageName
        }
            , {
                $inc: {
                    timesServed: incrementTimesServed ? 1 : 0
                    , timesResized: incrementTimesResized ? 1 : 0
                    , timesDirectlyServed: incrementTimesDirectlyServed ? 1 : 0
                }
            }, {
            new: true, upsert: true
        });
    }

    // https://mongoosejs.com/docs/promises.html
    public static async getImagesCollectionHtml(): Promise<string> {
        return DbClient.Models.Image.find({}).then(async (images) => {
            // write HTML output
            let output: string = "<html><header><title>Images</title></header><body>";
            output += "<h1>List retrieved from DB</h1>";
            output += '<table border="1">';
            output += `<tr> <td><b>Name</b></td><td><b>Nr of served images</b></td><td><b>Nr of times resized</b></td><td><b>Nr of times directly served</b></td></tr>`;

            // process list
            images.forEach((image) => {
                output += `<tr><td>${image.imageName}</td><td>${image.timesServed}</td><td>${image.timesResized}</td><td>${image.timesDirectlyServed}</td></tr>`;
            });

            // write HTML output (ending)
            output += "</table></body></html>";
            console.log(output);
            return output;
        });
    }

    private static instance: DbClient;

    private db: Connection;
    private models: IModels;

    private constructor() {
        connect("mongodb://mongo:27017/docker-node-mongo", { useNewUrlParser: true });
        this.db = connection;
        this.db.on("open", this.connected);
        this.db.on("error", this.error);

        this.models = {
            Image: new Image().model
            // this is where we initialise all models
        };
    }

    private connected() {
        console.log("Mongoose has connected");
    }

    private error(error: any) {
        console.log("Mongoose has errored", error);
    }

}

 

We run the docker command:  docker-compose up --build

After docker finishes building we call this address: http://localhost:3000/image/eye.jpg/600×300

And then this address: http://localhost:3000/image/cat.jpg/600×300

And then this address: http://localhost:3000/image/dog.jpg/600×300

And then the stats address http://localhost:3000/stats/

 

Node JS Image Delivery Microservice Challenge [Documentation & How to run]

Continuation from: http://justcodesnippets.durlut.ro/index.php/2019/12/08/node-js-image-delivery-microservice-challenge-unit-tests/

Source code can be gotten from here: https://github.com/dragos-durlut/nodejs-image-delivery-microservice/

General installation notes can be found in the README.md

To run the microsevice you will need to execute command npm run start

To see the stats you can go to http://localhost:8080/stats in Postman or in a broswer window. You should see something like this:

In order to get the resized image you call this address in Postman or in Web Browser: http://localhost:8080/image/eye.jpg/300×300

Important note: you must have the image in dist/images/original/ folder or you will get a 404 status.

You will get a 200 response along with the resized image:

You will also notice the creation of a folder that mathches thr requested resolution.

Node JS Image Delivery Microservice Challenge [Unit tests]

Continuation from http://justcodesnippets.durlut.ro/index.php/2019/12/08/node-js-image-delivery-microservice-challenge-mongo-db-stats-page/

Using some inspiration from here: https://buddy.works/guides/how-automate-nodejs-unit-tests-with-mocha-chai we start creating out unit tests:

First we install mocha and chai npm install mocha chai --save-dev

Then we install request npm install request --save-dev

Then we install request npm i @types/mocha

This is not enough to run the test as we need to do a setup in package.json as said here: https://stackoverflow.com/a/50903056/249895

We also have to set the test as below:
"scripts": {
"prebuild": "tslint -c tslint.json -p tsconfig.json --fix",
"build": "tsc",
"prestart": "npm run build",
"start": "node .",
"test": "mocha src/test/*.ts",
"smoketest": "mocha smoketest/**/*.ts"
}

We are having issues running th test so we take another look here: https://42coders.com/testing-typescript-with-mocha-and-chai/

We re-install the packages npm install chai mocha ts-node @types/chai @types/mocha --save-dev

The test line looks like this now:  "test": "mocha -r ts-node/register src/test/*.ts",

We are still having issues with assetions so we install chai-http npm install chai-http

A wild error pops up: Uncaught TypeError: Cannot read property 'statusCode' of undefined

We are still having issues with async calls so we will test something less dififcult: the validator.

We run npm test

We get passing tests.

 

Node JS Image Delivery Microservice Challenge [Mongo DB & Stats page]

Continuation from http://justcodesnippets.durlut.ro/index.php/2019/12/07/node-js-image-delivery-microservice-challenge-folder-structure/

We create a cluster on https://cloud.mongodb.com/ for development purposes.

We install mongodb driver npm install mongodb –save

We create DbClient class that will hold DB operations

import mongodb from "mongodb";

export class DbClient {

    public incrementValue(imageFullName: string, incrementTimesServed: boolean, incrementTimesResized: boolean, incrementTimesDirectlyServed: boolean) {
        const client = this.getClient();
        client.connect((err) => {

            const imagesCollection = this.getImagesCollection(client);
            // https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndUpdate/
            imagesCollection.findOneAndUpdate(
                { imageName: imageFullName }
                , {
                    $inc: {
                        timesServed: incrementTimesServed ? 1 : 0
                        , timesResized: incrementTimesResized ? 1 : 0
                        , timesDirectlyServed: incrementTimesDirectlyServed ? 1 : 0
                    }
                },
                { upsert: true }
            );

            client.close();
        });
    }

    public getImagesCollectionHtml(): Promise<string> {
        // https://medium.com/thecodinghype/https-medium-com-thecodinghype-reading-from-mongodb-database-using-express-js-and-node-js-250ef8b9282a
        // https://stackoverflow.com/questions/35246713/node-js-mongo-find-and-return-data
        const client: mongodb.MongoClient = this.getClient();
        return client.connect().then((connectedClient) => {
            return this.getImagesCollection(connectedClient);
        }).then((imagesCollection) => {
            return imagesCollection.find().toArray();
        }).then(async (images) => {
            // write HTML output
            let output: string = "<html><header><title>Images</title></header><body>";
            output += "<h1>List retrieved from DB</h1>";
            output += '<table border="1">';
            output += `<tr> <td><b>Name</b></td><td><b>Nr of served images</b></td><td><b>Nr of times resized</b></td><td><b>Nr of times directly served</b></td></tr>`;

            // process list
            images.forEach((image) => {
                output += `<tr><td>${image.imageName}</td><td>${image.timesServed}</td><td>${image.timesResized}</td><td>${image.timesDirectlyServed}</td></tr>`;
            });

            // write HTML output (ending)
            output += "</table></body></html>";
            console.log(output);
            await client.close();
            return output;
        });
    }

    private getImageStatsDb(client: mongodb.MongoClient): mongodb.Db {
        return client.db("imageStats");
    }

    private getImagesCollection(client: mongodb.MongoClient): mongodb.Collection<any> {
         return this.getImageStatsDb(client).collection("images");
    }

    private getClient(): mongodb.MongoClient {
        const uri = "mongodb+srv://admin:admin@cluster0-pyydy.mongodb.net/test?retryWrites=true&w=majority";
        const client = new mongodb.MongoClient(uri, { useNewUrlParser: true });
        return client;
    }

}

 

As the images are being served we increment in the database the images values:

In the end, the index.ts file holds 1 more route and the images route also makes calls to the database.

import express from "express";
import { DbClient } from "./db/db-client";
import { RequestImageValidatorService } from "./services/request-image-validator-service";
import { ServedImageService } from "./services/served-image-service";
import { FolderStructureUtils } from "./utils/folder-structure-utils";
const app = express();
const port = 8080; // default port to listen

app.use(express.static("images")); // https://expressjs.com/en/starter/static-files.html

// define a route handler for the default home page
app.get("/", (req, res) => {
    res.send("Hello world!");
});

// define a route handler for the image processing
app.get("/image/:imageName/:imageResolution", async (req, res, next) => {
    // res.send(`image named ${req.params.imageName} and resolution ${req.params.imageResolution}`);

    const imageName = req.params.imageName;
    const imageResolution = req.params.imageResolution;
    console.log(`requested image named ${imageName} and resolution ${imageResolution}`);
    const imageValidatorService: RequestImageValidatorService = new RequestImageValidatorService(imageName, imageResolution);
    if (!imageValidatorService.validateImage()) {
        res.status(404).send({ errors: imageValidatorService.errors });
    }

    const dbClient: DbClient = new DbClient();
    const incrementTimesServed: boolean = true;
    let incrementTimesResized: boolean = false;
    let incrementTimesDirectlyServed: boolean = false;

    const imageExistsPhysically: boolean = await FolderStructureUtils.imageWithResolutionExists(imageName, imageResolution);
    if (imageExistsPhysically) {

        incrementTimesDirectlyServed = true;
        dbClient.incrementValue(imageName, incrementTimesServed, incrementTimesResized, incrementTimesDirectlyServed);

        const imagePath = FolderStructureUtils.getImageWithResolutionPath(imageName, imageResolution);
        res.status(200).sendFile(imagePath);
        return;
    }

    const serverImageService: ServedImageService = new ServedImageService();
    const originalServedImage = await serverImageService.getOriginalServedImage(imageName); // https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016
    if (originalServedImage.existsOnFileSystem) {// if image exists we can proceed to try and serve the resized image

        incrementTimesResized = true;
        dbClient.incrementValue(imageName, incrementTimesServed, incrementTimesResized, incrementTimesDirectlyServed);

        const resizedImage = await serverImageService.getResizedServedImage(originalServedImage, imageResolution);
        res.status(200).sendFile(resizedImage.absolutePath);
    } else {
        res.status(404).send({ error: `File ${imageName} does not exist on file system at ${originalServedImage.absolutePath}` });
    }

});

// define a route handler for the default home page
app.get("/stats", async (req, res) => {
    const dbClient: DbClient = new DbClient();
    const html = await dbClient.getImagesCollectionHtml();
    res.status(200).send(html);
});

// start the Express server
app.listen(port, () => {
    // tslint:disable-next-line:no-console
    console.log(`server started at http://localhost:${port}`);
});

 

 

Node JS Image Delivery Microservice Challenge [Folder structure]

Continuation from here: http://justcodesnippets.durlut.ro/index.php/2019/12/07/node-js-image-delivery-microservice-challenge-refactoring-1/

 

First of all let’s address the issue of the synchronous calls to check for file existence.

const fileExists: boolean = fs.existsSync(fileAbsolutePath);

We need to make the call async. We can get some info from here: https://stackoverflow.com/questions/40593875/using-filesystem-in-node-js-with-async-await and here https://stackoverflow.com/questions/17699599/node-js-check-if-file-exists

We create a FileUtils class for reusability:

import fs from "fs";
import util from "util";

export class FileUtils {
    public static async createDirectory(directoryPath: string): Promise<boolean> {
        try {
            await this.fsMkDir(directoryPath);
            return true;
        } catch (err) {
            console.log(err);
            return false;
        }
    }

    public static async fileExists(fileAbsolutePath: string): Promise<boolean> {
        try {
            const result = await this.fsStat(fileAbsolutePath);
            return result.isFile();
        } catch (err) {
            console.log(err);
            return false;
        }
    }
    public static async directoryExists(fileAbsolutePath: string): Promise<boolean> {
        try {
            const result = await this.fsStat(fileAbsolutePath);
            return result.isDirectory();
        } catch (err) {
            console.log(err);
            return false;
        }
    }
    // https://stackoverflow.com/questions/40593875/using-filesystem-in-node-js-with-async-await
    // https://stackoverflow.com/questions/17699599/node-js-check-if-file-exists
    private static fsStat = util.promisify(fs.stat);
    private static fsMkDir = util.promisify(fs.mkdir);
}

 

We create a FolderStructureUtils class to controll the new folder structure that will unfold. This will help us bring results faster. Each resolution has its own folder.

import fs from "fs";
import path from "path";
import { FileUtils } from "../utils/file-utils";
export class FolderStructureUtils {

    public static getOriginalImagesDirectoryPath(): string {
        return `${this.imagesFolder}original/`;
    }

    public static getOriginalImageAbsolutePath(imageName: string): string {
        const originalImagesDirectoryPath: string = this.getOriginalImagesDirectoryPath();
        return `${originalImagesDirectoryPath}${imageName}`;
    }

    public static getResolutionImageDirectoryPath(imageResolution: string): string {
        return `${this.imagesFolder}${imageResolution}/`;
    }
    public static async resolutionImageDirectoryExists(imageResolution: string): Promise<boolean> {
        const directoryPath = this.getResolutionImageDirectoryPath(imageResolution);
        return await FileUtils.directoryExists(directoryPath);
    }
    public static async imageWithResolutionExists(imageName: string, imageResolution: string): Promise<boolean> {
        const filePath = this.getImageWithResolutionPath(imageName, imageResolution);
        return await FileUtils.fileExists(filePath);
    }

    public static getImageWithResolutionPath(imageName: string, imageResolution: string): string {
        const directoryPath = this.getResolutionImageDirectoryPath(imageResolution);
        const filePath = `${directoryPath}${imageName}`;
        return filePath;
    }

    public static async createImageResolutionDirectory(imageResolution: string): Promise<string> {
        const directoryPath = this.getResolutionImageDirectoryPath(imageResolution);
        const isDirCreated: boolean = await FileUtils.createDirectory(directoryPath);
        if (isDirCreated) {
            return directoryPath;
        } else {
            throw Error(`Directory ${directoryPath} could not be created`);
        }
    }
    private static imagesFolder: string = path.join(__dirname, `/../images/`);

}

 

Right now, index.ts just looks if the file exists at the specified location. If not, the original file is located and a folder and file at certain resolution is created.

import express from "express";
import { RequestImageValidatorService } from "./services/request-image-validator-service";
import { ServedImageService } from "./services/served-image-service";
import { FolderStructureUtils } from "./utils/folder-structure-utils";
const app = express();
const port = 8080; // default port to listen

app.use(express.static("images")); // https://expressjs.com/en/starter/static-files.html

// define a route handler for the default home page
app.get("/", (req, res) => {
    res.send("Hello world!");
});

// define a route handler for the image processing
app.get("/image/:imageName/:imageResolution", async (req, res, next) => {
    // res.send(`image named ${req.params.imageName} and resolution ${req.params.imageResolution}`);
    const imageName = req.params.imageName;
    const imageResolution = req.params.imageResolution;
    console.log(`requested image named ${imageName} and resolution ${imageResolution}`);
    const imageValidatorService: RequestImageValidatorService = new RequestImageValidatorService(imageName, imageResolution);
    if (!imageValidatorService.validateImage()) {
        res.status(404).send({ errors: imageValidatorService.errors });
    }

    const imageExistsPhysically: boolean = await FolderStructureUtils.imageWithResolutionExists(imageName, imageResolution);
    if (imageExistsPhysically) {
        const imagePath = FolderStructureUtils.getImageWithResolutionPath(imageName, imageResolution);
        res.status(200).sendFile(imagePath);
        return;
    }

    const serverImageService: ServedImageService = new ServedImageService();
    const originalServedImage = await serverImageService.getOriginalServedImage(imageName); // https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016
    if (originalServedImage.existsOnFileSystem) {// if image exists we can proceed to try and serve the resized image
        const resizedImage = await serverImageService.getResizedServedImage(originalServedImage, imageResolution);
        res.status(200).sendFile(resizedImage.absolutePath);
    } else {
        res.status(404).send({ error: `File ${imageName} does not exist on file system at ${originalServedImage.absolutePath}` });
    }

});

// start the Express server
app.listen(port, () => {
    // tslint:disable-next-line:no-console
    console.log(`server started at http://localhost:${port}`);
});

 

Node JS Image Delivery Microservice Challenge [Refactoring #1]

Continuation from here: http://justcodesnippets.durlut.ro/index.php/2019/12/06/node-js-image-delivery-microservice-challenge-setup/

First of all we need to arrange the image information. So we create a ServedImage class (and adjacent classes) to store the image information.

export class ServedImage {
    public fullName: string;
    public fileName: string;
    public extension: string;
    public absolutePath: string;
    public directoryAbsolutePath: string;
    public existsOnFileSystem: boolean | null;
    public resolution: ServedImageResolution;
}

export class ResizedServedImage extends ServedImage {
    public originalImage: ServedImage;
}

export class ServedImageResolution {

    public static AreResolutionsEqual(resolutionA: ServedImageResolution, resolutionB: ServedImageResolution): boolean {
        return resolutionA.height === resolutionB.height && resolutionA.width === resolutionB.width;
    }
    public width: number;
    public height: number;

}

 

We need to validate the call received from the client. For this purpose we create the RequestImageValidatorService.

export class RequestImageValidatorService {

    public errors: string[];

    private imageExtensionRegExp: RegExp = RegExp(/\.(gif|jpg|jpeg|tiff|png)$/i); // https://stackoverflow.com/questions/10473185/regex-javascript-image-file-extension
    private imageResolutionRegExp: RegExp = RegExp(/(^([\d ]{2,5})[x]([\d ]{2,5})$)/i); // https://regex101.com/

    constructor(private imageName: string, private imageResolution: string) {
    }

    public validateImage(): boolean {
        let isOk: boolean = true;
        this.errors = new Array<string>();
        if (!this.imageExtensionRegExp.test(this.imageName)) {
            isOk = false;
            this.errors.push(`Image does not have correct extension(gif|jpg|jpeg|tiff|png)`);
        }
        if (!this.imageResolutionRegExp.test(this.imageResolution)) {
            isOk = false;
            this.errors.push(`Image does not have correct resolution format; example 300x400 or 600x1024`);
        }
        return isOk;
    }
}

 

We need a service to retrieve an existing image as a ServedImage object and also retrive a ResizedServedImage. For this we create ServedImageService.

We make the 2 methods getServedImage() and getResizedServedImage() async to enable file processing. We avoid resizing the same image twice by checking for file existence beforehand.

https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016

import fs from "fs";
import path from "path";
import sharp, { Metadata } from "sharp";
import { ResizedServedImage } from "../models/resized-served-image";
import { ServedImage } from "../models/served-image";
import { ServedImageResolution } from "../models/served-image-resolution";

export class ServedImageService {

    // public servedImage: ServedImage;
    // public resizedServedImage: ResizedServedImage;
    private imagesFolder: string = path.join(  __dirname , `/../images/`);
    private imageResolutionHeightRegExp: RegExp = RegExp(/^([\d ]{2,4})/i); // https://regex101.com/
    private imageResolutionWidthRegExp: RegExp = RegExp(/([\d ]{2,4})$/i); // https://regex101.com/

    public async getServedImage(imageName: string): Promise<ServedImage> {

        console.log(`ServedImageService: processing image named ${imageName} in folder ${this.imagesFolder}`);

        const fileAbsolutePath: string = path.join( this.imagesFolder , imageName);
        const fileExists: boolean = fs.existsSync(fileAbsolutePath);
        const fileExtension: string = path.extname(fileAbsolutePath);
        const fileName: string = path.basename(fileAbsolutePath).replace(fileExtension, "");
        let height: number | null = null;
        let width: number | null = null;

        if (fileExists) {
            await sharp(fileAbsolutePath).metadata().then((metadata: Metadata) => {
                height = metadata.height;
                width = metadata.width;
            }).catch((err) => {
                console.log(err);
            });
        }

        // typescript field initializer (maintains "type" definition)
        const servedImage = Object.assign(new ServedImage(), {
            fullName: imageName,
            fileName,
            extension: fileExtension,
            absolutePath: fileAbsolutePath,
            directoryAbsolutePath: this.imagesFolder,
            existsOnFileSystem: fileExists,
            resolution: Object.assign(new ServedImageResolution(), {
                width,
                height
            })
        });

        return servedImage;

    }

    public async getResizedServedImage(servedImage: ServedImage, imageResolution: string): Promise<ResizedServedImage> {

        const resizedFileName = `${servedImage.fileName}_${imageResolution}${servedImage.extension}`;
        const resizedFileAbsolutePath = `${this.imagesFolder}${resizedFileName}`;
        const resizedImageResolution = this.getResolution(imageResolution);
        const areResolutionsEqual: boolean = ServedImageResolution.AreResolutionsEqual(resizedImageResolution, servedImage.resolution);
        if (areResolutionsEqual) {
            const originalServedImage = Object.assign(new ResizedServedImage(), {
                fullName: servedImage.fullName,
                fileName: servedImage.fileName,
                extension: servedImage.extension,
                absolutePath: servedImage.absolutePath,
                directoryAbsolutePath: servedImage.directoryAbsolutePath,
                existsOnFileSystem: servedImage.existsOnFileSystem,
                resolution: servedImage.resolution,
                isSameAsOriginalImage: true,
                originalImage: servedImage
            });
            return Promise.resolve<ResizedServedImage>(originalServedImage);
        } else {
            const resizedServedImage = Object.assign(new ResizedServedImage(), {
                fullName: resizedFileName,
                fileName: servedImage.fileName,
                extension: servedImage.extension,
                absolutePath: resizedFileAbsolutePath, // TODO: make path with other folder
                directoryAbsolutePath: this.imagesFolder, // TODO: make path with other folder
                existsOnFileSystem: null, // TODO: set below
                resolution: resizedImageResolution,
                isSameAsOriginalImage: false,
                originalImage: servedImage
            });
            const fileExists: boolean = fs.existsSync(resizedServedImage.absolutePath);
            if (!fileExists) {
                await sharp(servedImage.absolutePath).resize(resizedServedImage.resolution.width, resizedServedImage.resolution.height).toFile(resizedServedImage.absolutePath);
            }
            return Promise.resolve<ResizedServedImage>(resizedServedImage);
        }
    }

    private getResolution(imageResolution: string): ServedImageResolution {
        const height: number = parseInt(this.imageResolutionHeightRegExp.exec(imageResolution)[0], 10);
        const width: number = parseInt(this.imageResolutionWidthRegExp.exec(imageResolution)[0], 10);

        console.log(`height: ${height} | width: ${width}`);

        const servedImageResolution = Object.assign(new ServedImageResolution(), {
            width,
            height
        });
        return servedImageResolution;
    }
}

 

In order to serve images faster we try to use expressjs static middleware.

app.use(express.static("images"));

 

More info on expressjs middleware here: https://expressjs.com/en/guide/writing-middleware.html

More info on how to use static files cache https://expressjs.com/en/starter/static-files.html

The index.ts file looks a lot slimmer now.

import express from "express";
import { RequestImageValidatorService } from "./services/request-image-validator-service";
import { ServedImageService } from "./services/served-image-service";
const app = express();
const port = 8080; // default port to listen

app.use(express.static("images")); // https://expressjs.com/en/starter/static-files.html

// define a route handler for the default home page
app.get("/", (req, res) => {
    res.send("Hello world!");
});

// define a route handler for the image processing
app.get("/image/:imageName/:imageResolution", async (req, res, next) => {
    // res.send(`image named ${req.params.imageName} and resolution ${req.params.imageResolution}`);
    const imageName = req.params.imageName;
    const imageResolution = req.params.imageResolution;
    console.log(`requested image named ${imageName} and resolution ${imageResolution}`);
    const imageValidatorService: RequestImageValidatorService = new RequestImageValidatorService(imageName, imageResolution);
    if (!imageValidatorService.validateImage()) {
        res.status(404).send({ errors: imageValidatorService.errors });
    }

    const serverImageService: ServedImageService = new ServedImageService();
    const servedImage = await serverImageService.getServedImage(imageName); // https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016
    if (servedImage.existsOnFileSystem) {// if image exists we can proceed to try and serve the resized image
        const resizedImage = await serverImageService.getResizedServedImage(servedImage, imageResolution);
        res.status(200).sendFile(resizedImage.absolutePath);
    } else {
        res.status(404).send({ error: `File ${imageName} does not exist on file system at ${servedImage.absolutePath}` });
    }

});

// start the Express server
app.listen(port, () => {
    // tslint:disable-next-line:no-console
    console.log(`server started at http://localhost:${port}`);
});

Node JS Image Delivery Microservice Challenge [Setup]

First of all we need node js server installed in our server.

Second of all we need to create the microservice server. We need to use typescript.

We take quick look here: https://developer.okta.com/blog/2018/11/15/node-express-typescript

We set up the project to use typescriptnpm install --save-dev typescript

We install tslintnpm install --save-dev tslint

We install expressnpm install express

The first running code looks like this:

import express from "express";
const app = express();
const port = 8080; // default port to listen

// define a route handler for the default home page
app.get("/", (req, res) => {
    res.send("Hello world!");
});

// start the Express server
app.listen(port, () => {
    // tslint:disable-next-line:no-console
    console.log(`server started at http://localhost:${port}`);
});

 

We start the server: npm run start

If we call http://localhost:8080/ with Postman we get Hello World.

For the image pth we add this piece of code:

// define a route handler for the image processing
app.get("/image/:imageName/:imageResolution", (req, res) => {
    res.send(`image named ${req.params.imageName} and resolution ${req.params.imageResolution}`);
});

 

We call it with Postman:

For caching we might use info from here:
https://medium.com/the-node-js-collection/simple-server-side-cache-for-express-js-with-node-js-45ff296ca0f0

Cache will not be used right now. We will focus on image serving and image resizing.

For image resizing we use this article:

https://gabrieleromanato.name/nodejs-resize-images-as-thumbnails-on-the-fly-in-expressjs

This means that we are using imagemagick and graphicsmagick libraries.

We install gm npm install gm

Examples of usage here: https://www.npmjs.com/package/gm

Maybe use this for streaming and caching https://stackoverflow.com/questions/39232296/node-js-cache-image-to-filesystem-and-pipe-image-to-response?rq=1

We try using stream for streaming: npm install stream

We use path for file name manipulation

const fileExtension = path.extname(fileAbsolutePath);
const fileName = path.basename(fileAbsolutePath).replace(fileExtension, "");

 

GM not working, mabe use sharphttps://github.com/lovell/sharp

We uninstall the old packages gm and imagemagick.

We install new sharp package npm install sharp

We use sharp to resize the image, save it to the folder and send it to the user.

 sharp(fileAbsolutePath).resize(100, 100).toFile(resizeFileAbsolutePath, (err, info) => {
                if (err) {
                    // tslint:disable-next-line:no-console
                    console.error(err);
                    res.status(404).send({ error: err });
                } else {
                    res.status(200).sendFile(resizeFileAbsolutePath);
                }
            });

 

We finally get a barely working image processor with about 30 lines of code.

// define a route handler for the image processing
app.get("/image/:imageName/:imageResolution", (req, res) => {
    // res.send(`image named ${req.params.imageName} and resolution ${req.params.imageResolution}`);
    const imageName = req.params.imageName;
    const imageResolution = req.params.imageResolution;
    const imagesFolder = __dirname + `/images/`;
    const fileAbsolutePath = imagesFolder + `${imageName}`;
    const fileExists = fs.existsSync(fileAbsolutePath);
    if (fileExists) { // file with requested resolution already exists
        const resolutionExists: boolean = false; // to see how to organize file and check for different resolutions
        if (resolutionExists) {// If the image does exist on the file system, then stream the image to the response object
            fs.createReadStream(fileAbsolutePath).pipe(res);
        } else { // file with required resolution does not exist, we must resize it
            const fileExtension = path.extname(fileAbsolutePath);
            const fileName = path.basename(fileAbsolutePath).replace(fileExtension, "");
            const resizedFileName = `${fileName}_100x100${fileExtension}`;
            const resizeFileAbsolutePath = imagesFolder + `${resizedFileName}`;
            
            sharp(fileAbsolutePath).resize(100, 100).toFile(resizeFileAbsolutePath, (err, info) => {
                if (err) {
                    // tslint:disable-next-line:no-console
                    console.error(err);
                    res.status(404).send({ error: err });
                } else {
                    res.status(200).sendFile(resizeFileAbsolutePath);
                }
            });

            
        }
    } else {
        res.status(404).send({ error: `image named ${imageName} and resolution ${imageResolution} was not found` });
    }
});

 

If we call the url with POSTMAN we get the image created on the server and the image served. (24ms)

 

Issue of RazorViewEngine caching and Layout location in .NET Core

 

 

I recently tried to implement the razor  email template explained by Scott Sauber

Tutorial here: https://scottsauber.com/2018/07/07/walkthrough-creating-an-html-email-template-with-razor-and-razor-class-libraries-and-rendering-it-from-a-net-standard-class-library/

                         

All fine and dandy until I had issues regarding the template file. Problem was that even though the file is in ‘~\bin\Debug\netcoreapp2.2’ it searches for the template in the root folder such as ‘rootfolder\Views\Shared\_Layout.cshtml’ and not in ‘rootfolder\bin\Debug\netcoreapp2.2\Views\Shared\_Layout.cshtml’.

 

This is most likely generated by the fact that I have the views as an embedded resource in a CLASS LIBRARY and not in a Web Api solution directly.

 

The weird part is that if you do not have the files in the root folder, you still get the CACHED Layout page.

The good part is that when you PUBLISH the solution, it flattens the solution so the VIEWS are in ROOT folder.

 

[Solution]

The solution seems to be in the Startup.cs folder.

Got my solution from here: https://stackoverflow.com/q/50934768/249895

//https://stackoverflow.com/q/50934768/249895
     services.Configure<Microsoft.AspNetCore.Mvc.Razor.RazorViewEngineOptions>(o => {
                o.ViewLocationFormats.Add("/Views/{0}" + Microsoft.AspNetCore.Mvc.Razor.RazorViewEngine.ViewExtension);
                o.FileProviders.Add(new Microsoft.Extensions.FileProviders.PhysicalFileProvider(AppContext.BaseDirectory));
            });

After this, you can put your code like this:

var contentRootPath = _hostingEnvironment.ContentRootPath;
string executingAssemblyDirectoryAbsolutePath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
string executingAssemblyDirectoryRelativePath = System.IO.Path.GetRelativePath(contentRootPath, executingAssemblyDirectoryAbsolutePath);

string executingFilePath = $"{executingAssemblyDirectoryAbsolutePath.Replace('\\', '/')}/Views/Main.cshtml";
string viewPath = "~/Views/Main.cshtml";
string mainViewRelativePath = $"~/{executingAssemblyDirectoryRelativePath.Replace('\\','/')}/Views/Main.cshtml";

var getViewResult = _viewEngine.GetView(executingFilePath: executingFilePath, viewPath: viewPath, isMainPage: true);

<!-- OR -->

var getViewResult = _viewEngine.GetView(executingFilePath: viewPath, viewPath: viewPath, isMainPage: true);

Getting relative path to `bin\Debug\netcoreapp2.2` in ASP.NET Core 2

If you need the relative path of the executing assemblies, you will probably need something like this: ‘~\bin\Debug\netcoreapp2.2’

 

I recently had to do this because I had to render a razor view in an ASP.NET Core 2 application.

Tutorial here: https://scottsauber.com/2018/07/07/walkthrough-creating-an-html-email-template-with-razor-and-razor-class-libraries-and-rendering-it-from-a-net-standard-class-library/

Issue was that I had  to overcome a bug(https://stackoverflow.com/a/56504181/249895) that needed the relative netcodeapp2.2 that the asemblied resided in.

Example:

var viewPath = ‘~/bin/Debug/netcoreapp2.2/Views/MainView.cshtml’;

_viewEngine.GetView(executingFilePath: viewPath , viewPath: viewPath , isMainPage: true);

Getting the `bin\Debug\netcoreapp2.2` can be achieved by using this code:

 

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
public class RenderingService : IRenderingService
{
        
	private readonly IHostingEnvironment _hostingEnvironment;
	public RenderingService(IHostingEnvironment hostingEnvironment)
	{
	_hostingEnvironment = hostingEnvironment;
	}

	public string RelativeAssemblyDirectory()
	{
		var contentRootPath = _hostingEnvironment.ContentRootPath;
		string executingAssemblyDirectoryAbsolutePath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
		string executingAssemblyDirectoryRelativePath = System.IO.Path.GetRelativePath(contentRootPath, executingAssemblyDirectoryAbsolutePath);
		return executingAssemblyDirectoryRelativePath;
	}
}

Code Beautifier: http://hilite.me/

AutoFac AsyncRunner – Fix for Autofac not working with background tasks

Inspired from here:

https://stackoverflow.com/a/33830165/249895

Usage:

    public class ServiceModule :Autofac.Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            
            builder.RegisterType<AutoFac.AsyncRunner>().As<AutoFac.IAsyncRunner>().SingleInstance();
        }
    }

private AutoFac.IAsyncRunner _asyncRunner;

public Controller(AutoFac.IAsyncRunner asyncRunner)
{
	
	_asyncRunner = asyncRunner;
}

public void Function()
{
	_asyncRunner.Run<IService>((cis) =>
   {
	   try
	   {
		  //do stuff
	   }
	   catch
	   {
		   // catch stuff
		   throw;
	   }
   });
}

Class:

public class AsyncRunner : IAsyncRunner
{
	private ILifetimeScope _lifetimeScope { get; set; }

	public AsyncRunner(ILifetimeScope lifetimeScope)
	{
		//Guard.NotNull(() => lifetimeScope, lifetimeScope);
		_lifetimeScope = lifetimeScope;
	}

	public Task Run<T>(Action<T> action)
	{
		Task.Factory.StartNew(() =>
		{
			using (var lifetimeScope = _lifetimeScope.BeginLifetimeScope(MatchingScopeLifetimeTags.RequestLifetimeScopeTag))
			{
				var service = lifetimeScope.Resolve<T>();
				action(service);
			}
		});
		return Task.FromResult(0);
	}


}