Skip to content

Commit 9a865d6

Browse files
committed
feat: Implemented own downloader, removed mongodb-download package
- Better performance (less checks) - Lower disk space consumption (better for caching on CI)
1 parent a39eca5 commit 9a865d6

9 files changed

+1187
-389
lines changed

.eslintrc

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"singleQuote": true,
2424
"trailingComma": "es5",
2525
}],
26-
"no-prototype-builtins": 0
26+
"no-prototype-builtins": 0,
27+
"prefer-destructuring": 0
2728
},
2829
"env": {
2930
"jasmine": true,

package.json

+22-19
Original file line numberDiff line numberDiff line change
@@ -23,37 +23,40 @@
2323
},
2424
"homepage": "https://github.com/nodkz/mongodb-memory-server",
2525
"devDependencies": {
26-
"babel-cli": "^6.22.2",
27-
"babel-eslint": "^7.1.1",
28-
"babel-jest": "^20.0.3",
26+
"babel-cli": "^6.26.0",
27+
"babel-eslint": "^8.0.0",
28+
"babel-jest": "^21.0.2",
2929
"babel-plugin-transform-class-properties": "^6.24.1",
3030
"babel-plugin-transform-flow-strip-types": "^6.22.0",
31-
"babel-plugin-transform-object-rest-spread": "^6.22.0",
31+
"babel-plugin-transform-object-rest-spread": "^6.26.0",
3232
"babel-plugin-transform-runtime": "^6.23.0",
3333
"babel-preset-env": "^1.5.2",
3434
"cz-conventional-changelog": "^2.0.0",
35-
"eslint": "^4.0.0",
36-
"eslint-config-airbnb-base": "^11.2.0",
37-
"eslint-config-prettier": "^2.1.1",
38-
"eslint-plugin-flowtype": "^2.34.0",
35+
"eslint": "^4.6.1",
36+
"eslint-config-airbnb-base": "^12.0.0",
37+
"eslint-config-prettier": "^2.4.0",
38+
"eslint-plugin-flowtype": "^2.35.1",
3939
"eslint-plugin-import": "^2.3.0",
4040
"eslint-plugin-prettier": "^2.1.1",
41-
"flow-bin": "^0.53.0",
42-
"jest": "^20.0.4",
41+
"flow-bin": "^0.54.1",
42+
"jest": "^21.1.0",
4343
"mongodb": "^2.2.28",
44-
"npm-run-all": "^4.0.1",
45-
"prettier": "^1.4.4",
46-
"rimraf": "^2.5.4",
47-
"semantic-release": "^7.0.1"
44+
"npm-run-all": "^4.1.1",
45+
"prettier": "^1.6.1",
46+
"rimraf": "^2.6.2",
47+
"semantic-release": "^7.0.2"
4848
},
4949
"dependencies": {
5050
"babel-runtime": "^6.26.0",
51-
"debug": "^3.0.0",
52-
"get-port": "^3.1.0",
53-
"glob": "^7.1.2",
51+
"debug": "^3.0.1",
52+
"decompress": "^4.2.0",
53+
"fs-extra": "^4.0.2",
54+
"get-port": "^3.2.0",
55+
"getos": "^3.0.1",
5456
"lockfile": "^1.0.3",
57+
"md5-file": "^3.2.2",
5558
"mkdirp": "^0.5.1",
56-
"mongodb-download": "^2.2.3",
59+
"request-promise": "^4.2.1",
5760
"tmp": "^0.0.33",
5861
"uuid": "^3.0.1"
5962
},
@@ -65,7 +68,7 @@
6568
"watch": "jest --watch",
6669
"coverage": "jest --coverage",
6770
"lint": "eslint --ext .js ./src",
68-
"flow": "./node_modules/.bin/flow stop && ./node_modules/.bin/flow",
71+
"flow": "./node_modules/.bin/flow",
6972
"test": "npm run coverage && npm run lint && npm run flow",
7073
"semantic-release": "semantic-release pre && npm publish && semantic-release post"
7174
},

src/util/MongoBinary.js

+3-20
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
/* @flow */
22

3-
import { MongoDBDownload } from 'mongodb-download';
4-
import glob from 'glob';
53
import os from 'os';
64
import path from 'path';
75
import LockFile from 'lockfile';
86
import mkdirp from 'mkdirp';
7+
import MongoBinaryDownload from './MongoBinaryDownload';
98

