Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/lib/fetch.ts
5220 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import es from 'event-stream';
7
import VinylFile from 'vinyl';
8
import log from 'fancy-log';
9
import ansiColors from 'ansi-colors';
10
import crypto from 'crypto';
11
import through2 from 'through2';
12
import { Stream } from 'stream';
13
14
export interface IFetchOptions {
15
base?: string;
16
nodeFetchOptions?: RequestInit;
17
verbose?: boolean;
18
checksumSha256?: string;
19
}
20
21
export function fetchUrls(urls: string[] | string, options: IFetchOptions): es.ThroughStream {
22
if (options === undefined) {
23
options = {};
24
}
25
26
if (typeof options.base !== 'string' && options.base !== null) {
27
options.base = '/';
28
}
29
30
if (!Array.isArray(urls)) {
31
urls = [urls];
32
}
33
34
return es.readArray(urls).pipe(es.map<string, VinylFile | void>((data: string, cb) => {
35
const url = [options.base, data].join('');
36
fetchUrl(url, options).then(file => {
37
cb(undefined, file);
38
}, error => {
39
cb(error);
40
});
41
}));
42
}
43
44
export async function fetchUrl(url: string, options: IFetchOptions, retries = 10, retryDelay = 1000): Promise<VinylFile> {
45
const verbose = !!options.verbose || !!process.env['CI'] || !!process.env['BUILD_ARTIFACTSTAGINGDIRECTORY'] || !!process.env['GITHUB_WORKSPACE'];
46
try {
47
let startTime = 0;
48
if (verbose) {
49
log(`Start fetching ${ansiColors.magenta(url)}${retries !== 10 ? ` (${10 - retries} retry)` : ''}`);
50
startTime = new Date().getTime();
51
}
52
const controller = new AbortController();
53
let timeout = setTimeout(() => controller.abort(), 30 * 1000);
54
try {
55
const response = await fetch(url, {
56
...options.nodeFetchOptions,
57
signal: controller.signal
58
});
59
if (verbose) {
60
log(`Fetch completed: Status ${response.status}. Took ${ansiColors.magenta(`${new Date().getTime() - startTime} ms`)}`);
61
}
62
if (response.ok && (response.status >= 200 && response.status < 300)) {
63
// Reset timeout for body download - large files need more time
64
clearTimeout(timeout);
65
timeout = setTimeout(() => controller.abort(), 5 * 60 * 1000);
66
const contents = Buffer.from(await response.arrayBuffer());
67
if (options.checksumSha256) {
68
const actualSHA256Checksum = crypto.createHash('sha256').update(contents).digest('hex');
69
if (actualSHA256Checksum !== options.checksumSha256) {
70
throw new Error(`Checksum mismatch for ${ansiColors.cyan(url)} (expected ${options.checksumSha256}, actual ${actualSHA256Checksum}))`);
71
} else if (verbose) {
72
log(`Verified SHA256 checksums match for ${ansiColors.cyan(url)}`);
73
}
74
} else if (verbose) {
75
log(`Skipping checksum verification for ${ansiColors.cyan(url)} because no expected checksum was provided`);
76
}
77
if (verbose) {
78
log(`Fetched response body buffer: ${ansiColors.magenta(`${(contents as Buffer).byteLength} bytes`)}`);
79
}
80
return new VinylFile({
81
cwd: '/',
82
base: options.base,
83
path: url,
84
contents
85
});
86
}
87
let err = `Request ${ansiColors.magenta(url)} failed with status code: ${response.status}`;
88
if (response.status === 403) {
89
err += ' (you may be rate limited)';
90
}
91
throw new Error(err);
92
} finally {
93
clearTimeout(timeout);
94
}
95
} catch (e) {
96
if (verbose) {
97
log(`Fetching ${ansiColors.cyan(url)} failed: ${e}`);
98
}
99
if (retries > 0) {
100
await new Promise(resolve => setTimeout(resolve, retryDelay));
101
return fetchUrl(url, options, retries - 1, retryDelay);
102
}
103
throw e;
104
}
105
}
106
107
const ghApiHeaders: Record<string, string> = {
108
Accept: 'application/vnd.github.v3+json',
109
'User-Agent': 'VSCode Build',
110
};
111
if (process.env.GITHUB_TOKEN) {
112
ghApiHeaders.Authorization = 'Basic ' + Buffer.from(process.env.GITHUB_TOKEN).toString('base64');
113
}
114
const ghDownloadHeaders = {
115
...ghApiHeaders,
116
Accept: 'application/octet-stream',
117
};
118
119
export interface IGitHubAssetOptions {
120
version: string;
121
name: string | ((name: string) => boolean);
122
checksumSha256?: string;
123
verbose?: boolean;
124
}
125
126
/**
127
* @param repo for example `Microsoft/vscode`
128
* @param version for example `16.17.1` - must be a valid releases tag
129
* @param assetName for example (name) => name === `win-x64-node.exe` - must be an asset that exists
130
* @returns a stream with the asset as file
131
*/
132
export function fetchGithub(repo: string, options: IGitHubAssetOptions): Stream {
133
return fetchUrls(`/repos/${repo.replace(/^\/|\/$/g, '')}/releases/tags/v${options.version}`, {
134
base: 'https://api.github.com',
135
verbose: options.verbose,
136
nodeFetchOptions: { headers: ghApiHeaders }
137
}).pipe(through2.obj(async function (file, _enc, callback) {
138
const assetFilter = typeof options.name === 'string' ? (name: string) => name === options.name : options.name;
139
const asset = JSON.parse(file.contents.toString()).assets.find((a: { name: string }) => assetFilter(a.name));
140
if (!asset) {
141
return callback(new Error(`Could not find asset in release of ${repo} @ ${options.version}`));
142
}
143
try {
144
callback(null, await fetchUrl(asset.url, {
145
nodeFetchOptions: { headers: ghDownloadHeaders },
146
verbose: options.verbose,
147
checksumSha256: options.checksumSha256
148
}));
149
} catch (error) {
150
callback(error);
151
}
152
}));
153
}
154
155