UNPKG

mongodb-memory-server-core

Version:

MongoDB Server for testing (core package, without autodownload). The server will allow you to connect your favourite ODM or client library to the MongoDB Server and run parallel integration tests isolated from each other.

401 lines 19.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MongoBinaryDownload = void 0; const tslib_1 = require("tslib"); const os_1 = (0, tslib_1.__importDefault)(require("os")); const url_1 = require("url"); const path_1 = (0, tslib_1.__importDefault)(require("path")); const fs_1 = require("fs"); const md5_file_1 = (0, tslib_1.__importDefault)(require("md5-file")); const https_1 = (0, tslib_1.__importDefault)(require("https")); const zlib_1 = require("zlib"); const tar_stream_1 = (0, tslib_1.__importDefault)(require("tar-stream")); const yauzl_1 = (0, tslib_1.__importDefault)(require("yauzl")); const MongoBinaryDownloadUrl_1 = (0, tslib_1.__importDefault)(require("./MongoBinaryDownloadUrl")); const https_proxy_agent_1 = require("https-proxy-agent"); const resolveConfig_1 = (0, tslib_1.__importStar)(require("./resolveConfig")); const debug_1 = (0, tslib_1.__importDefault)(require("debug")); const utils_1 = require("./utils"); const DryMongoBinary_1 = require("./DryMongoBinary"); const readline_1 = require("readline"); const errors_1 = require("./errors"); const log = (0, debug_1.default)('MongoMS:MongoBinaryDownload'); /** * Download and extract the "mongod" binary */ class MongoBinaryDownload { // end get/set backwards compat section constructor(opts) { var _a, _b, _c, _d, _e, _f; (0, utils_1.assertion)(typeof opts.downloadDir === 'string', new Error('An DownloadDir must be specified!')); const version = (_a = opts.version) !== null && _a !== void 0 ? _a : (0, resolveConfig_1.default)(resolveConfig_1.ResolveConfigVariables.VERSION); (0, utils_1.assertion)(typeof version === 'string', new Error('An MongoDB Binary version must be specified!')); // DryMongoBinary.generateOptions cannot be used here, because its async this.binaryOpts = { platform: (_b = opts.platform) !== null && _b !== void 0 ? _b : os_1.default.platform(), arch: (_c = opts.arch) !== null && _c !== void 0 ? _c : os_1.default.arch(), version: version, downloadDir: opts.downloadDir, checkMD5: (_d = opts.checkMD5) !== null && _d !== void 0 ? _d : (0, resolveConfig_1.envToBool)((0, resolveConfig_1.default)(resolveConfig_1.ResolveConfigVariables.MD5_CHECK)), systemBinary: (_e = opts.systemBinary) !== null && _e !== void 0 ? _e : '', os: (_f = opts.os) !== null && _f !== void 0 ? _f : { os: 'unknown' }, }; this.dlProgress = { current: 0, length: 0, totalMb: 0, lastPrintedAt: 0, }; } // TODO: for an major version, remove the compat get/set // the following get/set are to not break existing stuff get checkMD5() { return this.binaryOpts.checkMD5; } set checkMD5(val) { this.binaryOpts.checkMD5 = val; } get downloadDir() { return this.binaryOpts.downloadDir; } set downloadDir(val) { this.binaryOpts.downloadDir = val; } get arch() { return this.binaryOpts.arch; } set arch(val) { this.binaryOpts.arch = val; } get version() { return this.binaryOpts.version; } set version(val) { this.binaryOpts.version = val; } get platform() { return this.binaryOpts.platform; } set platform(val) { this.binaryOpts.platform = val; } /** * Get the full path with filename * @returns Absoulte Path with FileName */ getPath() { return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { const opts = yield DryMongoBinary_1.DryMongoBinary.generateOptions(this.binaryOpts); return DryMongoBinary_1.DryMongoBinary.combineBinaryName(this.downloadDir, yield DryMongoBinary_1.DryMongoBinary.getBinaryName(opts)); }); } /** * Get the path of the already downloaded "mongod" file * otherwise download it and then return the path */ getMongodPath() { return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { log('getMongodPath'); const mongodPath = yield this.getPath(); if (yield (0, utils_1.pathExists)(mongodPath)) { log(`getMongodPath: mongod path "${mongodPath}" already exists, using this`); return mongodPath; } const mongoDBArchive = yield this.startDownload(); yield this.extract(mongoDBArchive); yield fs_1.promises.unlink(mongoDBArchive); if (yield (0, utils_1.pathExists)(mongodPath)) { return mongodPath; } throw new Error(`Cannot find downloaded mongod binary by path "${mongodPath}"`); }); } /** * Download the MongoDB Archive and check it against an MD5 * @returns The MongoDB Archive location */ startDownload() { return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { log('startDownload'); const mbdUrl = new MongoBinaryDownloadUrl_1.default(this.binaryOpts); yield (0, utils_1.mkdir)(this.downloadDir); try { yield fs_1.promises.access(this.downloadDir, fs_1.constants.X_OK | fs_1.constants.W_OK); // check that this process has permissions to create files & modify file contents & read file contents } catch (err) { console.error(`Download Directory at "${this.downloadDir}" does not have sufficient permissions to be used by this process\n` + 'Needed Permissions: Write & Execute (-wx)\n'); throw err; } const downloadUrl = yield mbdUrl.getDownloadUrl(); const mongoDBArchive = yield this.download(downloadUrl); yield this.makeMD5check(`${downloadUrl}.md5`, mongoDBArchive); return mongoDBArchive; }); } /** * Download MD5 file and check it against the MongoDB Archive * @param urlForReferenceMD5 URL to download the MD5 * @param mongoDBArchive The MongoDB Archive file location * * @returns {undefined} if "checkMD5" is falsey * @returns {true} if the md5 check was successful * @throws if the md5 check failed */ makeMD5check(urlForReferenceMD5, mongoDBArchive) { return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { log('makeMD5check: Checking MD5 of downloaded binary...'); if (!this.checkMD5) { log('makeMD5check: checkMD5 is disabled'); return undefined; } const archiveMD5Path = yield this.download(urlForReferenceMD5); const signatureContent = (yield fs_1.promises.readFile(archiveMD5Path)).toString('utf-8'); const regexMatch = signatureContent.match(/^\s*([\w\d]+)\s*/i); const md5SigRemote = regexMatch ? regexMatch[1] : null; const md5SigLocal = md5_file_1.default.sync(mongoDBArchive); log(`makeMD5check: Local MD5: ${md5SigLocal}, Remote MD5: ${md5SigRemote}`); if (md5SigRemote !== md5SigLocal) { throw new errors_1.Md5CheckFailedError(md5SigLocal, md5SigRemote || 'unknown'); } yield fs_1.promises.unlink(archiveMD5Path); return true; }); } /** * Download file from downloadUrl * @param downloadUrl URL to download a File * @returns The Path to the downloaded archive file */ download(downloadUrl) { return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { log('download'); const proxy = process.env['yarn_https-proxy'] || process.env.yarn_proxy || process.env['npm_config_https-proxy'] || process.env.npm_config_proxy || process.env.https_proxy || process.env.http_proxy || process.env.HTTPS_PROXY || process.env.HTTP_PROXY; const strictSsl = process.env.npm_config_strict_ssl === 'true'; const urlObject = new url_1.URL(downloadUrl); urlObject.port = urlObject.port || '443'; const requestOptions = { method: 'GET', rejectUnauthorized: strictSsl, protocol: (0, resolveConfig_1.envToBool)((0, resolveConfig_1.default)(resolveConfig_1.ResolveConfigVariables.USE_HTTP)) ? 'http:' : 'https:', agent: proxy ? new https_proxy_agent_1.HttpsProxyAgent(proxy) : undefined, }; const filename = urlObject.pathname.split('/').pop(); if (!filename) { throw new Error(`MongoBinaryDownload: missing filename for url "${downloadUrl}"`); } const downloadLocation = path_1.default.resolve(this.downloadDir, filename); const tempDownloadLocation = path_1.default.resolve(this.downloadDir, `${filename}.downloading`); log(`download: Downloading${proxy ? ` via proxy "${proxy}"` : ''}: "${downloadUrl}"`); if (yield (0, utils_1.pathExists)(downloadLocation)) { log('download: Already downloaded archive found, skipping download'); return downloadLocation; } this.assignDownloadingURL(urlObject); const downloadedFile = yield this.httpDownload(urlObject, requestOptions, downloadLocation, tempDownloadLocation); return downloadedFile; }); } /** * Extract given Archive * @param mongoDBArchive Archive location * @returns extracted directory location */ extract(mongoDBArchive) { var _a, _b; return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { log('extract'); const mongodbFullPath = yield this.getPath(); log(`extract: archive: "${mongoDBArchive}" final: "${mongodbFullPath}"`); yield (0, utils_1.mkdir)(path_1.default.dirname(mongodbFullPath)); const filter = (file) => /(?:bin\/(?:mongod(?:\.exe)?))$/i.test(file); if (/(.tar.gz|.tgz)$/.test(mongoDBArchive)) { yield this.extractTarGz(mongoDBArchive, mongodbFullPath, filter); } else if (/.zip$/.test(mongoDBArchive)) { yield this.extractZip(mongoDBArchive, mongodbFullPath, filter); } else { throw new Error(`MongoBinaryDownload: unsupported archive "${mongoDBArchive}" (downloaded from "${(_a = this._downloadingUrl) !== null && _a !== void 0 ? _a : 'unknown'}"). Broken archive from MongoDB Provider?`); } if (!(yield (0, utils_1.pathExists)(mongodbFullPath))) { throw new Error(`MongoBinaryDownload: missing mongod binary in "${mongoDBArchive}" (downloaded from "${(_b = this._downloadingUrl) !== null && _b !== void 0 ? _b : 'unknown'}"). Broken archive from MongoDB Provider?`); } return mongodbFullPath; }); } /** * Extract a .tar.gz archive * @param mongoDBArchive Archive location * @param extractPath Directory to extract to * @param filter Method to determine which files to extract */ extractTarGz(mongoDBArchive, extractPath, filter) { return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { log('extractTarGz'); const extract = tar_stream_1.default.extract(); extract.on('entry', (header, stream, next) => { if (filter(header.name)) { stream.pipe((0, fs_1.createWriteStream)(extractPath, { mode: 0o775, })); } stream.on('end', () => next()); stream.resume(); }); return new Promise((res, rej) => { (0, fs_1.createReadStream)(mongoDBArchive) .on('error', (err) => { rej(new errors_1.GenericMMSError('Unable to open tarball ' + mongoDBArchive + ': ' + err)); }) .pipe((0, zlib_1.createUnzip)()) .on('error', (err) => { rej(new errors_1.GenericMMSError('Error during unzip for ' + mongoDBArchive + ': ' + err)); }) .pipe(extract) .on('error', (err) => { rej(new errors_1.GenericMMSError('Error during untar for ' + mongoDBArchive + ': ' + err)); }) .on('finish', res); }); }); } /** * Extract a .zip archive * @param mongoDBArchive Archive location * @param extractPath Directory to extract to * @param filter Method to determine which files to extract */ extractZip(mongoDBArchive, extractPath, filter) { return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { log('extractZip'); return new Promise((resolve, reject) => { yauzl_1.default.open(mongoDBArchive, { lazyEntries: true }, (e, zipfile) => { if (e || !zipfile) { return reject(e); } zipfile.readEntry(); zipfile.on('end', () => resolve()); zipfile.on('entry', (entry) => { if (!filter(entry.fileName)) { return zipfile.readEntry(); } zipfile.openReadStream(entry, (e, r) => { if (e || !r) { return reject(e); } r.on('end', () => zipfile.readEntry()); r.pipe((0, fs_1.createWriteStream)(extractPath, { mode: 0o775, })); }); }); }); }); }); } /** * Downlaod given httpOptions to tempDownloadLocation, then move it to downloadLocation * @param httpOptions The httpOptions directly passed to https.get * @param downloadLocation The location the File should be after the download * @param tempDownloadLocation The location the File should be while downloading */ httpDownload(url, httpOptions, downloadLocation, tempDownloadLocation) { return (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { log('httpDownload'); const downloadUrl = this.assignDownloadingURL(url); return new Promise((resolve, reject) => { log(`httpDownload: trying to download "${downloadUrl}"`); https_1.default .get(url, httpOptions, (response) => { if (response.statusCode != 200) { if (response.statusCode === 403) { reject(new Error("Status Code is 403 (MongoDB's 404)\n" + "This means that the requested version-platform combination doesn't exist\n" + ` Used Url: "${downloadUrl}"\n` + "Try to use different version 'new MongoMemoryServer({ binary: { version: 'X.Y.Z' } })'\n" + 'List of available versions can be found here:\n' + ' https://www.mongodb.org/dl/linux for Linux\n' + ' https://www.mongodb.org/dl/osx for OSX\n' + ' https://www.mongodb.org/dl/win32 for Windows')); return; } reject(new Error('Status Code isnt 200!')); return; } if (typeof response.headers['content-length'] != 'string') { reject(new Error('Response header "content-length" is empty!')); return; } this.dlProgress.current = 0; this.dlProgress.length = parseInt(response.headers['content-length'], 10); this.dlProgress.totalMb = Math.round((this.dlProgress.length / 1048576) * 10) / 10; const fileStream = (0, fs_1.createWriteStream)(tempDownloadLocation); response.pipe(fileStream); fileStream.on('finish', () => (0, tslib_1.__awaiter)(this, void 0, void 0, function* () { var _a; if (this.dlProgress.current < this.dlProgress.length && !((_a = httpOptions.path) === null || _a === void 0 ? void 0 : _a.endsWith('.md5'))) { reject(new Error(`Too small (${this.dlProgress.current} bytes) mongod binary downloaded from ${downloadUrl}`)); return; } this.printDownloadProgress({ length: 0 }, true); fileStream.close(); yield fs_1.promises.rename(tempDownloadLocation, downloadLocation); log(`httpDownload: moved "${tempDownloadLocation}" to "${downloadLocation}"`); resolve(downloadLocation); })); response.on('data', (chunk) => { this.printDownloadProgress(chunk); }); }) .on('error', (e) => { // log it without having debug enabled console.error(`Couldnt download "${downloadUrl}"!`, e.message); reject(e); }); }); }); } /** * Print the Download Progress to STDOUT * @param chunk A chunk to get the length */ printDownloadProgress(chunk, forcePrint = false) { this.dlProgress.current += chunk.length; const now = Date.now(); if (now - this.dlProgress.lastPrintedAt < 2000 && !forcePrint) { return; } this.dlProgress.lastPrintedAt = now; const percentComplete = Math.round(((100.0 * this.dlProgress.current) / this.dlProgress.length) * 10) / 10; const mbComplete = Math.round((this.dlProgress.current / 1048576) * 10) / 10; const crReturn = this.platform === 'win32' ? '\x1b[0G' : '\r'; const message = `Downloading MongoDB "${this.version}": ${percentComplete}% (${mbComplete}mb / ${this.dlProgress.totalMb}mb)${crReturn}`; if (process.stdout.isTTY) { // if TTY overwrite last line over and over until finished and clear line to avoid residual characters (0, readline_1.clearLine)(process.stdout, 0); // this is because "process.stdout.clearLine" does not exist anymore process.stdout.write(message); } else { console.log(message); } } /** * Helper function to de-duplicate assigning "_downloadingUrl" */ assignDownloadingURL(url) { this._downloadingUrl = url.href; return this._downloadingUrl; } } exports.MongoBinaryDownload = MongoBinaryDownload; exports.default = MongoBinaryDownload; //# sourceMappingURL=MongoBinaryDownload.js.map