109
export type MongoBinaryCache = {
1110
[version: string]: string,
@@ -76,7 +75,7 @@ export default class MongoBinary {
7675

7776
// again check cache, maybe other instance resolve it
7877
if (!this.cache[version]) {
79-
const downloader = new MongoDBDownload({
78+
const downloader = new MongoBinaryDownload({
8079
downloadDir,
8180
platform,
8281
arch,
@@ -85,8 +84,7 @@ export default class MongoBinary {
8584
});
8685

8786
downloader.debug = debug;
88-
const releaseDir = await downloader.downloadAndExtract();
89-
this.cache[version] = await this.findBinPath(releaseDir);
87+
this.cache[version] = await downloader.getMongodPath();
9088
}
9189

9290
// remove lock
@@ -103,21 +101,6 @@ export default class MongoBinary {
103101
return this.cache[version];
104102
}
105103

106-
static findBinPath(releaseDir: string): Promise<string> {
107-
return new Promise((resolve, reject) => {
108-
glob(`${releaseDir}/*/bin`, {}, (err: any, files: string[]) => {
109-
if (err) {
110-
reject(err);
111-
} else if (this.hasValidBinPath(files) === true) {
112-
const resolvedBinPath: string = files[0];
113-
resolve(resolvedBinPath);
114-
} else {
115-
reject(`path not found`);
116-
}
117-
});
118-
});
119-
}
120-
121104
static hasValidBinPath(files: string[]): boolean {
122105
if (files.length === 1) {
123106
return true;

src/util/MongoBinaryDownload.js

+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/* @flow */
2+
/* eslint-disable class-methods-use-this */
3+
4+
import os from 'os';
5+
import path from 'path';
6+
import fs from 'fs-extra';
7+
import request from 'request-promise';
8+
import md5File from 'md5-file';
9+
import https from 'https';
10+
import decompress from 'decompress';
11+
import MongoBinaryDownloadUrl from './MongoBinaryDownloadUrl';
12+
13+
export type MongoBinaryDownloadOpts = {
14+
version: string,
15+
downloadDir: string,
16+
platform: string,
17+
arch: string,
18+
http: any,
19+
debug?: boolean | Function,
20+
};
21+
22+
type dlProgress = {
23+
current: number,
24+
length: number,
25+
totalMb: number,
26+
lastPrintedAt: number,
27+
};
28+
29+
export default class MongoBinaryDownload {
30+
debug: Function;
31+
dlProgress: dlProgress;
32+
33+
downloadDir: string;
34+
arch: string;
35+
version: string;
36+
platform: string;
37+
http: any;
38+
39+
constructor({
40+
platform,
41+
arch,
42+
downloadDir,
43+
version,
44+
http,
45+
debug,
46+
}: $Shape<MongoBinaryDownloadOpts>) {
47+
this.platform = platform || os.platform();
48+
this.arch = arch || os.arch();
49+
this.version = version || 'latest';
50+
this.http = http || {};
51+
this.downloadDir = path.resolve(downloadDir || 'mongodb-download');
52+
this.dlProgress = {
53+
current: 0,
54+
length: 0,
55+
totalMb: 0,
56+
lastPrintedAt: 0,
57+
};
58+
59+
if (debug) {
60+
if (debug.call && typeof debug === 'function' && debug.apply) {
61+
this.debug = debug;
62+
} else {
63+
this.debug = console.log.bind(null);
64+
}
65+
} else {
66+
this.debug = () => {};
67+
}
68+
}
69+
70+
async getMongodPath(): Promise<string> {
71+
const mongodPath = path.resolve(this.downloadDir, this.version, 'mongod');
72+
if (this.locationExists(mongodPath)) {
73+
return mongodPath;
74+
}
75+
76+
const mongoDBArchive = await this.download();
77+
await this.extract(mongoDBArchive);
78+
fs.unlinkSync(mongoDBArchive);
79+
80+
if (this.locationExists(mongodPath)) {
81+
return mongodPath;
82+
}
83+
84+
throw new Error(`Cannot find downloaded mongod binary by path ${mongodPath}`);
85+
}
86+
87+
async download(): Promise<string> {
88+
const mbdUrl = new MongoBinaryDownloadUrl({
89+
platform: this.platform,
90+
arch: this.arch,
91+
version: this.version,
92+
});
93+
94+
await fs.ensureDir(this.downloadDir);
95+
const url = await mbdUrl.getDownloadUrl();
96+
const archName = await mbdUrl.getArchiveName();
97+
const downloadLocation = path.resolve(this.downloadDir, archName);
98+
console.log('Downloading MongoDB:', url);
99+
const tempDownloadLocation = path.resolve(this.downloadDir, `${archName}.downloading`);
100+
const mongoDBArchive = await this.httpDownload(url, downloadLocation, tempDownloadLocation);
101+
const md5Remote = await this.downloadMD5(`${url}.md5`);
102+
const md5Local = md5File.sync(mongoDBArchive);
103+
if (md5Remote !== md5Local) {
104+
throw new Error('MongoBinaryDownload: md5 check is failed');
105+
}
106+
return mongoDBArchive;
107+
}
108+
109+
async downloadMD5(md5url: string): Promise<string> {
110+
const signatureContent = await request(md5url);
111+
this.debug(`getDownloadMD5Hash content: ${signatureContent}`);
112+
const signature = signatureContent.match(/(.*?)\s/)[1];
113+
this.debug(`getDownloadMD5Hash extracted signature: ${signature}`);
114+
return signature;
115+
}
116+
117+
async extract(mongoDBArchive: string): Promise<string> {
118+
const extractDir = path.resolve(this.downloadDir, this.version);
119+
this.debug(`extract(): ${extractDir}`);
120+
await fs.ensureDir(extractDir);
121+
await decompress(mongoDBArchive, extractDir, {
122+
// extract only `bin/mongod` file
123+
filter: file => /bin\/mongod$/.test(file.path),
124+
// extract to root folder
125+
map: file => {
126+
file.path = path.basename(file.path); // eslint-disable-line
127+
return file;
128+
},
129+
});
130+
131+
if (!this.locationExists(path.resolve(this.downloadDir, this.version, 'mongod'))) {
132+
throw new Error(`MongoBinaryDownload: missing mongod binary in ${mongoDBArchive}`);
133+
}
134+
return extractDir;
135+
}
136+
137+
async httpDownload(
138+
httpOptions: any,
139+
downloadLocation: string,
140+
tempDownloadLocation: string
141+
): Promise<string> {
142+
return new Promise((resolve, reject) => {
143+
const fileStream = fs.createWriteStream(tempDownloadLocation);
144+
145+
const req: any = https.get(httpOptions, (response: any) => {
146+
this.dlProgress.current = 0;
147+
this.dlProgress.length = parseInt(response.headers['content-length'], 10);
148+
this.dlProgress.totalMb = Math.round(this.dlProgress.length / 1048576 * 10) / 10;
149+
150+
response.pipe(fileStream);
151+
152+
fileStream.on('finish', () => {
153+
fileStream.close(() => {
154+
fs.renameSync(tempDownloadLocation, downloadLocation);
155+
this.debug(`renamed ${tempDownloadLocation} to ${downloadLocation}`);
156+
resolve(downloadLocation);
157+
});
158+
});
159+
160+
response.on('data', (chunk: any) => {
161+
this.printDownloadProgress(chunk);
162+
});
163+
164+
req.on('error', (e: any) => {
165+
this.debug('request error:', e);
166+
reject(e);
167+
});
168+
});
169+
});
170+
}
171+
172+
printDownloadProgress(chunk: *): void {
173+
this.dlProgress.current += chunk.length;
174+
175+
const now = Date.now();
176+
if (now - this.dlProgress.lastPrintedAt < 2000) return;
177+
this.dlProgress.lastPrintedAt = now;
178+
179+
const percentComplete =
180+
Math.round(100.0 * this.dlProgress.current / this.dlProgress.length * 10) / 10;
181+
const mbComplete = Math.round(this.dlProgress.current / 1048576 * 10) / 10;
182+
183+
const crReturn = this.platform === 'win32' ? '\x1b[0G' : '\r';
184+
process.stdout.write(
185+
`Downloading MongoDB ${this.version}: ${percentComplete} % (${mbComplete}mb / ${this
186+
.dlProgress.totalMb}mb)${crReturn}`
187+
);
188+
}
189+
190+
locationExists(location: string): boolean {
191+
try {
192+
fs.lstatSync(location);
193+
return true;
194+
} catch (e) {
195+
if (e.code !== 'ENOENT') throw e;
196+
return false;
197+
}
198+
}
199+
}

0 commit comments

Comments
 (0)