@@ -388,6 +388,9 @@ class DownloadManager extends ChangeNotifier {
388388 if (fileName.endsWith ('.zip' )) {
389389 // extract the zip file
390390 await _extractZipFile (extractDir, filePath, downloadsDir, binary);
391+ } else if (fileName.endsWith ('.tar.gz' ) || fileName.endsWith ('.tgz' )) {
392+ // extract the tar.gz file
393+ await _extractTarGzFile (extractDir, filePath, downloadsDir, binary);
391394 } else {
392395 // move the raw binary
393396 await _processRawBinary (extractDir, filePath, binary);
@@ -452,6 +455,90 @@ class DownloadManager extends ChangeNotifier {
452455 }
453456 }
454457
458+ /// Extract tar.gz file
459+ Future <void > _extractTarGzFile (Directory extractDir, String tarGzPath, Directory downloadsDir, Binary binary) async {
460+ // Create a temporary directory for extraction
461+ final tempDir = Directory (path.join (extractDir.path, 'temp' , path.basenameWithoutExtension (binary.binary)));
462+ try {
463+ await tempDir.delete (recursive: true );
464+ } catch (e) {
465+ // directory probably doesn't exist, swallow!
466+ }
467+ await tempDir.create (recursive: true );
468+
469+ // Read the file bytes, decode gzip, then decode tar
470+ final bytes = await File (tarGzPath).readAsBytes ();
471+ final gzipBytes = GZipDecoder ().decodeBytes (bytes);
472+ final archive = TarDecoder ().decodeBytes (gzipBytes);
473+
474+ try {
475+ await extractArchiveToDisk (archive, tempDir.path);
476+
477+ log.d ('Moving files from temp directory to final location' );
478+ await for (final entity in tempDir.list ()) {
479+ final baseName = path.basename (entity.path);
480+ final targetPath = path.join (extractDir.path, baseName);
481+
482+ // Handle nested directory structure
483+ if (entity is Directory &&
484+ baseName == path.basenameWithoutExtension (path.basenameWithoutExtension (tarGzPath))) {
485+ await for (final innerEntity in entity.list ()) {
486+ final innerBaseName = path.basename (innerEntity.path);
487+
488+ // Only extract the binary itself, skip LICENSE and other files
489+ if (innerBaseName == binary.binary ||
490+ innerBaseName == '${binary .binary }.exe' ||
491+ innerBaseName == path.basenameWithoutExtension (binary.binary)) {
492+ final targetFile = path.join (extractDir.path, innerBaseName);
493+ await safeMove (innerEntity, targetFile);
494+
495+ // Make binary executable on Unix-like systems
496+ if (! Platform .isWindows && innerEntity is File ) {
497+ await Process .run ('chmod' , ['+x' , targetFile]);
498+ }
499+ }
500+ }
501+ await entity.delete (recursive: true );
502+ continue ;
503+ }
504+
505+ await safeMove (entity, targetPath);
506+ }
507+
508+ // Clean up temp directory
509+ await tempDir.delete (recursive: true );
510+
511+ // Get the tar.gz name without extension
512+ final tarGzBaseName = path.basenameWithoutExtension (path.basenameWithoutExtension (tarGzPath));
513+ final expectedDirPath = path.join (extractDir.path, tarGzBaseName);
514+
515+ if (await Directory (expectedDirPath).exists ()) {
516+ final innerDir = Directory (expectedDirPath);
517+
518+ for (final entity in innerDir.listSync ()) {
519+ final entityBaseName = path.basename (entity.path);
520+
521+ // Only extract the binary itself, skip LICENSE and other files
522+ if (entityBaseName == binary.binary ||
523+ entityBaseName == '${binary .binary }.exe' ||
524+ entityBaseName == path.basenameWithoutExtension (binary.binary)) {
525+ final newPath = path.join (extractDir.path, entityBaseName);
526+ await safeMove (entity, newPath);
527+
528+ // Make binary executable on Unix-like systems
529+ if (! Platform .isWindows && entity is File ) {
530+ await Process .run ('chmod' , ['+x' , newPath]);
531+ }
532+ }
533+ }
534+ await innerDir.delete (recursive: true );
535+ }
536+ } catch (e) {
537+ log.e ('Extraction error: $e \n Stack trace: ${StackTrace .current }' );
538+ rethrow ;
539+ }
540+ }
541+
455542 /// Process raw binary file
456543 Future <void > _processRawBinary (Directory extractDir, String binaryPath, Binary binary) async {
457544 final downloadedFile = File (binaryPath);
0 commit comments