UNPKG

4.66 kBPlain TextView Raw
1import zlib from 'zlib';
2import { promises as fs } from 'fs';
3import { promisify } from 'util';
4import path from 'path';
5import os from 'os';
6import fetch from 'node-fetch';
7import semver from 'semver';
8import _debug from 'debug';
9const debug = _debug('mongodb-download-url:version-list');
10const gunzip = promisify(zlib.gunzip);
11const gzip = promisify(zlib.gzip);
12
13export type ArchiveBaseInfo = {
14 sha1: string;
15 sha256: string;
16 url: string;
17};
18
19export type DownloadInfo = {
20 edition: 'enterprise' | 'targeted' | 'base' | 'source' | 'subscription';
21 target?: string;
22 arch?: string;
23
24 archive: {
25 debug_symbols: string;
26 } & ArchiveBaseInfo;
27
28 cryptd?: ArchiveBaseInfo;
29 shell?: ArchiveBaseInfo;
30 packages?: string[];
31 msi?: string;
32};
33
34export type VersionInfo = {
35 changes: string;
36 notes: string;
37 date: string;
38 githash: string;
39
40 continuous_release: boolean;
41 current: boolean;
42 development_release: boolean;
43 lts_release: boolean;
44 production_release: boolean;
45 release_candidate: boolean;
46 version: string;
47
48 downloads: DownloadInfo[];
49};
50
51type FullJSON = {
52 versions: VersionInfo[];
53};
54
55export type VersionListOpts = {
56 version?: string;
57 versionListUrl?: string;
58 cachePath?: string;
59 cacheTimeMs?: number;
60 productionOnly?: boolean;
61};
62
63function defaultCachePath(): string {
64 return path.join(os.tmpdir(), '.mongodb-full.json.gz');
65}
66
67let fullJSON: FullJSON | undefined;
68let fullJSONFetchTime = 0;
69async function getFullJSON(opts: VersionListOpts): Promise<FullJSON> {
70 const versionListUrl = opts.versionListUrl ?? 'https://downloads.mongodb.org/full.json';
71 const cachePath = opts.cachePath ?? defaultCachePath();
72 const cacheTimeMs = opts.cacheTimeMs ?? 24 * 3600 * 1000;
73 let tryWriteCache = cacheTimeMs > 0;
74 const inMemoryCopyUpToDate = () => fullJSONFetchTime >= new Date().getTime() - cacheTimeMs;
75
76 try {
77 if ((!fullJSON || !inMemoryCopyUpToDate()) && cacheTimeMs > 0) {
78 debug('trying to load versions from cache', cachePath);
79 const fh = await fs.open(cachePath, 'r');
80 try {
81 const stat = await fh.stat();
82 if (process.getuid && (stat.uid !== process.getuid() || (stat.mode & 0o022) !== 0)) {
83 tryWriteCache = false;
84 debug('cannot use cache because it is not a file or we do not own it');
85 throw new Error();
86 }
87 if (stat.mtime.getTime() < new Date().getTime() - cacheTimeMs) {
88 debug('cache is outdated');
89 throw new Error();
90 }
91 debug('cache up-to-date');
92 tryWriteCache = false;
93 fullJSON = JSON.parse((await gunzip(await fh.readFile())).toString());
94 fullJSONFetchTime = new Date().getTime();
95 } finally {
96 await fh.close();
97 }
98 }
99 } catch {}
100 if (!fullJSON || !inMemoryCopyUpToDate()) {
101 debug('trying to load versions from source', versionListUrl);
102 const response = await fetch(versionListUrl);
103 if (!response.ok) {
104 throw new Error(`Could not get mongodb versions from ${versionListUrl}: ${response.statusText}`);
105 }
106 fullJSON = await response.json();
107 fullJSONFetchTime = new Date().getTime();
108 if (tryWriteCache) {
109 const partialFilePath = cachePath + `.partial.${process.pid}`;
110 await fs.mkdir(path.dirname(cachePath), { recursive: true });
111 try {
112 const compressed = await gzip(JSON.stringify(fullJSON), { level: 9 });
113 await fs.writeFile(partialFilePath, compressed, { mode: 0o644, flag: 'wx' });
114 await fs.rename(partialFilePath, cachePath);
115 debug('wrote cache', cachePath);
116 } catch {
117 try {
118 await fs.unlink(partialFilePath);
119 } catch {}
120 }
121 }
122 }
123 return fullJSON;
124}
125
126export async function getVersion(opts: VersionListOpts): Promise<VersionInfo> {
127 const fullJSON = await getFullJSON(opts);
128 let versions = fullJSON.versions;
129 versions = versions.filter((info: VersionInfo) => info.downloads.length > 0);
130 if (opts.productionOnly) {
131 versions = versions.filter((info: VersionInfo) => info.production_release);
132 }
133 if (opts.version && opts.version !== '*') {
134 versions = versions.filter((info: VersionInfo) => semver.satisfies(info.version, opts.version));
135 }
136 versions = versions.sort((a: VersionInfo, b: VersionInfo) => semver.rcompare(a.version, b.version));
137 return versions[0];
138}
139
140export async function clearCache(cachePath?: string): Promise<void> {
141 debug('clearing cache');
142 fullJSON = undefined;
143 fullJSONFetchTime = 0;
144 if (cachePath !== '') {
145 try {
146 await fs.unlink(cachePath ?? defaultCachePath());
147 } catch (err) {
148 if (err.code === 'ENOENT') return;
149 throw err;
150 }
151 }
152}