downloadFile method
Download a single file by UUID.
Implementation
Future<Map<String, dynamic>> downloadFile(
String uuid, {
String? savePath,
int maxConcurrentChunks = kDefaultDownloadConcurrency,
ChunkSemaphore? globalChunkSlots,
Function(int bytesDownloaded, int totalBytes)? onProgress,
}) async {
api.log('Downloading file: $uuid');
final info = await api.post('/v3/file', {'uuid': uuid});
final d = info['data'];
final metaStr = await crypto.tryDecrypt(d['metadata'], masterKeys);
final meta = json.decode(metaStr);
final keyBytes = crypto.decodeUniversalKey(meta['key']);
final chunks = int.parse(d['chunks'].toString());
final host = 'https://egest.filen.io';
final filename = meta['name'] ?? 'file';
final fileSize = meta['size'] ?? 0;
final modificationTime = meta['lastModified'];
if (onProgress == null) {
print(' 📄 File: $filename (${formatSize(fileSize)})');
}
final targetPath = savePath ?? filename;
// Tiny files keep the simple sequential streaming path.
final useConcurrency =
maxConcurrentChunks > 1 && chunks > kSequentialDownloadChunkThreshold;
int bytesDownloaded = 0;
if (!useConcurrency) {
final sink = File(targetPath).openWrite();
try {
for (var i = 0; i < chunks; i++) {
// Shared batch budget (Step 2): one permit per chunk in flight across
// the batch. No-op for a lone file (globalChunkSlots == null).
if (globalChunkSlots != null) await globalChunkSlots.acquire();
Uint8List decrypted;
try {
decrypted = await _fetchChunk(host, d, uuid, i, keyBytes);
} finally {
if (globalChunkSlots != null) globalChunkSlots.release();
}
sink.add(decrypted);
bytesDownloaded += decrypted.length;
if (onProgress != null) onProgress(bytesDownloaded, fileSize);
}
} finally {
await sink.close();
}
} else {
// Fetch N chunks concurrently and write each at its fixed offset (every
// plaintext chunk is exactly 1 MB except the last), so out-of-order
// completion still reassembles byte-exactly. A 1-permit lock serialises
// the seek+write critical section. At most N decrypted chunks are live.
final raf = await File(targetPath).open(mode: FileMode.write);
try {
if (fileSize is int && fileSize > 0) {
await raf.truncate(fileSize);
}
final sem = ChunkSemaphore(maxConcurrentChunks);
final writeLock = ChunkSemaphore(1);
final inflight = <Future<void>>[];
Object? firstError;
for (var i = 0; i < chunks; i++) {
if (firstError != null) break;
await sem.acquire();
if (firstError != null) {
sem.release();
break;
}
final idx = i;
inflight.add(() async {
try {
// Shared batch budget (Step 2): bound chunk fetches in flight
// across the WHOLE batch, not just this file.
if (globalChunkSlots != null) await globalChunkSlots.acquire();
Uint8List decrypted;
try {
decrypted = await _fetchChunk(host, d, uuid, idx, keyBytes);
} finally {
if (globalChunkSlots != null) globalChunkSlots.release();
}
await writeLock.acquire();
try {
await raf.setPosition(idx * _kDownloadChunkSize);
await raf.writeFrom(decrypted);
} finally {
writeLock.release();
}
bytesDownloaded += decrypted.length;
if (onProgress != null) onProgress(bytesDownloaded, fileSize);
} catch (e) {
firstError ??= e;
} finally {
sem.release();
}
}());
}
await Future.wait(inflight);
if (firstError != null) throw firstError!;
} finally {
await raf.close();
}
}
return {
'data': await File(targetPath).readAsBytes(),
'filename': filename,
'modificationTime': modificationTime,
};
}