diff --git a/server/config/default-test.json5 b/server/config/default-test.json5 index 1f4644b..f496e09 100644 --- a/server/config/default-test.json5 +++ b/server/config/default-test.json5 @@ -4,7 +4,6 @@ server: 'dar-test-server', api: 'dar-test-api', email: 'dar-test-email', - image: 'dar-test-image', }, uri: { mongo: 'mongodb://localhost/dar-test-v1', diff --git a/server/config/default.json5 b/server/config/default.json5 index a0ca52a..7388004 100644 --- a/server/config/default.json5 +++ b/server/config/default.json5 @@ -4,7 +4,6 @@ server: 'dar-server', api: 'dar-api', email: 'dar-email', - image: 'dar-image', }, uri: { mongo: 'mongodb://localhost/dar-v1', diff --git a/server/package.json b/server/package.json index 3e7044c..972ddbc 100644 --- a/server/package.json +++ b/server/package.json @@ -11,8 +11,6 @@ "test": "jest", "actor:api": "monzilla 'src/api/**/*.js:src/database/**/*.js' -- babel-node src/api/index.js", "actor:api:debug": "babel-node --inspect-brk src/api/index.js", - "actor:image": "monzilla 'src/image/**/*.js:src/(message-service|database)/**/*.js' -- babel-node src/image/index.js", - "actor:image:debug": "babel-node --inspect-brk src/image/index.js", "actor:email": "monzilla 'src/email/**/*.js:src/(message-service|database)/**/*.js' -- babel-node src/email/index.js", "actor:email:debug": "babel-node --inspect-brk src/email/index.js" }, diff --git a/server/src/ServerTool.js b/server/src/ServerTool.js index ab2441f..08b6438 100644 --- a/server/src/ServerTool.js +++ b/server/src/ServerTool.js @@ -1,18 +1,14 @@ -import childProcess from 'child_process' -import path from 'path' -import timers from 'timers' -import autobind from 'autobind-decorator' +import childProcess from "child_process" +import path from "path" +import timers from "timers" +import autobind from "autobind-decorator" @autobind export class ServerTool { constructor(toolName, log) { this.toolName = toolName this.log = log - this.actors = [ - { name: 'api' }, - { name: 'email' }, - { name: 'image' }, - ] + this.actors = [{ name: "api" }, { name: "email" }] } restart(actor) { @@ -27,7 +23,11 @@ export class ServerTool { const backOffTime = (Math.pow(2, Math.min(actor.backOff, 7)) - 1) * 1000 if (backOffTime > 0) { - this.log.warn(`Actor '${actor.name}' died quickly, waiting ${Math.floor(backOffTime / 1000)} seconds to restart`) + this.log.warn( + `Actor '${actor.name}' died quickly, waiting ${Math.floor( + backOffTime / 1000 + )} seconds to restart` + ) } else { this.log.warn(`Actor ${actor.name} died, restarting`) } @@ -36,9 +36,9 @@ export class ServerTool { actor.starts += 1 actor.startTime = Date.now() actor.proc = childProcess.fork(actor.modulePath) - actor.proc.on('exit', (code, signal) => { + actor.proc.on("exit", (code, signal) => { // Don't restart if the exit was clean or Control+C - if (code === 0 || signal === 'SIGINT') { + if (code === 0 || signal === "SIGINT") { this.log.info(`Actor ${actor.name} terminated normally`) return } @@ -53,43 +53,53 @@ export class ServerTool { let promises = [] this.actors.forEach((actor) => { - promises.push(new Promise((resolve, reject) => { - actor.modulePath = path.join(__dirname, actor.name) - actor.startTime = Date.now() - actor.backOff = 0 - actor.timeToInit = actor.timeToInit || 3000 - actor.proc = childProcess.fork(actor.modulePath) - actor.proc.on('exit', (code, signal) => { - const timeSinceStart = Date.now() - actor.startTime + promises.push( + new Promise((resolve, reject) => { + actor.modulePath = path.join(__dirname, actor.name) + actor.startTime = Date.now() + actor.backOff = 0 + actor.timeToInit = actor.timeToInit || 3000 + actor.proc = childProcess.fork(actor.modulePath) + actor.proc.on("exit", (code, signal) => { + const timeSinceStart = Date.now() - actor.startTime - if (timeSinceStart < actor.timeToInit) { - this.log.error(`Actor '${actor.name}' exited during initialization`) + if (timeSinceStart < actor.timeToInit) { + this.log.error( + `Actor '${actor.name}' exited during initialization` + ) - if (this.actors) { - this.actors.forEach((otherActor) => { - if (otherActor !== actor) { - this.log.info(`Terminating actor ${actor.name}, pid ${otherActor.proc.pid}`) - otherActor.proc.kill('SIGINT') - otherActor.proc = null - } - }) + if (this.actors) { + this.actors.forEach((otherActor) => { + if (otherActor !== actor) { + this.log.info( + `Terminating actor ${actor.name}, pid ${ + otherActor.proc.pid + }` + ) + otherActor.proc.kill("SIGINT") + otherActor.proc = null + } + }) - actor.proc = null - this.actors = null + actor.proc = null + this.actors = null + } + + return reject( + new Error("All actors must initialize on first start") + ) } - return reject(new Error('All actors must initialize on first start')) - } - - this.restart(actor) + this.restart(actor) + }) + timers.setTimeout(() => { + if (actor.proc) { + resolve() + } + }, actor.timeToInit) + this.log.info(`Started actor '${actor.name}', pid ${actor.proc.pid}`) }) - timers.setTimeout(() => { - if (actor.proc) { - resolve() - } - }, actor.timeToInit) - this.log.info(`Started actor '${actor.name}', pid ${actor.proc.pid}`) - })) // new Promise() + ) // new Promise() }) return Promise.all(promises) diff --git a/server/src/bin/sendMessage.js b/server/src/bin/sendMessage.js index 27641e7..f114fc5 100644 --- a/server/src/bin/sendMessage.js +++ b/server/src/bin/sendMessage.js @@ -29,7 +29,7 @@ class SendMessageTool { usage: ${this.toolName} [options] options: - -x --exchange Exchange to send the message too, e.g. tmr-image + -x --exchange Exchange to send the message too -t --type The type of the message content `) return 0 diff --git a/server/src/database/schemas/user.js b/server/src/database/schemas/user.js index fb14178..98969cd 100644 --- a/server/src/database/schemas/user.js +++ b/server/src/database/schemas/user.js @@ -22,7 +22,6 @@ export let userSchema = new Schema( unique: true, }, newEmail: { type: String, match: regExpPattern.email }, - thumbnailImageId: { type: Schema.Types.ObjectId }, emailToken: { type: { value: String, @@ -71,8 +70,6 @@ userSchema.methods.toClient = function(authUser) { _id: this._id, email: this.email, emailValidated: !!this.emailToken !== true, - imageId: this.imageId, - thumbnailImageId: this.thumbnailImageId, firstName: this.firstName, lastName: this.lastName, administrator: this.administrator, diff --git a/server/src/image/ImageHandlers.js b/server/src/image/ImageHandlers.js deleted file mode 100644 index 9e18ba3..0000000 --- a/server/src/image/ImageHandlers.js +++ /dev/null @@ -1,298 +0,0 @@ -import Canvas from 'canvas' -import fs from 'fs' -import util from 'util' -import createError from 'http-errors' -import autobind from 'autobind-decorator' -import stream from 'stream' - -function streamToBuffer(readable) { - return new Promise((resolve, reject) => { - var chunks = [] - var writeable = new stream.Writable() - - writeable._write = function (chunk, enc, done) { - chunks.push(chunk) - done() - } - - readable.on('end', function () { - resolve(Buffer.concat(chunks)) - }) - - readable.on('error', (err) => { - reject(err) - }) - - readable.pipe(writeable); - }) -} - -function pipeToGridFS(readable, gfsWriteable) { - const promise = new Promise((resolve, reject) => { - readable.on('error', (error) => { - reject(error) - }) - gfsWriteable.on('error', (error) => { - reject(error) - }) - gfsWriteable.on('close', (file) => { - resolve(file) - }) - }) - readable.pipe(gfsWriteable) - return promise -} - -function loadImage(buf) { - return new Promise((resolve, reject) => { - const image = new Canvas.Image() - - function cleanup () { - image.onload = null - image.onerror = null - } - - image.onload = () => { - cleanup(); - resolve(image) - } - image.onerror = () => { - cleanup(); - reject(new Error(`Failed to load the image "${buf}"`)) - } - - image.src = buf - }) -} - -// Derived from https://stackoverflow.com/questions/20600800/js-client-side-exif-orientation-rotate-and-mirror-jpeg-images -function getExifOrientation(buf) { - if (buf.length < 2 || buf.readUInt16BE(0) != 0xFFD8) { - return -2 - } - - let length = buf.byteLength - let offset = 2 - - while (offset < length) { - let marker = buf.readUInt16BE(offset) - offset += 2 - - if (marker == 0xFFE1) { - if (buf.readUInt32BE(offset += 2) != 0x45786966) { - return -1 - } - let little = (buf.readUInt16BE(offset += 6) == 0x4949) - offset += (little ? buf.readUInt32LE(offset + 4) : buf.readUInt32BE(offset + 4)) - - const numTags = (little ? buf.readUInt16LE(offset) : buf.readUInt16BE(offset)) - - offset += 2 - for (let i = 0; i < numTags; i++) { - let val = (little ? buf.readUInt16LE(offset + (i * 12)) : buf.readUInt16BE(offset + (i * 12))) - - if (val === 0x0112) { - return (little ? buf.readUInt16LE(offset + (i * 12) + 8) : buf.readUInt16BE(offset + (i * 12) + 8)) - } - } - } else if ((marker & 0xFF00) != 0xFF00) { - break - } else { - offset += buf.readUInt16BE(offset) - } - } -} - -function normalizeOrientation(image, orientation) { - let width = image.width - let height = image.height - let canvas = new Canvas(width, height) - let ctx = canvas.getContext("2d") - - if (4 < orientation && orientation < 9) { - canvas.width = height - canvas.height = width - } else { - canvas.width = width - canvas.height = height - } - - switch (orientation) { - case 2: ctx.transform(-1, 0, 0, 1, width, 0); break - case 3: ctx.transform(-1, 0, 0, -1, width, height ); break - case 4: ctx.transform(1, 0, 0, -1, 0, height ); break - case 5: ctx.transform(0, 1, 1, 0, 0, 0); break - case 6: ctx.transform(0, 1, -1, 0, height , 0); break - case 7: ctx.transform(0, -1, -1, 0, height , width); break - case 8: ctx.transform(0, -1, 1, 0, 0, width); break - default: return Promise.resolve(image) - } - - ctx.drawImage(image, 0, 0) - return loadImage(canvas.toBuffer()) -} - -@autobind -export class ImageHandlers { - constructor(container) { - this.db = container.db - this.log = container.log - } - - scaleImage(options) { - const { - newWidth = 400, - newHeight = 100, - scaleMode = 'scaleToFill', - inputFile, - inputAssetId, - outputFile - } = options - - if (!inputFile && !inputAssetId) { - return Promise.reject(createError.BadRequest(`No inputAssetId or inputFile given`)) - } - - let canvas = new Canvas(newWidth, newHeight) - let ctx = canvas.getContext("2d") - - ctx.imageSmoothingEnabled = true - - let loadPromise = null - - if (inputFile) { - loadPromise = util.promisify(fs.readFile)(inputFile) - } else { - loadPromise = streamToBuffer(this.db.gridfs.createReadStream({ _id: inputAssetId })) - } - - let orientation - - return loadPromise.then((buf) => { - orientation = getExifOrientation(buf) - return loadImage(buf) - }).then((img) => { - return normalizeOrientation(img, orientation) - }).then((img) => { - let x = 0 - let y = 0 - let scale = 1 - - switch (scaleMode) { - case 'aspectFill': - if (img.width - newWidth > img.height - newHeight) { - scale = newHeight / img.height - x = -(img.width * scale - newWidth) / 2 - } else { - scale = newWidth / img.width - y = -(img.height * scale - newHeight) / 2 - } - ctx.drawImage(img, x, y, img.width * scale, img.height * scale) - break - case 'aspectFit': - if (img.width - newWidth > img.height - newHeight) { - scale = newWidth / img.width - y = (newHeight - img.height * scale) / 2 - } else { - scale = newHeight / img.height - x = (newWidth - img.width * scale) / 2 - } - ctx.drawImage(img, x, y, img.width * scale, img.height * scale) - break - case 'scaleToFill': - default: - ctx.drawImage(img, 0, 0, newWidth, newHeight) - break - } - - let savePromise = null - let readable = canvas.createPNGStream() - let writeable = null - - if (outputFile) { - writeable = fs.createWriteStream(outputFile) - } else { - const _id = this.db.newObjectId() - writeable = this.db.gridfs.createWriteStream({ - _id, - filename: _id + '.png', - content_type: 'image/png', - metadata: { - scaledFrom: this.db.newObjectId(inputAssetId), - width: newWidth, - height: newHeight - } - }) - } - - return pipeToGridFS(readable, writeable).then((file) => { - let res = {} - if (outputFile) { - res.outputFile = outputFile - } else if (file) { - res.outputAssetId = file._id - } - return Promise.resolve(res) - }) - }) - } - - createPlaceholder(options) { - const { - width = 400, - height = 100, - fontSize = 36, - fontName = 'helvetica neue, arial black, sans serif', - fontWeight = 'bold', - background = '#1B1B1B', - foreground = '#333333', - outputFile - } = options - - let canvas = new Canvas(width, height) - let ctx = canvas.getContext("2d") - const text = `${width}x${height}` - - ctx.fillStyle = background - ctx.fillRect(0, 0, width, height) - - ctx.font = `${fontWeight} ${fontSize}px ${fontName}` - ctx.fillStyle = foreground - const tm = ctx.measureText(text) - - if (tm.width <= width / 2 && fontSize <= height / 2) { - ctx.textAlign = 'center' - ctx.textBaseline = 'middle' - ctx.fillText(text, width / 2, height / 2, width) - } - - let promise = null - let readable = canvas.createPNGStream() - let writeable = null - - if (outputFile) { - writeable = fs.createWriteStream(outputFile) - } else { - const _id = this.db.newObjectId() - writeable = this.db.gridfs.createWriteStream({ - _id, - filename: `${width}x${height}.png`, - content_type: 'image/png', - metadata: { - width: width, - height: height, - } - }) - } - - return pipeToGridFS(readable, writeable).then((file) => { - let res = {} - if (outputFile) { - res.outputFile = outputFile - } else if (file) { - res.outputAssetId = file._id - } - return Promise.resolve(res) - }) - } -} diff --git a/server/src/image/index.js b/server/src/image/index.js deleted file mode 100644 index 5b72464..0000000 --- a/server/src/image/index.js +++ /dev/null @@ -1,45 +0,0 @@ -import config from "config" -import pino from "pino" -import * as pinoExpress from "pino-pretty-express" -import { DB } from "../database" -import { MS } from "../message-service" -import { ImageHandlers } from "./ImageHandlers" -import path from "path" -import fs from "fs" - -const serviceName = config.get("serviceName.image") -const isProduction = process.env.NODE_ENV === "production" -let log = null - -if (isProduction) { - log = pino( - { name: serviceName }, - fs.createWriteStream(path.join(config.get("logDir"), serviceName + ".log")) - ) -} else { - const pretty = pinoExpress.pretty({}) - pretty.pipe(process.stdout) - log = pino({ name: serviceName }, pretty) -} - -const ms = new MS(serviceName, { durable: false }, log) -const db = new DB() -let container = { db, ms, log } -const mongoUri = config.get("uri.mongo") -const amqpUri = config.get("uri.amqp") - -Promise.all([db.connect(mongoUri), ms.connect(amqpUri)]) - .then(() => { - log.info(`Connected to MongoDB at ${mongoUri}`) - log.info(`Connected to RabbitMQ at ${amqpUri}`) - - container = { - ...container, - handlers: new ImageHandlers(container), - } - - ms.listen(container.handlers) - }) - .catch((err) => { - log.error(isProduction ? err.message : err) - })