1+ import type { AxiosInstance , AxiosRequestConfig } from "axios" ;
12import { Api } from "coder/site/src/api/api" ;
23import { createWriteStream } from "fs" ;
34import fs from "fs/promises" ;
@@ -202,122 +203,22 @@ export class Storage {
202203 const etag = stat !== undefined ? await cli . eTag ( binPath ) : "" ;
203204 this . output . info ( "Using ETag" , etag ) ;
204205
205- // Make the download request.
206- const controller = new AbortController ( ) ;
207- const resp = await restClient . getAxiosInstance ( ) . get ( binSource , {
208- signal : controller . signal ,
209- baseURL : baseUrl ,
210- responseType : "stream" ,
211- headers : {
212- "Accept-Encoding" : "gzip" ,
213- "If-None-Match" : `"${ etag } "` ,
214- } ,
215- decompress : true ,
216- // Ignore all errors so we can catch a 404!
217- validateStatus : ( ) => true ,
206+ // Download the binary to a temporary file.
207+ await fs . mkdir ( path . dirname ( binPath ) , { recursive : true } ) ;
208+ const tempFile =
209+ binPath + ".temp-" + Math . random ( ) . toString ( 36 ) . substring ( 8 ) ;
210+ const writeStream = createWriteStream ( tempFile , {
211+ autoClose : true ,
212+ mode : 0o755 ,
213+ } ) ;
214+ const client = restClient . getAxiosInstance ( ) ;
215+ const status = await this . download ( client , binSource , writeStream , {
216+ "Accept-Encoding" : "gzip" ,
217+ "If-None-Match" : `"${ etag } "` ,
218218 } ) ;
219- this . output . info ( "Got status code" , resp . status ) ;
220219
221- switch ( resp . status ) {
220+ switch ( status ) {
222221 case 200 : {
223- const rawContentLength = resp . headers [ "content-length" ] ;
224- const contentLength = Number . parseInt ( rawContentLength ) ;
225- if ( Number . isNaN ( contentLength ) ) {
226- this . output . warn (
227- "Got invalid or missing content length" ,
228- rawContentLength ,
229- ) ;
230- } else {
231- this . output . info ( "Got content length" , prettyBytes ( contentLength ) ) ;
232- }
233-
234- // Download to a temporary file.
235- await fs . mkdir ( path . dirname ( binPath ) , { recursive : true } ) ;
236- const tempFile =
237- binPath + ".temp-" + Math . random ( ) . toString ( 36 ) . substring ( 8 ) ;
238-
239- // Track how many bytes were written.
240- let written = 0 ;
241-
242- const completed = await vscode . window . withProgress < boolean > (
243- {
244- location : vscode . ProgressLocation . Notification ,
245- title : `Downloading ${ buildInfo . version } from ${ baseUrl } to ${ binPath } ` ,
246- cancellable : true ,
247- } ,
248- async ( progress , token ) => {
249- const readStream = resp . data as IncomingMessage ;
250- let cancelled = false ;
251- token . onCancellationRequested ( ( ) => {
252- controller . abort ( ) ;
253- readStream . destroy ( ) ;
254- cancelled = true ;
255- } ) ;
256-
257- // Reverse proxies might not always send a content length.
258- const contentLengthPretty = Number . isNaN ( contentLength )
259- ? "unknown"
260- : prettyBytes ( contentLength ) ;
261-
262- // Pipe data received from the request to the temp file.
263- const writeStream = createWriteStream ( tempFile , {
264- autoClose : true ,
265- mode : 0o755 ,
266- } ) ;
267- readStream . on ( "data" , ( buffer : Buffer ) => {
268- writeStream . write ( buffer , ( ) => {
269- written += buffer . byteLength ;
270- progress . report ( {
271- message : `${ prettyBytes ( written ) } / ${ contentLengthPretty } ` ,
272- increment : Number . isNaN ( contentLength )
273- ? undefined
274- : ( buffer . byteLength / contentLength ) * 100 ,
275- } ) ;
276- } ) ;
277- } ) ;
278-
279- // Wait for the stream to end or error.
280- return new Promise < boolean > ( ( resolve , reject ) => {
281- writeStream . on ( "error" , ( error ) => {
282- readStream . destroy ( ) ;
283- reject (
284- new Error (
285- `Unable to download binary: ${ errToStr ( error , "no reason given" ) } ` ,
286- ) ,
287- ) ;
288- } ) ;
289- readStream . on ( "error" , ( error ) => {
290- writeStream . close ( ) ;
291- reject (
292- new Error (
293- `Unable to download binary: ${ errToStr ( error , "no reason given" ) } ` ,
294- ) ,
295- ) ;
296- } ) ;
297- readStream . on ( "close" , ( ) => {
298- writeStream . close ( ) ;
299- if ( cancelled ) {
300- resolve ( false ) ;
301- } else {
302- resolve ( true ) ;
303- }
304- } ) ;
305- } ) ;
306- } ,
307- ) ;
308-
309- // False means the user canceled, although in practice it appears we
310- // would not get this far because VS Code already throws on cancelation.
311- if ( ! completed ) {
312- this . output . warn ( "User aborted download" ) ;
313- throw new Error ( "User aborted download" ) ;
314- }
315-
316- this . output . info (
317- `Downloaded ${ prettyBytes ( written ) } to` ,
318- path . basename ( tempFile ) ,
319- ) ;
320-
321222 // Move the old binary to a backup location first, just in case. And,
322223 // on Linux at least, you cannot write onto a binary that is in use so
323224 // moving first works around that (delete would also work).
@@ -389,7 +290,7 @@ export class Storage {
389290 }
390291 const params = new URLSearchParams ( {
391292 title : `Failed to download binary on \`${ cli . goos ( ) } -${ cli . goarch ( ) } \`` ,
392- body : `Received status code \`${ resp . status } \` when downloading the binary.` ,
293+ body : `Received status code \`${ status } \` when downloading the binary.` ,
393294 } ) ;
394295 const uri = vscode . Uri . parse (
395296 `https://github.com/coder/vscode-coder/issues/new?` +
@@ -402,6 +303,121 @@ export class Storage {
402303 }
403304 }
404305
306+ /**
307+ * Download the source to the provided stream with a progress dialog. Return
308+ * the status code or throw if the user aborts or there is an error.
309+ */
310+ private async download (
311+ client : AxiosInstance ,
312+ source : string ,
313+ writeStream : WriteStream ,
314+ headers ?: AxiosRequestConfig [ "headers" ] ,
315+ ) : Promise < number > {
316+ const baseUrl = client . defaults . baseURL ;
317+
318+ const controller = new AbortController ( ) ;
319+ const resp = await client . get ( source , {
320+ signal : controller . signal ,
321+ baseURL : baseUrl ,
322+ responseType : "stream" ,
323+ headers,
324+ decompress : true ,
325+ // Ignore all errors so we can catch a 404!
326+ validateStatus : ( ) => true ,
327+ } ) ;
328+ this . output . info ( "Got status code" , resp . status ) ;
329+
330+ if ( resp . status === 200 ) {
331+ const rawContentLength = resp . headers [ "content-length" ] ;
332+ const contentLength = Number . parseInt ( rawContentLength ) ;
333+ if ( Number . isNaN ( contentLength ) ) {
334+ this . output . warn (
335+ "Got invalid or missing content length" ,
336+ rawContentLength ,
337+ ) ;
338+ } else {
339+ this . output . info ( "Got content length" , prettyBytes ( contentLength ) ) ;
340+ }
341+
342+ // Track how many bytes were written.
343+ let written = 0 ;
344+
345+ const completed = await vscode . window . withProgress < boolean > (
346+ {
347+ location : vscode . ProgressLocation . Notification ,
348+ title : `Downloading ${ baseUrl } ` ,
349+ cancellable : true ,
350+ } ,
351+ async ( progress , token ) => {
352+ const readStream = resp . data as IncomingMessage ;
353+ let cancelled = false ;
354+ token . onCancellationRequested ( ( ) => {
355+ controller . abort ( ) ;
356+ readStream . destroy ( ) ;
357+ cancelled = true ;
358+ } ) ;
359+
360+ // Reverse proxies might not always send a content length.
361+ const contentLengthPretty = Number . isNaN ( contentLength )
362+ ? "unknown"
363+ : prettyBytes ( contentLength ) ;
364+
365+ // Pipe data received from the request to the stream.
366+ readStream . on ( "data" , ( buffer : Buffer ) => {
367+ writeStream . write ( buffer , ( ) => {
368+ written += buffer . byteLength ;
369+ progress . report ( {
370+ message : `${ prettyBytes ( written ) } / ${ contentLengthPretty } ` ,
371+ increment : Number . isNaN ( contentLength )
372+ ? undefined
373+ : ( buffer . byteLength / contentLength ) * 100 ,
374+ } ) ;
375+ } ) ;
376+ } ) ;
377+
378+ // Wait for the stream to end or error.
379+ return new Promise < boolean > ( ( resolve , reject ) => {
380+ writeStream . on ( "error" , ( error ) => {
381+ readStream . destroy ( ) ;
382+ reject (
383+ new Error (
384+ `Unable to download binary: ${ errToStr ( error , "no reason given" ) } ` ,
385+ ) ,
386+ ) ;
387+ } ) ;
388+ readStream . on ( "error" , ( error ) => {
389+ writeStream . close ( ) ;
390+ reject (
391+ new Error (
392+ `Unable to download binary: ${ errToStr ( error , "no reason given" ) } ` ,
393+ ) ,
394+ ) ;
395+ } ) ;
396+ readStream . on ( "close" , ( ) => {
397+ writeStream . close ( ) ;
398+ if ( cancelled ) {
399+ resolve ( false ) ;
400+ } else {
401+ resolve ( true ) ;
402+ }
403+ } ) ;
404+ } ) ;
405+ } ,
406+ ) ;
407+
408+ // False means the user canceled, although in practice it appears we
409+ // would not get this far because VS Code already throws on cancelation.
410+ if ( ! completed ) {
411+ this . output . warn ( "User aborted download" ) ;
412+ throw new Error ( "Download aborted" ) ;
413+ }
414+
415+ this . output . info ( `Downloaded ${ prettyBytes ( written ) } ` ) ;
416+ }
417+
418+ return resp . status ;
419+ }
420+
405421 /**
406422 * Return the directory for a deployment with the provided label to where its
407423 * binary is cached.
0 commit comments