|
1 | 1 | import 'dart:async'; |
2 | 2 | import 'dart:io'; |
3 | 3 |
|
4 | | -import 'package:archive/archive_io.dart'; |
5 | 4 | import 'package:http/http.dart' as http; |
6 | 5 | import 'package:path/path.dart' as p; |
7 | 6 | import 'package:runanywhere/core/types/model_types.dart'; |
@@ -323,39 +322,57 @@ class ModelDownloadService { |
323 | 322 | return Directory(modelPath); |
324 | 323 | } |
325 | 324 |
|
326 | | - /// Extract an archive to the destination using streaming to avoid OOM on large files |
| 325 | + /// Extract an archive using the system `tar`/`unzip` command. |
| 326 | + /// This runs in a separate process (non-blocking) and is orders of magnitude |
| 327 | + /// faster than the pure-Dart `archive` package for large model files (1GB+). |
| 328 | + /// Android has `tar` via toybox since API 23 (min SDK 24). |
327 | 329 | Future<String> _extractArchive( |
328 | 330 | String archivePath, |
329 | 331 | String destDir, |
330 | 332 | ModelArtifactType artifactType, |
331 | 333 | ) async { |
332 | 334 | _logger.info('Extracting archive: $archivePath'); |
333 | 335 |
|
334 | | - final supported = archivePath.endsWith('.tar.gz') || |
335 | | - archivePath.endsWith('.tgz') || |
336 | | - archivePath.endsWith('.tar.bz2') || |
337 | | - archivePath.endsWith('.tbz2') || |
338 | | - archivePath.endsWith('.zip') || |
339 | | - archivePath.endsWith('.tar'); |
340 | | - |
341 | | - if (!supported) { |
| 336 | + final List<String> args; |
| 337 | + |
| 338 | + if (archivePath.endsWith('.tar.gz') || archivePath.endsWith('.tgz')) { |
| 339 | + args = ['-xzf', archivePath, '-C', destDir]; |
| 340 | + } else if (archivePath.endsWith('.tar.bz2') || |
| 341 | + archivePath.endsWith('.tbz2')) { |
| 342 | + args = ['-xjf', archivePath, '-C', destDir]; |
| 343 | + } else if (archivePath.endsWith('.tar')) { |
| 344 | + args = ['-xf', archivePath, '-C', destDir]; |
| 345 | + } else if (archivePath.endsWith('.zip')) { |
| 346 | + // Use unzip for .zip files |
| 347 | + final result = await Process.run('unzip', ['-o', archivePath, '-d', destDir]); |
| 348 | + if (result.exitCode != 0) { |
| 349 | + throw Exception('unzip failed (exit ${result.exitCode}): ${result.stderr}'); |
| 350 | + } |
| 351 | + _logger.info('Extraction complete: $destDir'); |
| 352 | + return _resolveExtractedDir(destDir); |
| 353 | + } else { |
342 | 354 | _logger.warning('Unknown archive format: $archivePath'); |
343 | 355 | return archivePath; |
344 | 356 | } |
345 | 357 |
|
346 | | - // Use streaming extraction — reads file in chunks, never loads entire archive into RAM |
347 | | - await extractFileToDisk(archivePath, destDir); |
| 358 | + // Run tar extraction — runs as a separate OS process, does not block the Dart event loop |
| 359 | + final result = await Process.run('tar', args); |
| 360 | + if (result.exitCode != 0) { |
| 361 | + throw Exception('tar failed (exit ${result.exitCode}): ${result.stderr}'); |
| 362 | + } |
348 | 363 |
|
349 | 364 | _logger.info('Extraction complete: $destDir'); |
| 365 | + return _resolveExtractedDir(destDir); |
| 366 | + } |
350 | 367 |
|
351 | | - // Return the nested root directory if one exists inside destDir |
| 368 | + /// If extraction produced a single subdirectory, return that path instead. |
| 369 | + Future<String> _resolveExtractedDir(String destDir) async { |
352 | 370 | final destDirectory = Directory(destDir); |
353 | 371 | final entries = await destDirectory.list().toList(); |
354 | 372 | final subdirs = entries.whereType<Directory>().toList(); |
355 | 373 | if (subdirs.length == 1) { |
356 | 374 | return subdirs.first.path; |
357 | 375 | } |
358 | | - |
359 | 376 | return destDir; |
360 | 377 | } |
361 | 378 |
|
|
0 commit comments