From d83a2c35222c3f3b26e485c76fa8bde4db72a481 Mon Sep 17 00:00:00 2001 From: Brice Vandeputte Date: Sun, 10 Mar 2024 21:16:58 +0100 Subject: [PATCH] Fix #5 add plugins cmd,config,rsc onMessage --- package-lock.json | 152 ++++++++++++++++++ package.json | 1 + src/device-mode/service/LoDevice.js | 10 +- src/device-mode/service/LoDeviceController.js | 2 +- .../service/feature/LoCommandFeature.js | 23 ++- .../service/feature/LoConfigFeature.js | 32 +++- .../service/feature/LoResourceFeature.js | 135 +++++++++++++++- src/device-mode/service/util.js | 40 ++++- 8 files changed, 376 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index a6a0e63..e493a13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "axios": "^1.6.7", "dotenv": "^16.4.5", "log4js": "^6.9.1", "mqtt": "^5.3.6", @@ -62,6 +63,21 @@ "node": ">=6.5" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -125,6 +141,17 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commist": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", @@ -232,6 +259,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -324,6 +359,38 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -566,6 +633,25 @@ "node": ">=10" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -693,6 +779,11 @@ "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -976,6 +1067,21 @@ "event-target-shim": "^5.0.0" } }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "requires": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1011,6 +1117,14 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commist": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", @@ -1086,6 +1200,11 @@ "object-keys": "^1.1.1" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -1148,6 +1267,21 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" }, + "follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -1318,6 +1452,19 @@ "escape-string-regexp": "^4.0.0" } }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, "minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -1421,6 +1568,11 @@ "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==" }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", diff --git a/package.json b/package.json index 6d6cb6d..f5488fe 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "author": "", "license": "ISC", "dependencies": { + "axios": "^1.6.7", "dotenv": "^16.4.5", "log4js": "^6.9.1", "mqtt": "^5.3.6", diff --git a/src/device-mode/service/LoDevice.js b/src/device-mode/service/LoDevice.js index bff26aa..2581088 100644 --- a/src/device-mode/service/LoDevice.js +++ b/src/device-mode/service/LoDevice.js @@ -143,11 +143,13 @@ export default class LoDevice { }); // MQTT on message - client.on('message', (topic, message) => { - logger.debug(`[${topic}]< ${message}`); + client.on('message', (topic, messageString) => { + const request = JSON.parse(messageString); + logger.debug(`[${topic}]< ${JSON.stringify(request)}`); loDevice.features.forEach(feature => { - if (feature.getHandledTopics().indexOf(topic) > 0) { - feature.onMessage(topic, message); + let featureTopics = feature.getHandledTopics(); + if (featureTopics?.includes(topic)) { + feature.onMessage(topic, request); } }); }); diff --git a/src/device-mode/service/LoDeviceController.js b/src/device-mode/service/LoDeviceController.js index 1916180..536b1b1 100644 --- a/src/device-mode/service/LoDeviceController.js +++ b/src/device-mode/service/LoDeviceController.js @@ -49,7 +49,7 @@ export default class LoDeviceController { }) } - ask("mqttController> ") + ask("|> ") } stopAskInput() { diff --git a/src/device-mode/service/feature/LoCommandFeature.js b/src/device-mode/service/feature/LoCommandFeature.js index ea1b177..54cfcb0 100644 --- a/src/device-mode/service/feature/LoCommandFeature.js +++ b/src/device-mode/service/feature/LoCommandFeature.js @@ -2,10 +2,13 @@ import log4js from "log4js"; const topicCommand = 'dev/cmd'; const topicCommandRsp = 'dev/cmd/res'; +const nodeResponse = 'this is an answer from nodeJs client LoCommandFeature'; export default class LoCommandFeature { constructor({}) { this.logger = log4js.getLogger(); this.logger.level = 'DEBUG'; + this.handledCommand = 0; + this.commandNoAnswer = 0; } getName() { @@ -13,7 +16,8 @@ export default class LoCommandFeature { } getStats() { - return {handledCommand: 0} + const {handledCommand} = this; + return {handledCommand} } getHandledTopics() { @@ -21,10 +25,25 @@ export default class LoCommandFeature { } onConnect({client}) { + this.publishCommandResponse = object => client.publishTopic(topicCommandRsp, object); client.subscribeTopic(topicCommand); } onMessage(topic, message) { - this.logger.debug(`[${topic}]< ${message}`); + if (topicCommand === topic) { + this.onCommandMessage(message); + } + } + + onCommandMessage(message) { + if (this.commandNoAnswer <= 0) { + const msgCommand = {"res": {"data": nodeResponse}, "cid": message.cid}; + this.logger.info(`OK result [${topicCommandRsp}]> ${JSON.stringify(msgCommand)}`); + this.publishCommandResponse(msgCommand); + } else { + this.logger.info('no answer to command'); + this.commandNoAnswer--; + } + this.handledCommand++; } } \ No newline at end of file diff --git a/src/device-mode/service/feature/LoConfigFeature.js b/src/device-mode/service/feature/LoConfigFeature.js index 81ec276..412b542 100644 --- a/src/device-mode/service/feature/LoConfigFeature.js +++ b/src/device-mode/service/feature/LoConfigFeature.js @@ -9,26 +9,52 @@ export default class LoConfigFeature { this.logger.level = 'DEBUG'; this.config = config; this.publishDeviceConfig = publishDeviceConfig; + this.configFailure = 0; + this.handledConfig = 0; } getName() { return "config"; } + getStats() { + const {handledConfig} = this; + return {handledConfig} + } + getHandledTopics() { return [topicConfigUpdate]; } onConnect({client}) { const {config, publishDeviceConfig} = this; - client.publishConfig = object => client.publishTopic(topicConfig, object); + this.publishConfig = object => client.publishTopic(topicConfig, object); client.subscribeTopic(topicConfigUpdate); if (isTrue(publishDeviceConfig)) { - client.publishConfig(config); + this.publishConfig(config); } } onMessage(topic, message) { - this.logger.debug(`[${topic}]< ${message}`); + if (topicConfigUpdate === topic) { + this.onConfigUpdateMessage(message); + } + } + + onConfigUpdateMessage(message) { + if (this.configFailure > 0) { + // hack a wrong value for a requested parameter + const msgWrongCfg = message; + const firstParam = Object.keys(message.cfg)[0]; + msgWrongCfg.cfg[firstParam].v = 666; + this.logger.info(`FAILED config [${topicConfig}]> ${JSON.stringify(msgWrongCfg)}`); + this.publishConfig(msgWrongCfg); + this.configFailure--; + } else { + // success + this.config = message; + this.publishConfig(this.config); + } + this.handledConfig++; } } \ No newline at end of file diff --git a/src/device-mode/service/feature/LoResourceFeature.js b/src/device-mode/service/feature/LoResourceFeature.js index 1324809..b7b554a 100644 --- a/src/device-mode/service/feature/LoResourceFeature.js +++ b/src/device-mode/service/feature/LoResourceFeature.js @@ -1,37 +1,164 @@ import log4js from "log4js"; -import {isTrue} from "../util.js"; +import {downloadFile, isTrue, randomFromArray} from "../util.js"; const topicResource = 'dev/rsc'; const topicResourceUpd = 'dev/rsc/upd'; const topicResourceUpdResp = 'dev/rsc/upd/res'; const topicResourceUpdErr = 'dev/rsc/upd/err'; +const nodeResponse = 'this is an answer from nodeJs client LoCommandFeature'; +// one of predefined error +const possibleErrors = [ + // 2.1 // "UNKNOWN_RESOURCE", + 'INVALID_RESOURCE', + 'WRONG_SOURCE_VERSION', + 'WRONG_TARGET_VERSION', + 'NOT_AUTHORIZED', + 'INTERNAL_ERROR', +]; export default class LoResourceFeature { constructor({resource, publishDeviceResource}) { this.logger = log4js.getLogger(); this.logger.level = 'DEBUG'; this.resource = resource; this.publishDeviceResource = publishDeviceResource; + this.resourceFailure = 0; + this.withNoAnswer = false; + this.withDeviceError = process.env.LO_MQTT_DEFAULT_WITH_DEVICE_ERROR === 'true' | false;4 + this.withDownloadStep = true; + this.withDownloadExit = false; + this.handledResource = 0; } getName() { return "resource"; } + getStats() { + const {handledResource} = this; + return {handledResource} + } + getHandledTopics() { return [topicResourceUpd]; } onConnect({client}) { const {resource, publishDeviceResource} = this; - client.publishResource = object => client.publishTopic(topicResource, object); + this.publishResource = object => client.publishTopic(topicResource, object); + this.publishResourceUpdateError = object => client.publishTopic(topicResourceUpdErr, object); + this.publishResourceUpdateResponse = object => client.publishTopic(topicResourceUpdResp, object); client.subscribeTopic(topicResourceUpd); if (isTrue(publishDeviceResource)) { - client.publishResource(resource); + this.publishResource(resource); } } onMessage(topic, message) { - this.logger.debug(`[${topic}]< ${message}`); + if (topicResourceUpd === topic) { + this.onMessageRessourceUpdate(message); + } + } + + onMessageRessourceUpdate(message) { + if (this.resourceFailure > 0) { + this.onMessageResourceUpdateFailure(message); + } else { + this.onMessageResourceUpdateSuccess(message); + } + this.handledResource++; + } + + onMessageResourceUpdateFailure(message) { + const {logger} = this; + if (this.withNoAnswer) { + logger.info('no answer to resource update'); + } else if (this.withDeviceError) { + // device custom details + const msgDeviceError = { + errorCode: 'NY_NODE_JS_UNKNOWN_RESOURCE', + errorDetails: nodeResponse, + }; + logger.info(`[${topicResourceUpdErr}]> ${JSON.stringify(msgDeviceError)}`); + this.publishResourceUpdateError(msgDeviceError); + } else { + const randomError = randomFromArray(possibleErrors); + const msgError = {res: randomError, cid: message.cid}; + logger.info(`FAILED [${topicResourceUpdResp}]> ${JSON.stringify(msgError)}`); + this.publishResourceUpdateResponse(msgError); + } + this.resourceFailure--; + } + + onMessageResourceUpdateSuccess(message) { + const {logger} = this; + + // good resource update + const correlationId = message.cid; + const resourceId = message.id; + const resourceNewVersion = message.new; + const resourceUrl = message.m.uri; + const resourceSize = message.m.size; + const resourceMd5 = message.m.md5; + + // accept update + const msgOk = {res: 'OK', cid: correlationId}; + logger.info(`ACCEPT [${topicResourceUpdResp}]> ${JSON.stringify(msgOk)}`); + this.publishResourceUpdateResponse(msgOk); + const simulateUpdateDone = () => this.simulateResourceUpdateDone(resourceId, resourceNewVersion); + + if (this.withDownloadStep) { + // download resource + this.downloadFileStep( + resourceId, + resourceNewVersion, + resourceUrl, + resourceSize, + resourceMd5, + this.withDownloadExit + ).then(simulateUpdateDone); + } else { + simulateUpdateDone(); + } + } + + simulateResourceUpdateDone(resourceId, resourceNewVersion) { + // act version update internally + this.resource.rsc[resourceId].v = resourceNewVersion; + // publish new version + this.publishResource(this.resource); + } + + downloadFileStep( + resourceId, + resourceNewVersion, + resourceUrl, + resourceSize, + resourceMd5, + downloadError + ) { + const {logger} = this; + return new Promise((resolve, reject) => { + logger.debug(`download resource ${resourceId} version ${resourceNewVersion} from ${resourceUrl}`); + downloadFile(resourceUrl, 'lastFirmware.raw') + .catch(err => {// download issues are just reported as warn + let details = ""; + if ("EPROTO" === err.code) { + details += " protocol issue (seems that resource server don't match url scheme)."; + } + logger.warn(`Download error code:${err.code} errno:${err.errno} ${details}`); + reject(err); + }) + .then(result => { + logger.debug('download done'); + if (downloadError) { + setTimeout(() => { + logger.debug('simulate an error while downloading firmware: enforce exit()'); + process.exit(); + }, 10); + } + resolve(); + }); + }); } } \ No newline at end of file diff --git a/src/device-mode/service/util.js b/src/device-mode/service/util.js index 2979f3c..4901e8e 100644 --- a/src/device-mode/service/util.js +++ b/src/device-mode/service/util.js @@ -1,10 +1,10 @@ import InvalidConfigurationError from "../exception/InvalidConfigurationError.js"; import * as fs from "fs"; -import path from 'path'; -import {fileURLToPath} from 'url'; +import Axios from "axios"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} function isSet(variable) { return (variable !== undefined && variable !== null); @@ -14,6 +14,10 @@ function isTrue(variable) { return ["true", "TRUE", "1"].indexOf(variable) >= 0 } +function randomFromArray(arrayValue) { + return arrayValue[Math.floor(Math.random() * arrayValue.length)]; +} + function assumeConfigurationKeySet(object, keyName, envDefaultValue = null, defaultValue = null) { if (!keyName in object || !isSet(object[keyName])) { const envValue = process.env[envDefaultValue]; @@ -35,4 +39,30 @@ function loadJSON(path) { return JSON.parse(buffer); } -export {isSet, isTrue, assumeConfigurationKeySet, loadJSON}; \ No newline at end of file +async function downloadFile(url, outputLocationPath) { + return new Promise((resolve, reject) => { + const method = "get", responseType = "stream"; + const writer = fs.createWriteStream(outputLocationPath); + Axios({method, url, responseType}) + .then(response => { + //ensure that the user can call `then()` only when the file has + //been downloaded entirely. + response.data.pipe(writer); + let error = null; + writer.on('error', err => { + error = err; + writer.close(); + reject(err); + }); + writer.on('close', () => { + if (!error) { + resolve(true); + } + //no need to call the reject here, as it will have been called in the + //'error' stream; + }); + }); + }); +} + +export {sleep, isSet, isTrue, randomFromArray, assumeConfigurationKeySet, loadJSON, downloadFile}; \ No newline at end of file