getOrphanedFiles static method

Future<List<OrphanedFileInfo>> getOrphanedFiles({
  1. List<String>? protectedFiles,
  2. List<String>? supportedExtensions,
})

Get information about orphaned files without deleting them

⚠️ This method only returns information, it does NOT delete files. Call cleanupOrphanedFiles() explicitly to delete them.

Implementation

static Future<List<OrphanedFileInfo>> getOrphanedFiles({
  List<String>? protectedFiles,
  List<String>? supportedExtensions,
}) async {
  final storageDirPath = await ServiceRegistry.instance.fileSystemService
      .getModelStorageDirectory();
  final directory = Directory(storageDirPath);
  final extensions =
      supportedExtensions ?? ModelFileSystemManager.supportedExtensions;
  final protected = protectedFiles ?? [];

  final orphaned = <OrphanedFileInfo>[];

  final files = directory
      .listSync()
      .whereType<File>()
      .where((file) => extensions.any((ext) => file.path.endsWith(ext)))
      .toList();

  for (final file in files) {
    final fileName = path.basename(file.path);

    if (protected.contains(fileName)) {
      continue;
    }

    // Check if has active task
    final hasTask = await _hasActiveDownloadTask(fileName);
    if (hasTask) {
      continue;
    }

    // This file is orphaned
    final stat = await file.stat();
    orphaned.add(
      OrphanedFileInfo(
        filename: fileName,
        path: file.path,
        sizeBytes: stat.size,
        lastModified: stat.modified,
      ),
    );
  }

  // Downloader partial temps are extensionless and live in applicationSupport
  // (Android filesDir), outside the model dir — invisible to the loop above
  // (#383/#3, #6). Scan for them so getOrphanedFiles/cleanupStorage can report
  // and delete them. Android-only: iOS uses opaque URLSession resume data with
  // no filesDir temp. A live/resumable temp is spared via the resume keep-set;
  // explicit cleanup additionally cancels all group tasks first (#383/#5).
  if (Platform.isAndroid) {
    try {
      final downloader = FileDownloader();
      // ignore: invalid_use_of_visible_for_testing_member
      final keep = (await downloader.database.storage.retrieveAllResumeData())
          .map((r) => r.tempFilepath)
          .toSet();
      final supportDir = await getApplicationSupportDirectory();
      for (final entity in supportDir.listSync(followLinks: false)) {
        if (entity is! File) continue;
        final name = path.basename(entity.path);
        if (!isDownloadFragmentName(name)) continue;
        if (keep.contains(entity.path)) continue;
        try {
          final stat = await entity.stat();
          orphaned.add(
            OrphanedFileInfo(
              filename: name,
              path: entity.path,
              sizeBytes: stat.size,
              lastModified: stat.modified,
              isDownloadFragment: true,
            ),
          );
        } catch (_) {
          continue; // vanished mid-scan (e.g. a concurrent reclaim)
        }
      }
    } catch (e) {
      gemmaLog('Fragment scan failed (non-fatal): $e');
    }
  }

  return orphaned;
}