diff --git a/.env.development b/.env.development index 101e9379f8..f2d9e3ea33 100644 --- a/.env.development +++ b/.env.development @@ -12,6 +12,11 @@ SESS_SECRET='XopEn BlowFISH' DEBUG=app +# Amazon S3 Creds +S3_ACCESS_KEY_ID="" +S3_SECRET_ACCESS_KEY="" +S3_BUCKET_NAME="bhima-backups-v1" + UPLOAD_DIR='client/upload' # control Redis Pub/Sub diff --git a/package.json b/package.json index 7958eb5b22..c9501f4883 100644 --- a/package.json +++ b/package.json @@ -64,14 +64,17 @@ "json-2-csv": "^2.1.0", "json2xls": "^0.1.2", "lodash": "^4.16.2", + "lzma-native": "^3.0.4", "mkdirp": "^0.5.1", "moment": "^2.15.0", "morgan": "^1.6.1", "multer": "^1.1.0", "mysql": "^2.14.0", "q": "~1.5.0", + "s3-client": "^4.4.1", "snyk": "^1.41.1", "stream-to-promise": "^2.2.0", + "tempy": "^0.2.1", "use-strict": "^1.0.1", "uuid": "^3.1.0", "uuid-parse": "^1.0.0", diff --git a/server/lib/backup.js b/server/lib/backup.js new file mode 100644 index 0000000000..8137cc05fb --- /dev/null +++ b/server/lib/backup.js @@ -0,0 +1,188 @@ +/** + * @overview backup + * + * @description + * This file contains a collection of tools to automate backups of the BHIMA + * database. + */ + +const s3 = require('s3-client'); +const debug = require('debug')('backups'); +const tmp = require('tempy'); +const util = require('./util'); +const lzma = require('lzma-native'); +const streamToPromise = require('stream-to-promise'); +const fs = require('fs'); +const moment = require('moment'); +const q = require('q'); + +const client = s3.createClient({ + s3Options : { + accessKeyId : process.env.S3_ACCESS_KEY_ID, + secretAccessKey : process.env.S3_SECRET_ACCESS_KEY, + }, +}); + +/** + * @method backup + * + * @description + * This function runs all the backup functions in order from dump to upload. It + * should probably be tested live in production to see if this is actually + * something we want to do before calling it all the time. + */ +function backup(filename) { + const file = filename || tmp.file({ extension : '.sql' }); + + debug(`#backup() beginning backup routine.`); + + return mysqldump(file) + .then(() => xz(file)) + .then(upload); +} + +/** + * @function mysqldump + * + * @description + * This function runs mysqldump on the database with provided options. There is + * a switch to allow the user to dump the schema as necessary. + */ +function mysqldump(file, options = {}) { + const cmd = `mysqldump %s > ${file}`; + + debug(`#mysqldump() dumping database ${options.includeSchema ? 'with' : 'without'} schema.`); + + // this is an array to make it easy to add or remove options + const flags = [ + `--user=${process.env.DB_USER}`, + `-p${process.env.DB_PASS}`, + `--databases ${process.env.DB_NAME}`, + + // compress information between the client and server in case we are on a + // networked database. + '--compress', + + // wrap everything in a START TRANSACTION and COMMIT at the end. + '--single-transaction', + + // preserve UTF-8 names in the database dump. These can be removed manually + // if we need to remove it. + '--set-charset', + + // make sure binary data is dumped out as hexadecimal. + '--hex-blob', + + // retrieve rows one row at a time instead of buffering the entire table in memory + '--quick', + + // do not pollute the dump with comments + '--skip-comments', + + // show every column name in the INSERT statements. This helps with later + // database migrations. + '--complete-insert', + + // speed up the dump and rebuild of the database. + '--disable-keys', + + // building the dump twice should produce no side-effects. + '--add-drop-database', + '--add-drop-table', + ]; + + // do not dump schemas by default. If we want create info, we can turn it on. + if (!options.includeSchema) { + flags.push('--no-create-info'); + } + + if (options.includeSchema) { + flags.push('--routines'); + } + + const program = util.format(cmd, flags.join(' ')); + return util.execp(program); +} + +/** + * @function xz + * + * @description + * This function uses the lzma-native library for ultra-fast compression of the + * backup file. Since streams are used, the memory requirements should stay + * relatively low. + */ +function xz(file) { + const outfile = `${file}.xz`; + + debug(`#xz() compressing ${file} into ${outfile}.`); + + const compressor = lzma.createCompressor(); + const input = fs.createReadStream(file); + const output = fs.createWriteStream(outfile); + + let beforeSizeInMegabytes; + let afterSizeInMegabytes; + + return util.statp(file) + .then(stats => { + beforeSizeInMegabytes = stats.size / 1000000.0; + debug(`#xz() ${file} is ${beforeSizeInMegabytes}MB`); + + // start the compresion + const streams = input.pipe(compressor).pipe(output); + return streamToPromise(streams); + }) + .then(() => util.statp(outfile)) + .then(stats => { + afterSizeInMegabytes = stats.size / 1000000.0; + debug(`#xz() ${outfile} is ${afterSizeInMegabytes}MB`); + + const ratio = + Number(beforeSizeInMegabytes / afterSizeInMegabytes).toFixed(2); + + debug(`#xz() compression ratio: ${ratio}`); + + return outfile; + }); +} + +/** + * @method upload + * + * @description + * This function uploads a file to Amazon S3 storage. + */ +function upload(file, options = {}) { + debug(`#upload() uploading backup file ${file} to Amazon S3.`); + + if (!options.name) { + options.name = + `${process.env.DB_NAME}-${moment().format('YYYY-MM-DD')}.sql.xz`; + } + + const params = { + localFile : file, + s3Params : { + Bucket : process.env.S3_BUCKET_NAME, + Key : options.name, + }, + }; + + const deferred = q.defer(); + + const uploader = client.uploadFile(params); + + uploader.on('error', deferred.reject); + uploader.on('end', deferred.resolve); + + return deferred.promise + .then((tags) => { + debug(`#upload() upload completed. Resource ETag: ${tags.ETag}`); + }); +} + +exports.backup = backup; +exports.mysqldump = mysqldump; +exports.upload = upload; +exports.xz = xz; diff --git a/server/lib/util.js b/server/lib/util.js index 1757b599fb..d18f817a14 100644 --- a/server/lib/util.js +++ b/server/lib/util.js @@ -10,17 +10,25 @@ * @requires q * @requires moment * @requires debug + * @requires child_process + * @requires util */ const _ = require('lodash'); const q = require('q'); const moment = require('moment'); const debug = require('debug')('util'); +const { exec } = require('child_process'); +const fs = require('fs'); module.exports.take = take; module.exports.loadModuleIfExists = requireModuleIfExists; exports.dateFormatter = dateFormatter; exports.resolveObject = resolveObject; +exports.execp = execp; +exports.unlinkp = unlinkp; +exports.statp = statp; +exports.format = require('util').format; /** * @function take @@ -118,3 +126,56 @@ function dateFormatter(rows, dateFormat) { return rows; } + +/** + * @method execp + * + * @description + * This method promisifies the child process exec() function. It is used in + * lib/backup.js, but will likely be handy in other places as well. + */ +function execp(cmd) { + debug(`#execp(): ${cmd}`); + const deferred = q.defer(); + const child = exec(cmd); + child.addListener('error', deferred.reject); + child.addListener('exit', deferred.resolve); + return deferred.promise; +} + +/** + * @method statp + * + * @description + * This method promisifies the stats method. + */ +function statp(file) { + debug(`#statp(): ${file}`); + const deferred = q.defer(); + + fs.stat(file, (err, stats) => { + if (err) { return deferred.reject(err); } + return deferred.resolve(stats); + }); + + return deferred.promise; +} + + +/** + * @method statp + * + * @description + * This method promisifies the unlink method. + */ +function unlinkp(file) { + debug(`#unlinkp(): ${file}`); + const deferred = q.defer(); + + fs.unlink(file, (err) => { + if (err) { return deferred.reject(err); } + return deferred.resolve(); + }); + + return deferred.promise; +} diff --git a/yarn.lock b/yarn.lock index ed714f13ae..4a39adc25e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -315,6 +315,21 @@ autoprefixer@^6.3.1: postcss "^5.2.16" postcss-value-parser "^3.2.3" +aws-sdk@~2.140.0: + version "2.140.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.140.0.tgz#c15fc5c3db7a1805c59f85767eb1067ae34f9ae4" + dependencies: + buffer "4.9.1" + crypto-browserify "1.0.9" + events "^1.1.1" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.1.0" + xml2js "0.4.17" + xmlbuilder "4.2.1" + aws-sign2@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" @@ -355,6 +370,10 @@ base64-js@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.2.tgz#024f0f72afa25b75f9c0ee73cd4f55ec1bed9784" +base64-js@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" + base64id@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6" @@ -514,6 +533,14 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: caniuse-db "^1.0.30000639" electron-to-chromium "^1.2.7" +buffer@4.9.1: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -1207,6 +1234,14 @@ cryptiles@3.x.x: dependencies: boom "5.x.x" +crypto-browserify@1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-1.0.9.tgz#cc5449685dfb85eb11c9828acc7cb87ab5bbfcc0" + +crypto-random-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" + css-color-names@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" @@ -1790,6 +1825,10 @@ eventemitter3@1.x.x: version "1.2.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" +events@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + excel-export@~0.3.11: version "0.3.11" resolved "https://registry.yarnpkg.com/excel-export/-/excel-export-0.3.11.tgz#6bf6b9d65555ebe3f0daeaa5732f7b69986a7eb4" @@ -1975,6 +2014,12 @@ fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" +fd-slicer@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" + dependencies: + pend "~1.2.0" + figures@^1.3.5, figures@^1.5.0: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -2065,6 +2110,10 @@ find-up@^2.0.0: dependencies: locate-path "^2.0.0" +findit2@~2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/findit2/-/findit2-2.2.3.tgz#58a466697df8a6205cdfdbf395536b8bd777a5f6" + findup-sync@0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.4.2.tgz#a8117d0f73124f5a4546839579fe52d7129fb5e5" @@ -2519,7 +2568,7 @@ graceful-fs@^3.0.0: dependencies: natives "^1.1.0" -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@~4.1.11: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -2902,6 +2951,10 @@ iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@^0.4.4: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" +ieee754@^1.1.4: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + ienoopen@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.0.0.tgz#346a428f474aac8f50cf3784ea2d0f16f62bda6b" @@ -3308,6 +3361,10 @@ jasminewd2@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/jasminewd2/-/jasminewd2-2.2.0.tgz#e37cf0b17f199cce23bea71b2039395246b4ec4e" +jmespath@0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + js-base64@^2.1.9: version "2.3.2" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.2.tgz#a79a923666372b580f8e27f51845c6f7e8fbfbaf" @@ -3863,6 +3920,15 @@ lru-cache@^4.0.0, lru-cache@^4.0.1: pseudomap "^1.0.2" yallist "^2.1.2" +lzma-native@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lzma-native/-/lzma-native-3.0.4.tgz#1fe863dbbbaa58f8132dfcf7df261ec9af6f2b72" + dependencies: + nan "2.5.1" + node-pre-gyp "^0.6.39" + readable-stream "^2.0.5" + rimraf "^2.6.1" + macaddress@^0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" @@ -3976,7 +4042,7 @@ mime@1.2.11, mime@~1.2.11: version "1.2.11" resolved "https://registry.yarnpkg.com/mime/-/mime-1.2.11.tgz#58203eed86e3a5ef17aed2b7d9ebd47f0a60dd10" -mime@1.4.1, mime@^1.2.11, mime@^1.3.4: +mime@1.4.1, mime@^1.2.11, mime@^1.3.4, mime@~1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" @@ -4140,6 +4206,10 @@ mysql@^2.14.0: safe-buffer "5.1.1" sqlstring "2.3.0" +nan@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2" + nan@^2.3.0: version "2.8.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" @@ -4614,6 +4684,10 @@ pathval@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" @@ -5364,7 +5438,7 @@ right-pad@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/right-pad/-/right-pad-1.0.1.tgz#8ca08c2cbb5b55e74dafa96bf7fd1a27d568c8d0" -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1: +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@~2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: @@ -5390,6 +5464,20 @@ rx@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" +s3-client@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/s3-client/-/s3-client-4.4.1.tgz#c41f65e17a38e95dd8294b135aacefde431d10ff" + dependencies: + aws-sdk "~2.140.0" + fd-slicer "~1.0.1" + findit2 "~2.2.3" + graceful-fs "~4.1.11" + mime "~1.4.1" + mkdirp "~0.5.1" + pend "~1.2.0" + rimraf "~2.6.2" + streamsink "~1.2.0" + safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -5404,6 +5492,10 @@ sax@0.6.x: version "0.6.1" resolved "https://registry.yarnpkg.com/sax/-/sax-0.6.1.tgz#563b19c7c1de892e09bfc4f2fc30e3c27f0952b9" +sax@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + sax@>=0.6.0, sax@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -5893,6 +5985,10 @@ streamsearch@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" +streamsink@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/streamsink/-/streamsink-1.2.0.tgz#efafee9f1e22d3591ed7de3dcaa95c3f5e79f73c" + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" @@ -6091,6 +6187,10 @@ tar@^2.2.1: fstream "^1.0.2" inherits "2" +temp-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" + tempfile@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/tempfile/-/tempfile-1.1.1.tgz#5bcc4eaecc4ab2c707d8bc11d99ccc9a2cb287f2" @@ -6098,6 +6198,13 @@ tempfile@^1.1.1: os-tmpdir "^1.0.0" uuid "^2.0.1" +tempy@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tempy/-/tempy-0.2.1.tgz#9038e4dbd1c201b74472214179bc2c6f7776e54c" + dependencies: + temp-dir "^1.0.0" + unique-string "^1.0.0" + ternary-stream@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/ternary-stream/-/ternary-stream-2.0.1.tgz#064e489b4b5bf60ba6a6b7bc7f2f5c274ecf8269" @@ -6318,6 +6425,12 @@ unique-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-1.0.0.tgz#d59a4a75427447d9aa6c91e70263f8d26a4b104b" +unique-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" + dependencies: + crypto-random-string "^1.0.0" + universalify@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7" @@ -6359,6 +6472,13 @@ url-parse-lax@^1.0.0: dependencies: prepend-http "^1.0.1" +url@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -6399,14 +6519,14 @@ uuid-parse@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.0.0.tgz#f4657717624b0e4b88af36f98d89589a5bbee569" +uuid@3.1.0, uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" + uuid@^2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" -uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" - v8flags@^2.0.2: version "2.1.1" resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4" @@ -6657,6 +6777,13 @@ xdg-basedir@^2.0.0: dependencies: os-homedir "^1.0.0" +xml2js@0.4.17: + version "0.4.17" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.17.tgz#17be93eaae3f3b779359c795b419705a8817e868" + dependencies: + sax ">=0.6.0" + xmlbuilder "^4.1.0" + xml2js@0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.4.tgz#3111010003008ae19240eba17497b57c729c555d" @@ -6671,6 +6798,12 @@ xml2js@^0.4.17: sax ">=0.6.0" xmlbuilder "~9.0.1" +xmlbuilder@4.2.1, xmlbuilder@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-4.2.1.tgz#aa58a3041a066f90eaa16c2f5389ff19f3f461a5" + dependencies: + lodash "^4.0.0" + xmlbuilder@>=1.0.0, xmlbuilder@~9.0.1: version "9.0.4" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.4.tgz#519cb4ca686d005a8420d3496f3f0caeecca580f"