getOrphanedFiles static method
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;
}