Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 63 additions & 38 deletions packages/mongodb-downloader/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/* eslint-disable no-console */
import fetch from 'node-fetch';
import * as tar from 'tar';
import { createHash } from 'crypto';
import { promisify } from 'util';
import { promises as fs, createWriteStream } from 'fs';
import path from 'path';
import decompress from 'decompress';
import { pipeline } from 'stream';
import { pipeline, Transform } from 'stream';
import getDownloadURL from 'mongodb-download-url';
import type {
Options as DownloadOptions,
Expand All @@ -31,6 +32,11 @@ export type MongoDBDownloaderOptions = {
useLockfile: boolean;
/** The options to pass to the download URL lookup. */
downloadOptions?: DownloadOptions;
/**
* A direct URL to a MongoDB tarball to download. If set, `version` and
* `downloadOptions` are ignored and no download URL lookup is performed.
*/
downloadUrl?: string;
Comment on lines +35 to +39

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should think about some different approach for adding this functionality in the downloader: the issue I see here is that while downloadOptions will be ignored, we will still consider them somewhat when returning the DownloadResult metadata, which from the consumer perspective might put you in a weird state where what you download doesn't match the returned options at all

};

export class MongoDBDownloader {
Expand All @@ -39,6 +45,7 @@ export class MongoDBDownloader {
version = '*',
directory,
useLockfile,
downloadUrl,
}: MongoDBDownloaderOptions): Promise<DownloadResult> {
await fs.mkdir(directory, { recursive: true });
const isWindows = ['win32', 'windows'].includes(
Expand All @@ -58,12 +65,20 @@ export class MongoDBDownloader {
versionName = versionName + (isEnterprise ? '-enterprise' : '-community');
}

const downloadTarget = path.resolve(
directory,
`mongodb-${process.platform}-${process.env.DISTRO_ID || 'none'}-${
process.arch
}-${versionName}`.replace(/[^a-zA-Z0-9_-]/g, ''),
);
const downloadTarget = downloadUrl
? path.resolve(
directory,
`mongodb-custom-${createHash('sha256')
.update(downloadUrl)
.digest('hex')
.slice(0, 16)}`,
)
: path.resolve(
directory,
`mongodb-${process.platform}-${process.env.DISTRO_ID || 'none'}-${
process.arch
}-${versionName}`.replace(/[^a-zA-Z0-9_-]/g, ''),
);
const bindir = path.resolve(
downloadTarget,
isCryptLibrary && !isWindows ? 'lib' : 'bin',
Expand Down Expand Up @@ -99,11 +114,13 @@ export class MongoDBDownloader {
}

await fs.mkdir(downloadTarget, { recursive: true });
const artifactInfo = await this.lookupDownloadUrl({
targetVersion: version,
enterprise: isEnterprise,
options: downloadOptions,
});
const artifactInfo = downloadUrl
? ({ url: downloadUrl } as DownloadArtifactInfo)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a dangerous assertion that makes the public interface here return a value that doesn't match the provided type at all and can cause consumer code to crash completely

: await this.lookupDownloadUrl({
targetVersion: version,
enterprise: isEnterprise,
options: downloadOptions,
});
const { url } = artifactInfo;
debug(`Downloading ${url} into ${downloadTarget}...`);

Expand Down Expand Up @@ -139,11 +156,35 @@ export class MongoDBDownloader {
const response = await fetch(url, {
highWaterMark: MongoDBDownloader.HWM,
} as Parameters<typeof fetch>[1]);
if (!response.ok) {
throw new Error(
`Failed to download ${url}: ${response.status} ${response.statusText}`,
);
}
const totalBytes = +(response.headers.get('content-length') ?? '');
const totalMB = totalBytes ? (totalBytes / 1048576).toFixed(1) : null;
debug(`Download started`, { url, totalMB });
let downloadedBytes = 0;
let lastProgressLog = Date.now();
const progress = new Transform({
transform(chunk: Buffer, _encoding, callback) {
downloadedBytes += chunk.length;
if (Date.now() - lastProgressLog >= 3000) {
lastProgressLog = Date.now();
const downloadedMB = (downloadedBytes / 1048576).toFixed(1);
debug(
`Downloading: ${downloadedMB}MB${totalMB ? ` / ${totalMB}MB` : ''}`,
);
}
callback(null, chunk);
},
});
if (/\.tgz$|\.tar(\.[^.]+)?$/.exec(url)) {
// the server's tarballs can contain hard links, which the (unmaintained?)
// `download` package is unable to handle (https://github.com/kevva/decompress/issues/93)
await promisify(pipeline)(
response.body,
progress,
tar.x({ cwd: downloadTarget, strip: isCryptLibrary ? 0 : 1 }),
);
} else {
Expand All @@ -153,6 +194,7 @@ export class MongoDBDownloader {
);
await promisify(pipeline)(
response.body,
progress,
createWriteStream(filename, { highWaterMark: MongoDBDownloader.HWM }),
);
debug(`Written file ${url} to ${filename}, extracting...`);
Expand Down Expand Up @@ -230,32 +272,15 @@ async function withoutLock<T>(
const downloader = new MongoDBDownloader();

/** Download mongod + mongos with version info and return version info and the path to a directory containing them. */
export async function downloadMongoDbWithVersionInfo({
downloadOptions = {},
version = '*',
directory,
useLockfile,
}: MongoDBDownloaderOptions): Promise<DownloadResult> {
return await downloader.downloadMongoDbWithVersionInfo({
downloadOptions,
version,
directory,
useLockfile,
});
export async function downloadMongoDbWithVersionInfo(
options: MongoDBDownloaderOptions,
): Promise<DownloadResult> {
return await downloader.downloadMongoDbWithVersionInfo(options);
}
/** Download mongod + mongos and return the path to a directory containing them. */
export async function downloadMongoDb({
downloadOptions = {},
version = '*',
directory,
useLockfile,
}: MongoDBDownloaderOptions): Promise<string> {
return (
await downloader.downloadMongoDbWithVersionInfo({
downloadOptions,
version,
directory,
useLockfile,
})
).downloadedBinDir;
export async function downloadMongoDb(
options: MongoDBDownloaderOptions,
): Promise<string> {
return (await downloader.downloadMongoDbWithVersionInfo(options))
.downloadedBinDir;
}
7 changes: 7 additions & 0 deletions packages/mongodb-runner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ You can specify a config file for the CLI using `--config`:
$ npx mongodb-runner start --config <path/to/config.json>
```

## DSC clusters

mongodb-runner can launch clusters that use DSC, backed by an SLS storage
layer, managing the storage backend's docker compose project alongside the
mongod processes. See
[docs/disaggregated-storage.md](./docs/disaggregated-storage.md).

## License

Apache 2.0
Loading
Loading