/* @flow */ /* eslint-disable class-methods-use-this */ import os from 'os'; import url from 'url'; import path from 'path'; import fs from 'fs'; import md5File from 'md5-file'; import https from 'https'; import HttpsProxyAgent from 'https-proxy-agent'; import decompress from 'decompress'; import MongoBinaryDownloadUrl from './MongoBinaryDownloadUrl'; import type { DebugFn, DebugPropT, DownloadProgressT } from '../types'; export type MongoBinaryDownloadOpts = { version: string, downloadDir: string, platform: string, arch: string, debug?: DebugPropT, }; export default class MongoBinaryDownload { debug: DebugFn; dlProgress: DownloadProgressT; downloadDir: string; arch: string; version: string; platform: string; constructor({ platform, arch, downloadDir, version, debug }: $Shape) { this.platform = platform || os.platform(); this.arch = arch || os.arch(); this.version = version || 'latest'; this.downloadDir = path.resolve(downloadDir || 'mongodb-download'); this.dlProgress = { current: 0, length: 0, totalMb: 0, lastPrintedAt: 0, }; if (debug) { if (debug.call && typeof debug === 'function' && debug.apply) { this.debug = debug; } else { this.debug = console.log.bind(null); } } else { this.debug = () => {}; } } async getMongodPath(): Promise { const binaryName = this.platform === 'win32' ? 'mongod.exe' : 'mongod'; const mongodPath = path.resolve(this.downloadDir, this.version, binaryName); if (this.locationExists(mongodPath)) { return mongodPath; } const mongoDBArchive = await this.startDownload(); await this.extract(mongoDBArchive); fs.unlinkSync(mongoDBArchive); if (this.locationExists(mongodPath)) { return mongodPath; } throw new Error(`Cannot find downloaded mongod binary by path ${mongodPath}`); } async startDownload(): Promise { const mbdUrl = new MongoBinaryDownloadUrl({ platform: this.platform, arch: this.arch, version: this.version, }); if (!fs.existsSync(this.downloadDir)) { fs.mkdirSync(this.downloadDir); } const downloadUrl = await mbdUrl.getDownloadUrl(); const mongoDBArchive = await this.download(downloadUrl); const mongoDBArchiveMd5 = await this.download(`${downloadUrl}.md5`); await this.checkMd5(mongoDBArchiveMd5, mongoDBArchive); return mongoDBArchive; } async checkMd5(mongoDBArchiveMd5: string, mongoDBArchive: string) { const signatureContent = fs.readFileSync(mongoDBArchiveMd5).toString('UTF-8'); const m = signatureContent.match(/(.*?)\s/); const md5Remote = m ? m[1] : null; const md5Local = md5File.sync(mongoDBArchive); if (md5Remote !== md5Local) { throw new Error('MongoBinaryDownload: md5 check is failed'); } } async download(downloadUrl: string) { 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; const urlObject = url.parse(downloadUrl); const downloadOptions = { hostname: urlObject.hostname, port: urlObject.port || 443, path: urlObject.path, method: 'GET', agent: proxy ? new HttpsProxyAgent(proxy) : undefined, }; const filename = (urlObject.pathname || '').split('/').pop(); if (!filename) { throw new Error(`MongoBinaryDownload: missing filename for url ${downloadUrl}`); } const downloadLocation = path.resolve(this.downloadDir, filename); const tempDownloadLocation = path.resolve(this.downloadDir, `${filename}.downloading`); this.debug(`Downloading${proxy ? ` via proxy ${proxy}` : ''}:`, downloadUrl); const downloadedFile = await this.httpDownload( downloadOptions, downloadLocation, tempDownloadLocation ); return downloadedFile; } async extract(mongoDBArchive: string): Promise { const binaryName = this.platform === 'win32' ? 'mongod.exe' : 'mongod'; const extractDir = path.resolve(this.downloadDir, this.version); this.debug(`extract(): ${extractDir}`); if (!fs.existsSync(extractDir)) { fs.mkdirSync(extractDir); } let filter; if (this.platform === 'win32') { filter = file => { return /bin\/mongod.exe$/.test(file.path) || /.dll$/.test(file.path); }; } else { filter = file => /bin\/mongod$/.test(file.path); } await decompress(mongoDBArchive, extractDir, { // extract only `bin/mongod` file filter, // extract to root folder map: file => { file.path = path.basename(file.path); // eslint-disable-line return file; }, }); if (!this.locationExists(path.resolve(this.downloadDir, this.version, binaryName))) { throw new Error(`MongoBinaryDownload: missing mongod binary in ${mongoDBArchive}`); } return extractDir; } async httpDownload( httpOptions: any, downloadLocation: string, tempDownloadLocation: string ): Promise { return new Promise((resolve, reject) => { const fileStream = fs.createWriteStream(tempDownloadLocation); const req: any = https.get(httpOptions, (response: any) => { this.dlProgress.current = 0; this.dlProgress.length = parseInt(response.headers['content-length'], 10); this.dlProgress.totalMb = Math.round((this.dlProgress.length / 1048576) * 10) / 10; response.pipe(fileStream); fileStream.on('finish', () => { fileStream.close(); fs.renameSync(tempDownloadLocation, downloadLocation); this.debug(`renamed ${tempDownloadLocation} to ${downloadLocation}`); resolve(downloadLocation); }); response.on('data', (chunk: any) => { this.printDownloadProgress(chunk); }); req.on('error', (e: any) => { this.debug('request error:', e); reject(e); }); }); }); } printDownloadProgress(chunk: *): void { this.dlProgress.current += chunk.length; const now = Date.now(); if (now - this.dlProgress.lastPrintedAt < 2000) 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'; process.stdout.write( `Downloading MongoDB ${this.version}: ${percentComplete} % (${mbComplete}mb ` + `/ ${this.dlProgress.totalMb}mb)${crReturn}` ); } locationExists(location: string): boolean { try { fs.lstatSync(location); return true; } catch (e) { if (e.code !== 'ENOENT') throw e; return false; } } }