1+ import { log } from './log'
2+ import fetch from 'node-fetch'
3+ import * as vscode from 'vscode'
4+ import * as stream from 'stream'
5+ import * as fs from 'fs'
6+ import * as util from 'util'
7+
8+ const pipeline = util . promisify ( stream . pipeline )
9+
10+ interface GithubRelease {
11+ tag_name : string ;
12+ assets : Array < {
13+ name : string ;
14+ browser_download_url : string ;
15+ } > ;
16+ }
17+
18+ export async function getReleaseInfo ( releaseTag : string ) : Promise < GithubRelease > {
19+ const response = await fetch ( `https://api.github.com/repos/strum355/mcshader-lsp/releases/tags/${ releaseTag } ` , {
20+ headers : { Accept : 'application/vnd.github.v3+json' }
21+ } )
22+
23+ const isRelease = ( obj : unknown ) : obj is GithubRelease => {
24+ return obj != null && typeof obj === 'object'
25+ && typeof ( obj as GithubRelease ) . tag_name === 'string'
26+ && Array . isArray ( ( obj as GithubRelease ) . assets )
27+ && ( obj as GithubRelease ) . assets . every ( ( a ) => typeof a . name === 'string' && typeof a . browser_download_url === 'string' )
28+ }
29+
30+ const json = await response . json ( )
31+ if ( ! isRelease ( json ) ) {
32+ throw new TypeError ( 'Received malformed request from Github Release API' )
33+ }
34+ return json
35+ }
36+
37+ export async function download ( url : string , downloadDest : string ) {
38+ await vscode . window . withProgress (
39+ {
40+ location : vscode . ProgressLocation . Notification ,
41+ cancellable : false ,
42+ title : `Downloading ${ url } `
43+ } ,
44+ async ( progress , _ ) => {
45+ let lastPercentage = 0
46+ await downloadFile ( url , downloadDest , ( readBytes , totalBytes ) => {
47+ const newPercentage = Math . round ( ( readBytes / totalBytes ) * 100 )
48+ if ( newPercentage !== lastPercentage ) {
49+ progress . report ( {
50+ message : `${ newPercentage . toFixed ( 0 ) } %` ,
51+ increment : newPercentage - lastPercentage
52+ } )
53+
54+ lastPercentage = newPercentage
55+ }
56+ } )
57+ }
58+ )
59+ }
60+
61+ async function downloadFile (
62+ url : string ,
63+ destFilePath : fs . PathLike ,
64+ onProgress : ( readBytes : number , totalBytes : number ) => void
65+ ) : Promise < void > {
66+ const res = await fetch ( url )
67+ if ( ! res . ok ) {
68+ log . error ( res . status , 'while downloading file from' , url )
69+ log . error ( { body : await res . text ( ) , headers : res . headers } )
70+ throw new Error ( `Got response ${ res . status } when trying to download ${ url } .` )
71+ }
72+
73+ const totalBytes = Number ( res . headers . get ( 'content-length' ) )
74+
75+ log . debug ( 'downloading file of' , totalBytes , 'bytes size from' , url , 'to' , destFilePath )
76+
77+ let readBytes = 0
78+ res . body . on ( 'data' , ( chunk : Buffer ) => {
79+ readBytes += chunk . length
80+ onProgress ( readBytes , totalBytes )
81+ } )
82+
83+ const destFileStream = fs . createWriteStream ( destFilePath , { mode : 0o755 } )
84+
85+ await pipeline ( res . body , destFileStream )
86+
87+ // Don't apply the workaround in fixed versions of nodejs, since the process
88+ // freezes on them, the process waits for no-longer emitted `close` event.
89+ // The fix was applied in commit 7eed9d6bcc in v13.11.0
90+ // See the nodejs changelog:
91+ // https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V13.md
92+ const [ , major , minor ] = / v ( \d + ) \. ( \d + ) \. ( \d + ) / . exec ( process . version ) !
93+ if ( + major > 13 || ( + major === 13 && + minor >= 11 ) ) return
94+
95+ await new Promise < void > ( resolve => {
96+ destFileStream . on ( 'close' , resolve )
97+ destFileStream . destroy ( )
98+ // This workaround is awaiting to be removed when vscode moves to newer nodejs version:
99+ // https://github.com/rust-analyzer/rust-analyzer/issues/3167
100+ } )
101+ }
0 commit comments