diff options
Diffstat (limited to 'editors/code/src/net.ts')
-rw-r--r-- | editors/code/src/net.ts | 131 |
1 files changed, 131 insertions, 0 deletions
diff --git a/editors/code/src/net.ts b/editors/code/src/net.ts new file mode 100644 index 000000000..492213937 --- /dev/null +++ b/editors/code/src/net.ts | |||
@@ -0,0 +1,131 @@ | |||
1 | import fetch from "node-fetch"; | ||
2 | import * as vscode from "vscode"; | ||
3 | import * as fs from "fs"; | ||
4 | import * as stream from "stream"; | ||
5 | import * as util from "util"; | ||
6 | import { log, assert } from "./util"; | ||
7 | |||
8 | const pipeline = util.promisify(stream.pipeline); | ||
9 | |||
10 | const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; | ||
11 | const OWNER = "rust-analyzer"; | ||
12 | const REPO = "rust-analyzer"; | ||
13 | |||
14 | export async function fetchRelease( | ||
15 | releaseTag: string | ||
16 | ): Promise<GithubRelease> { | ||
17 | |||
18 | const apiEndpointPath = `/repos/${OWNER}/${REPO}/releases/tags/${releaseTag}`; | ||
19 | |||
20 | const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; | ||
21 | |||
22 | log.debug("Issuing request for released artifacts metadata to", requestUrl); | ||
23 | |||
24 | const response = await fetch(requestUrl, { headers: { Accept: "application/vnd.github.v3+json" } }); | ||
25 | |||
26 | if (!response.ok) { | ||
27 | log.error("Error fetching artifact release info", { | ||
28 | requestUrl, | ||
29 | releaseTag, | ||
30 | response: { | ||
31 | headers: response.headers, | ||
32 | status: response.status, | ||
33 | body: await response.text(), | ||
34 | } | ||
35 | }); | ||
36 | |||
37 | throw new Error( | ||
38 | `Got response ${response.status} when trying to fetch ` + | ||
39 | `release info for ${releaseTag} release` | ||
40 | ); | ||
41 | } | ||
42 | |||
43 | // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`) | ||
44 | const release: GithubRelease = await response.json(); | ||
45 | return release; | ||
46 | } | ||
47 | |||
48 | // We omit declaration of tremendous amount of fields that we are not using here | ||
49 | export interface GithubRelease { | ||
50 | name: string; | ||
51 | id: number; | ||
52 | // eslint-disable-next-line camelcase | ||
53 | published_at: string; | ||
54 | assets: Array<{ | ||
55 | name: string; | ||
56 | // eslint-disable-next-line camelcase | ||
57 | browser_download_url: string; | ||
58 | }>; | ||
59 | } | ||
60 | |||
61 | |||
62 | export async function download( | ||
63 | downloadUrl: string, | ||
64 | destinationPath: string, | ||
65 | progressTitle: string, | ||
66 | { mode }: { mode?: number } = {}, | ||
67 | ) { | ||
68 | await vscode.window.withProgress( | ||
69 | { | ||
70 | location: vscode.ProgressLocation.Notification, | ||
71 | cancellable: false, | ||
72 | title: progressTitle | ||
73 | }, | ||
74 | async (progress, _cancellationToken) => { | ||
75 | let lastPercentage = 0; | ||
76 | await downloadFile(downloadUrl, destinationPath, mode, (readBytes, totalBytes) => { | ||
77 | const newPercentage = (readBytes / totalBytes) * 100; | ||
78 | progress.report({ | ||
79 | message: newPercentage.toFixed(0) + "%", | ||
80 | increment: newPercentage - lastPercentage | ||
81 | }); | ||
82 | |||
83 | lastPercentage = newPercentage; | ||
84 | }); | ||
85 | } | ||
86 | ); | ||
87 | } | ||
88 | |||
89 | /** | ||
90 | * Downloads file from `url` and stores it at `destFilePath` with `destFilePermissions`. | ||
91 | * `onProgress` callback is called on recieveing each chunk of bytes | ||
92 | * to track the progress of downloading, it gets the already read and total | ||
93 | * amount of bytes to read as its parameters. | ||
94 | */ | ||
95 | async function downloadFile( | ||
96 | url: string, | ||
97 | destFilePath: fs.PathLike, | ||
98 | mode: number | undefined, | ||
99 | onProgress: (readBytes: number, totalBytes: number) => void | ||
100 | ): Promise<void> { | ||
101 | const res = await fetch(url); | ||
102 | |||
103 | if (!res.ok) { | ||
104 | log.error("Error", res.status, "while downloading file from", url); | ||
105 | log.error({ body: await res.text(), headers: res.headers }); | ||
106 | |||
107 | throw new Error(`Got response ${res.status} when trying to download a file.`); | ||
108 | } | ||
109 | |||
110 | const totalBytes = Number(res.headers.get('content-length')); | ||
111 | assert(!Number.isNaN(totalBytes), "Sanity check of content-length protocol"); | ||
112 | |||
113 | log.debug("Downloading file of", totalBytes, "bytes size from", url, "to", destFilePath); | ||
114 | |||
115 | let readBytes = 0; | ||
116 | res.body.on("data", (chunk: Buffer) => { | ||
117 | readBytes += chunk.length; | ||
118 | onProgress(readBytes, totalBytes); | ||
119 | }); | ||
120 | |||
121 | const destFileStream = fs.createWriteStream(destFilePath, { mode }); | ||
122 | |||
123 | await pipeline(res.body, destFileStream); | ||
124 | return new Promise<void>(resolve => { | ||
125 | destFileStream.on("close", resolve); | ||
126 | destFileStream.destroy(); | ||
127 | |||
128 | // Details on workaround: https://github.com/rust-analyzer/rust-analyzer/pull/3092#discussion_r378191131 | ||
129 | // Issue at nodejs repo: https://github.com/nodejs/node/issues/31776 | ||
130 | }); | ||
131 | } | ||