@@ -14,28 +14,33 @@ import type { CodeQLCliServer } from "../codeql-cli/cli";
14
14
import {
15
15
pathExists ,
16
16
ensureDir ,
17
- readdir ,
18
- move ,
19
17
remove ,
20
18
unlink ,
21
- mkdtemp ,
22
19
readFile ,
23
20
writeFile ,
21
+ createWriteStream ,
24
22
} from "fs-extra" ;
25
- import { withProgress , progressUpdate } from "../common/vscode/progress" ;
23
+ import {
24
+ withProgress ,
25
+ progressUpdate ,
26
+ reportStreamProgress ,
27
+ } from "../common/vscode/progress" ;
26
28
import type { ProgressCallback } from "../common/vscode/progress" ;
27
- import { join , dirname , parse } from "path" ;
29
+ import { join , parse } from "path" ;
28
30
import { tryGetQueryMetadata } from "../codeql-cli/query-metadata" ;
29
31
import { pluralize } from "../common/word" ;
30
32
import { readRepoTask } from "./repo-tasks-store" ;
31
- import { tmpdir } from "os " ;
33
+ import { tmpDir } from "../tmp-dir " ;
32
34
import { spawn } from "child_process" ;
33
35
import type { execFileSync } from "child_process" ;
34
36
import { tryOpenExternalFile } from "../common/vscode/external-files" ;
35
37
import type { VariantAnalysisManager } from "./variant-analysis-manager" ;
36
38
import type { VariantAnalysisResultsManager } from "./variant-analysis-results-manager" ;
37
- import { getAutofixPath , getAutofixModel } from "../config" ;
39
+ import { getAutofixPath , getAutofixModel , downloadTimeout } from "../config" ;
38
40
import { getErrorMessage } from "../common/helpers-pure" ;
41
+ import { createTimeoutSignal } from "../common/fetch-stream" ;
42
+ import { unzipToDirectoryConcurrently } from "../common/unzip-concurrently" ;
43
+ import { reportUnzipProgress } from "../common/vscode/unzip-progress" ;
39
44
40
45
// Limit to three repos when generating autofixes so not sending
41
46
// too many requests to autofix. Since we only need to validate
@@ -308,7 +313,7 @@ async function processSelectedRepositories(
308
313
withProgress (
309
314
async ( progressForRepo : ProgressCallback ) => {
310
315
// Get the sarif file.
311
- progressForRepo ( progressUpdate ( 1 , 3 , `Getting sarif` ) ) ;
316
+ progressForRepo ( progressUpdate ( 1 , 3 , `Getting sarif... ` ) ) ;
312
317
const sarifFile = await getRepoSarifFile (
313
318
variantAnalysisResultsManager ,
314
319
variantAnalysisIdStoragePath ,
@@ -332,17 +337,17 @@ async function processSelectedRepositories(
332
337
333
338
// Download the source root.
334
339
// Using `0` as the progress step to force a dynamic vs static progress bar.
335
- // Consider using `reportStreamProgress` as a future enhancement.
336
- progressForRepo ( progressUpdate ( 0 , 3 , `Downloading source root` ) ) ;
340
+ progressForRepo ( progressUpdate ( 0 , 3 , `Fetching source root...` ) ) ;
337
341
const srcRootPath = await downloadPublicCommitSource (
338
342
nwo ,
339
343
repoTask . databaseCommitSha ,
340
344
sourceRootsStoragePath ,
341
345
credentials ,
346
+ progressForRepo ,
342
347
) ;
343
348
344
349
// Run autofix.
345
- progressForRepo ( progressUpdate ( 2 , 3 , `Running autofix` ) ) ;
350
+ progressForRepo ( progressUpdate ( 2 , 3 , `Running autofix... ` ) ) ;
346
351
await runAutofixForRepository (
347
352
nwo ,
348
353
sarifFile ,
@@ -354,7 +359,7 @@ async function processSelectedRepositories(
354
359
) ;
355
360
} ,
356
361
{
357
- title : `Processing ${ nwo } ` ,
362
+ title : `${ nwo } ` ,
358
363
cancellable : false ,
359
364
} ,
360
365
) ,
@@ -399,6 +404,7 @@ async function downloadPublicCommitSource(
399
404
sha : string ,
400
405
outputPath : string ,
401
406
credentials : Credentials ,
407
+ progressCallback : ProgressCallback ,
402
408
) : Promise < string > {
403
409
const [ owner , repo ] = nwo . split ( "/" ) ;
404
410
if ( ! owner || ! repo ) {
@@ -425,77 +431,128 @@ async function downloadPublicCommitSource(
425
431
void extLogger . log ( `Fetching source of repository ${ nwo } at ${ sha } ...` ) ;
426
432
427
433
try {
428
- // Create a temporary directory for downloading
429
- const downloadDir = await mkdtemp ( join ( tmpdir ( ) , "download-source-" ) ) ;
430
- const tarballPath = join ( downloadDir , "source.tar.gz" ) ;
431
-
432
434
const octokit = await credentials . getOctokit ( ) ;
433
435
434
- // Get the tarball URL
435
- const { url } = await octokit . rest . repos . downloadTarballArchive ( {
436
+ // Get the zipball URL
437
+ const { url } = await octokit . rest . repos . downloadZipballArchive ( {
436
438
owner,
437
439
repo,
438
440
ref : sha ,
439
441
} ) ;
440
442
441
- // Download the tarball using spawn
442
- await new Promise < void > ( ( resolve , reject ) => {
443
- const curlArgs = [
444
- "-H" ,
445
- "Accept: application/octet-stream" ,
446
- "--user-agent" ,
447
- "GitHub-CodeQL-Extension" ,
448
- "-L" , // Follow redirects
449
- "-o" ,
450
- tarballPath ,
451
- url ,
452
- ] ;
453
-
454
- const process = spawn ( "curl" , curlArgs , { cwd : downloadDir } ) ;
455
-
456
- process . on ( "error" , reject ) ;
457
- process . on ( "exit" , ( code ) =>
458
- code === 0
459
- ? resolve ( )
460
- : reject ( new Error ( `curl exited with code ${ code } ` ) ) ,
461
- ) ;
462
- } ) ;
443
+ // Create a temporary directory for downloading
444
+ const archivePath = join (
445
+ tmpDir . name ,
446
+ `source-${ owner } -${ repo } -${ Date . now ( ) } .zip` ,
447
+ ) ;
463
448
464
- void extLogger . log ( `Download complete, extracting source...` ) ;
449
+ // Set timeout
450
+ const {
451
+ signal,
452
+ onData,
453
+ dispose : disposeTimeout ,
454
+ } = createTimeoutSignal ( downloadTimeout ( ) ) ;
465
455
466
- // Extract the tarball
467
- await new Promise < void > ( ( resolve , reject ) => {
468
- const process = spawn ( "tar" , [ "-xzf" , tarballPath ] , { cwd : downloadDir } ) ;
456
+ // Fetch the url
457
+ let response : Response ;
458
+ try {
459
+ response = await fetch ( url , {
460
+ headers : {
461
+ Accept : "application/zip" ,
462
+ } ,
463
+ signal,
464
+ } ) ;
465
+ } catch ( e ) {
466
+ disposeTimeout ( ) ;
469
467
470
- process . on ( "error" , reject ) ;
471
- process . on ( "exit" , ( code ) =>
472
- code === 0
473
- ? resolve ( )
474
- : reject ( new Error ( `tar extraction failed with code ${ code } ` ) ) ,
468
+ if ( e instanceof DOMException && e . name === "AbortError" ) {
469
+ const thrownError = new Error ( "The request timed out." ) ;
470
+ thrownError . stack = e . stack ;
471
+ throw thrownError ;
472
+ }
473
+ throw new Error (
474
+ `Error fetching source root. Reason: ${ getErrorMessage ( e ) } ` ,
475
475
) ;
476
- } ) ;
476
+ }
477
477
478
- // Remove the tarball to save space
479
- await unlink ( tarballPath ) ;
478
+ // Download the source root from the response body
479
+ const body = response . body ;
480
+ if ( ! body ) {
481
+ throw new Error ( "No response body found" ) ;
482
+ }
480
483
481
- // Find the extracted directory (GitHub tarballs extract to a single directory)
482
- const extractedFiles = await readdir ( downloadDir ) ;
483
- const sourceDir = extractedFiles . filter ( ( f ) => f !== "source.tar.gz" ) [ 0 ] ;
484
+ const archiveFileStream = createWriteStream ( archivePath ) ;
484
485
485
- if ( ! sourceDir ) {
486
- throw new Error ( "Failed to find extracted source directory" ) ;
487
- }
486
+ const contentLength = response . headers . get ( "content-length" ) ;
487
+ const totalNumBytes = contentLength
488
+ ? parseInt ( contentLength , 10 )
489
+ : undefined ;
490
+
491
+ const reportProgress = reportStreamProgress (
492
+ "Downloading source root..." ,
493
+ totalNumBytes ,
494
+ progressCallback ,
495
+ ) ;
496
+
497
+ try {
498
+ const reader = body . getReader ( ) ;
499
+ for ( ; ; ) {
500
+ const { done, value } = await reader . read ( ) ;
501
+ if ( done ) {
502
+ break ;
503
+ }
504
+
505
+ onData ( ) ;
506
+ reportProgress ( value ?. length ?? 0 ) ;
507
+
508
+ await new Promise ( ( resolve , reject ) => {
509
+ archiveFileStream . write ( value , ( err ) => {
510
+ if ( err ) {
511
+ reject ( err ) ;
512
+ }
513
+ resolve ( undefined ) ;
514
+ } ) ;
515
+ } ) ;
516
+ }
488
517
489
- const extractedSourcePath = join ( downloadDir , sourceDir ) ;
518
+ await new Promise ( ( resolve , reject ) => {
519
+ archiveFileStream . close ( ( err ) => {
520
+ if ( err ) {
521
+ reject ( err ) ;
522
+ }
523
+ resolve ( undefined ) ;
524
+ } ) ;
525
+ } ) ;
526
+ } catch ( e ) {
527
+ // Close and remove the file if an error occurs
528
+ archiveFileStream . close ( ( ) => {
529
+ void remove ( archivePath ) ;
530
+ } ) ;
531
+
532
+ if ( e instanceof DOMException && e . name === "AbortError" ) {
533
+ const thrownError = new Error ( "The download timed out." ) ;
534
+ thrownError . stack = e . stack ;
535
+ throw thrownError ;
536
+ }
490
537
491
- // Ensure the destination directory's parent exists
492
- await ensureDir ( dirname ( checkoutDir ) ) ;
538
+ throw new Error (
539
+ `Error downloading source root. Reason: ${ getErrorMessage ( e ) } ` ,
540
+ ) ;
541
+ } finally {
542
+ disposeTimeout ( ) ;
543
+ }
493
544
494
- // Move the extracted source to the final location
495
- await move ( extractedSourcePath , checkoutDir ) ;
545
+ void extLogger . log ( `Download complete, extracting source...` ) ;
496
546
497
- // Clean up the temporary directory
498
- await remove ( downloadDir ) ;
547
+ // Extract the downloaded zip file
548
+ await unzipToDirectoryConcurrently (
549
+ archivePath ,
550
+ outputPath ,
551
+ progressCallback
552
+ ? reportUnzipProgress ( `Unzipping source root...` , progressCallback )
553
+ : undefined ,
554
+ ) ;
555
+ await remove ( archivePath ) ;
499
556
500
557
return checkoutDir ;
501
558
} catch ( error ) {
0 commit comments