1
+ import type { AxiosInstance , AxiosRequestConfig } from "axios" ;
1
2
import { Api } from "coder/site/src/api/api" ;
2
3
import { createWriteStream } from "fs" ;
3
4
import fs from "fs/promises" ;
@@ -202,122 +203,22 @@ export class Storage {
202
203
const etag = stat !== undefined ? await cli . eTag ( binPath ) : "" ;
203
204
this . output . info ( "Using ETag" , etag ) ;
204
205
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 } "` ,
218
218
} ) ;
219
- this . output . info ( "Got status code" , resp . status ) ;
220
219
221
- switch ( resp . status ) {
220
+ switch ( status ) {
222
221
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
-
321
222
// Move the old binary to a backup location first, just in case. And,
322
223
// on Linux at least, you cannot write onto a binary that is in use so
323
224
// moving first works around that (delete would also work).
@@ -389,7 +290,7 @@ export class Storage {
389
290
}
390
291
const params = new URLSearchParams ( {
391
292
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.` ,
393
294
} ) ;
394
295
const uri = vscode . Uri . parse (
395
296
`https://github.com/coder/vscode-coder/issues/new?` +
@@ -402,6 +303,121 @@ export class Storage {
402
303
}
403
304
}
404
305
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
+
405
421
/**
406
422
* Return the directory for a deployment with the provided label to where its
407
423
* binary is cached.
0 commit comments