Plugins

In the spirit of Seneca, we should make a plugin for our imagini service. Let's split our code into two parts:

  • The imagini plugin, a service that manipulates images
  • A Seneca microservice, which exposes the imagini plugin, and possibly others later on

There's lot of room for improvement on our code, starting with code we repeated constantly. It's important to detect repetitions while our service is still very small.

The most repeated part is the local filename. This is actually something you might want to configure when starting the service, so let's change that to a function. Start by changing our imagini.js file to be a plugin. Clear all content and write this code:

const sharp   = require("sharp");
const path = require("path");
const fs = require("fs");

module.exports = function (settings = { path: "uploads" }) {
// plugin code goes here
};

This is the basis of our plugin. We're loading the modules we need, but not Seneca, because our plugin will have access to the service directly. Seneca itself will load the plugin by calling our exported function. Following the idea of being able to configure the local image folder, we define an optional settings parameter, which will default to an object with the property path equal to uploads, which is the folder we've been using so far.

Now, let's add the content of our plugin, inside the preceding function:

const localpath = (image) => {
return path.join(settings.path, image);
}

We start by defining a function that will convert our image parameter to the local path. We can actually reduce the function to be written in one single line:

const localpath = (image) => (path.join(settings.path, image));

Then, let's create another function that will check whether we have access to a local file, and return a Boolean (if it exists or not) and the filename we provided:

const access = (filename, next) => {
fs.access(filename, fs.constants.R_OK , (err) => {
return next(!err, filename);
});
};

We can use this for our image check, and for our image download. This way, we can improve or even cache the results for greater performance, avoiding excessive filesystem hits. Our image check route can now be written in a very concise way:

this.add("role:check,image:*", (msg, next) => {
access(localpath(msg.image), (exists) => {
return next(null, { exists : exists });
});
});

Notice that we're referring to the this object. Our Seneca service will call our plugin function and reference itself to this. Again, we can write it in a more concise way:

this.add("role:check,image:*", (msg, next) => {
access(localpath(msg.image), (exists) => (next(null, { exists })));
});

Our upload route is fairly simple and has no changes:

this.add("role:upload,image:*,data:*", (msg, next) => {
let data = Buffer.from(msg.data, "base64");

fs.writeFile(localpath(msg.image), data, (err) => {
return next(err, { size : data.length });
});
});

The download route uses our previously created helper functions to avoid storing our local filename. We also made some tweaks to how width and height were treated:

this.add("role:download,image:*", (msg, next) => {
access(localpath(msg.image), (exists, filename) => {
if (!exists) return next(new Error("image not found"));

let image = sharp(filename);
let width = +msg.width || null;
let height = +msg.height || null;
let blur = +msg.blur;
let sharpen = +msg.sharpen;
let greyscale = !!msg.greyscale;
let flip = !!msg.flip;
let flop = !!msg.flop;

if (width && height) image.ignoreAspectRatio();
if (width || height) image.resize(width, height);
if (flip) image.flip();
if (flop) image.flop();
if (blur > 0) image.blur(blur);
if (sharpen > 0) image.sharpen(sharpen);
if (greyscale) image.greyscale();

image.toBuffer().then((data) => {
return next(null, { data: data.toString("base64") });
});
});
});

There are actually a lot of variables we're using where we could just check the message parameter instead. We can rewrite our download function and get one-third reduction:

this.add("role:download,image:*", (msg, next) => {
access(localpath(msg.image), (exists, filename) => {
if (!exists) return next(new Error("image not found"));

let image = sharp(filename);
let width = +msg.width || null;
let height = +msg.height || null;

if (width && height) image.ignoreAspectRatio();
if (width || height) image.resize(width, height);
if (msg.flip) image.flip();
if (msg.flop) image.flop();
if (msg.blur > 0) image.blur(blur);
if (msg.sharpen > 0) image.sharpen(sharpen);
if (msg.greyscale) image.greyscale();

image.toBuffer().then((data) => {
return next(null, { data: data.toString("base64") });
});
});
});

In the end, you should have an imagini.js file with the following content:

const sharp   = require("sharp");
const path = require("path");
const fs = require("fs");

module.exports = function (settings = { path: "uploads" }) {
const localpath = (image) => (path.join(settings.path, image));
const access = (filename, next) => {
fs.access(filename, fs.constants.R_OK , (err) => {
return next(!err, filename);
});
};

this.add("role:check,image:*", (msg, next) => {
access(localpath(msg.image), (exists) => (next(null, { exists })));
});

this.add("role:upload,image:*,data:*", (msg, next) => {
let data = Buffer.from(msg.data, "base64");

fs.writeFile(localpath(msg.image), data, (err) => {
return next(err, { size : data.length });
});
});

this.add("role:download,image:*", (msg, next) => {
access(localpath(msg.image), (exists, filename) => {
if (!exists) return next(new Error("image not found"));

let image = sharp(filename);
let width = +msg.width || null;
let height = +msg.height || null;

if (width && height) image.ignoreAspectRatio();
if (width || height) image.resize(width, height);
if (msg.flip) image.flip();
if (msg.flop) image.flop();
if (msg.blur > 0) image.blur(blur);
if (msg.sharpen > 0) image.sharpen(sharpen);
if (msg.greyscale) image.greyscale();

image.toBuffer().then((data) => {
return next(null, { data: data.toString("base64") });
});
});
});
};

We just need to create our Seneca service and use our plugin. This is actually very straightforward. Create a file called seneca.js, and add the following:

const seneca  = require("seneca");
const service = seneca();

service.use("./imagini.js", { path: __dirname + "/uploads" });

service.listen(3000);

What the code does, line by line, is as follows:

  1. Loads the seneca module
  2. Creates a Seneca service
  3. Loads the imagini.js plugin and passes our desired path
  4. Starts service on port 3000

That's it, our service is now a plugin and could be used by any Seneca service! You should now start our service by running the new file and not imagini.js directly:

node seneca
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset