mongodb-download-url
Version:
Lookup download URLs for MongoDB versions.
387 lines (356 loc) • 11.1 kB
text/typescript
import os from 'os';
import path from 'path';
import semver from 'semver';
import type { DownloadInfo, VersionListOpts } from './version-list';
import { getVersion, clearCache } from './version-list';
import { getCurrentLinuxDistro } from './linux-distro';
import { inspect } from 'util';
import _debug from 'debug';
const debug = _debug('mongodb-download-url');
type PriorityValue<T> = { value: T; priority: number };
type ArtifactOptions = {
/**
* Specify the version as a semver string. This also accepts the special values:
* - 'stable' (or 'latest') for the latest stable version (default)
* - 'unstable' for the latest unstable version
* - 'latest-alpha' for the latest alpha release of the server
*/
version?: string;
/**
* Specify the binary architecture. Default is host architecture.
*/
arch?: string;
/**
* Specify the binary platforn. Default is host platform, as given by
* `process.platform`.
*/
platform?: string;
/**
* If known, specify a mongodb DISTRO_ID string to pick a specific Linux distro.
*/
distro?: string;
/**
* If true, this will return enterprise servers rather than community servers.
*/
enterprise?: boolean;
/**
* If true, this will return the mongocryptd-only package, if available.
* (This typically only makes sense with { enterprise: true }.)
*/
cryptd?: boolean;
/**
* If true, this will return the mongo_crypt_shared library package, if available.
* (This typically only makes sense with { enterprise: true }.)
*/
crypt_shared?: boolean;
/**
* @deprecated Use crypt_shared instead.
*/
csfle?: boolean;
/**
* @deprecated Use arch instead.
*/
bits?: '32' | '64' | 32 | 64;
};
export type Options = ArtifactOptions & VersionListOpts;
export type DownloadArtifactInfo = Required<Omit<ArtifactOptions, 'csfle'>> & {
/** Full download URL. */
url: string;
/** Filename for the given download URL. */
artifact: string;
/** Currently always 'mongodb'. */
name: string;
// Legacy properties:
/** @deprecated */
ext: string;
/** @deprecated */
filenamePlatform: string;
/** @deprecated */
debug: false;
/** @deprecated */
branch: 'master';
};
type ProcessedOptions = {
version: string;
arch: string[];
target: PriorityValue<string>[];
enterprise: boolean;
cryptd: boolean;
crypt_shared: boolean;
};
function getPriority<T>(values: PriorityValue<T>[], candidate: T): number {
for (const { value, priority } of values) {
if (value === candidate) {
return priority;
}
}
return 0;
}
function maximizer<T>(values: T[], evaluator: (v: T) => number): T | undefined {
let max = -Infinity;
let maximizer: T | undefined;
for (const v of values) {
const result = evaluator(v);
if (result > max) {
max = result;
maximizer = v;
}
}
return maximizer;
}
function parseArch(arch: string): string[] {
if (['i686', 'i386', 'x86', 'ia32'].includes(arch)) {
return ['i686', 'i386', 'x86', 'ia32'];
}
if (['x86_64', 'x64'].includes(arch)) {
return ['x86_64', 'x64'];
}
if (['arm64', 'aarch64'].includes(arch)) {
return ['arm64', 'aarch64'];
}
if (['ppc64', 'ppc64le'].includes(arch)) {
return ['ppc64', 'ppc64le'];
}
return [arch];
}
async function parseTarget(
distro: string | undefined,
platform: string,
archs: string[],
version: string,
): Promise<PriorityValue<string>[]> {
if (platform === 'linux') {
const results: PriorityValue<string>[] = [];
if (distro) {
results.push({ value: distro, priority: 1000 });
if (archs.includes('x86_64')) {
if (distro === 'amzn64' || distro === 'amazon1') {
results.push({ value: 'amazon', priority: 900 });
}
if (distro === 'amazon' || distro === 'amazon1') {
results.push({ value: 'amzn64', priority: 900 });
}
}
}
if (archs.includes('x86_64')) {
results.push({ value: 'linux_x86_64', priority: 1 });
} else if (archs.includes('i686')) {
results.push({ value: 'linux_i686', priority: 1 });
}
let distroResultsErr;
try {
results.push(...(await getCurrentLinuxDistro()));
} catch (err) {
distroResultsErr = err;
}
if (
distro === undefined &&
distroResultsErr &&
(version === '*' ||
version === 'latest-alpha' ||
semver.gte(version, '4.0.0'))
) {
throw distroResultsErr;
}
return results;
} else if (platform === 'sunos') {
return [{ value: 'sunos5', priority: 1 }];
} else if (['win32', 'windows'].includes(platform)) {
if (archs.includes('i686')) {
return [
{ value: 'windows', priority: 1 },
{ value: 'windows_i686', priority: 10 },
];
} else {
return [
{ value: 'windows', priority: 1 },
{ value: 'windows_x86_64', priority: 10 },
{ value: 'windows_x86_64-2008plus', priority: 10 },
{ value: 'windows_x86_64-2008plus-ssl', priority: 100 },
{ value: 'windows_x86_64-2012plus', priority: 100 },
];
}
} else if (['darwin', 'osx', 'macos'].includes(platform)) {
return [
{ value: 'osx', priority: 1 },
{ value: 'osx-ssl', priority: 10 },
{ value: 'darwin', priority: 1 },
{ value: 'macos', priority: 1 },
];
}
return [{ value: platform, priority: 1 }];
}
async function resolve(opts: ProcessedOptions): Promise<DownloadArtifactInfo> {
let download: DownloadInfo | undefined;
if (opts.version === 'latest-alpha' && opts.enterprise) {
const targets = opts.target.map(({ value }) => value);
const arch = opts.arch.includes('arm64') ? 'arm64' : 'x86_64';
let url, target: string | undefined;
if (targets.includes('macos')) {
url = `https://downloads.mongodb.com/osx/mongodb-macos-${arch}-enterprise-latest.tgz`;
target = 'macos';
} else if (targets.includes('linux_x86_64')) {
target = maximizer(opts.target, (candidate) => candidate.priority)!.value;
url = `https://downloads.mongodb.com/linux/mongodb-linux-${arch}-enterprise-${target}-latest.tgz`;
} else if (targets.includes('windows_x86_64')) {
target = 'windows';
url = `https://downloads.mongodb.com/windows/mongodb-windows-${arch}-enterprise-latest.zip`;
}
if (url) {
download = {
target,
edition: 'enterprise',
arch: 'x86_64',
archive: {
url,
sha1: '',
sha256: '',
debug_symbols: '',
},
};
}
}
let version;
if (!download) {
version = await getVersion(opts);
if (!version) {
throw new Error(`Could not find version matching ${inspect(opts)}`);
}
const bestDownload = maximizer(
version.downloads.map((candidate: DownloadInfo) => {
if (opts.enterprise) {
if (candidate.edition !== 'enterprise') {
return { value: candidate, priority: 0 };
}
} else {
if (
candidate.edition !== 'targeted' &&
candidate.edition !== 'base'
) {
return { value: candidate, priority: 0 };
}
}
if (!candidate.arch || !opts.arch.includes(candidate.arch)) {
return { value: candidate, priority: 0 };
}
const targetPriority = getPriority(opts.target, candidate.target);
return { value: candidate, priority: targetPriority };
}),
(candidate: PriorityValue<DownloadInfo>) => candidate.priority,
);
if (bestDownload && bestDownload.priority > 0) {
download = bestDownload.value;
}
}
if (!download) {
throw new Error(
`Could not find download URL for version ${version?.version} ${inspect(
opts,
)}`,
);
}
const wantsCryptd = opts.cryptd && download.target;
const wantsCryptShared = opts.crypt_shared && download.target;
if (wantsCryptShared && !download.crypt_shared && !download.csfle) {
throw new Error(
`No crypt_shared library download for version ${
version?.version
} available ${inspect(opts)}`,
);
}
debug('fully resolved', JSON.stringify(opts, null, 2), download);
// mongocryptd is contained in the regular enterprise archive, the csfle lib is not
let { url } = wantsCryptShared
? (download.crypt_shared ?? download.csfle)!
: ((wantsCryptd ? download.cryptd : null) ?? download.archive);
if (wantsCryptd) {
// cryptd package on Windows was buggy: https://jira.mongodb.org/browse/BUILD-13653
url = url.replace('mongodb-shell-windows', 'mongodb-cryptd-windows');
}
return {
...opts,
name: 'mongodb',
url: url,
arch: download.arch!,
distro: download.target!,
platform: download.target!,
filenamePlatform: download.target!,
version: version?.version ?? '*',
artifact: path.basename(url),
debug: false,
enterprise: download.edition === 'enterprise',
branch: 'master',
bits: ['i386', 'i686'].includes(download.arch!) ? '32' : '64',
ext: url.match(/\.([^.]+)$/)?.[1] ?? 'tgz',
};
}
async function options(
opts: Options | string = {},
): Promise<ProcessedOptions & VersionListOpts> {
if (typeof opts === 'string') {
opts = {
version: opts,
};
} else {
opts = { ...opts };
}
opts.crypt_shared ??= opts.csfle;
if (opts.cryptd && opts.crypt_shared) {
throw new Error('Cannot request both cryptd and csfle package');
}
if (opts.bits && !opts.arch) {
opts.arch = +opts.bits === 32 ? 'ia32' : 'x64';
}
if (!opts.arch) {
opts.arch = os.arch();
}
if (!opts.platform) {
opts.platform = os.platform();
}
if (!opts.version) {
opts.version = process.env.MONGODB_VERSION || 'stable';
}
if (opts.productionOnly) {
opts.allowedTags = ['production_release'];
}
if (
opts.version === 'stable' ||
opts.version === 'latest' ||
opts.version === '*'
) {
opts.version = '*';
opts.allowedTags ??= ['production_release'];
} else if (opts.version === 'rapid' || opts.version === 'continuous') {
// a.k.a. quarterly etc.
opts.version = '*';
opts.allowedTags ??= ['production_release', 'continuous_release'];
} else if (opts.version === 'unstable') {
opts.version = '*';
opts.allowedTags ??= ['*'];
}
const processedOptions: ProcessedOptions & VersionListOpts = {
...opts,
arch: parseArch(opts.arch),
target: [],
enterprise: !!opts.enterprise,
cryptd: !!opts.cryptd,
crypt_shared: !!opts.crypt_shared,
version: opts.version,
};
processedOptions.target = await parseTarget(
opts.distro,
opts.platform,
processedOptions.arch,
processedOptions.version,
);
return processedOptions;
}
export async function getDownloadURL(
opts?: Options | string,
): Promise<DownloadArtifactInfo> {
const parsedOptions = await options(opts);
debug('Building URL for options `%j`', parsedOptions);
return await resolve(parsedOptions);
}
export default getDownloadURL;
export { clearCache };