downloadPath method
Future<void>
downloadPath(
- String remotePath, {
- String? localDestination,
- required bool recursive,
- required String onConflict,
- required bool preserveTimestamps,
- required List<
String> include, - required List<
String> exclude, - required String batchId,
- Map<
String, dynamic> ? initialBatchState, - required Future<
void> saveStateCallback(), - int maxWorkers = kDefaultFileConcurrency,
Batch download by path with recursive support and resume.
Implementation
Future<void> downloadPath(
String remotePath, {
String? localDestination,
required bool recursive,
required String onConflict,
required bool preserveTimestamps,
required List<String> include,
required List<String> exclude,
required String batchId,
Map<String, dynamic>? initialBatchState,
required Future<void> Function(Map<String, dynamic>) saveStateCallback,
int maxWorkers = kDefaultFileConcurrency,
}) async {
final itemInfo = await drive.resolvePath(remotePath);
// Handle single file
if (itemInfo['type'] == 'file') {
final filename = p.basename(remotePath);
if (!shouldIncludeFile(filename, include, exclude)) return;
final localPath = localDestination != null &&
FileSystemEntity.isDirectorySync(localDestination)
? p.join(localDestination, filename)
: (localDestination ?? filename);
if (File(localPath).existsSync() && onConflict == 'skip') {
print('โญ๏ธ Skipping: $filename (exists)');
return;
}
print('๐ฅ Downloading: $filename');
await downloadFile(itemInfo['uuid'], savePath: localPath);
print('โ
Downloaded: $localPath');
return;
}
// Handle folder
if (itemInfo['type'] != 'folder') throw Exception("Unknown type");
if (!recursive) throw Exception("Use -r for recursive download");
final baseDestPath =
localDestination ?? (itemInfo['metadata']?['name'] ?? 'download');
await Directory(baseDestPath).create(recursive: true);
Map<String, dynamic> batchState;
List<dynamic> tasks;
if (initialBatchState != null) {
print("๐ Resuming batch...");
batchState = initialBatchState;
tasks = batchState['tasks'];
} else {
print("๐ Building task list (Fast)...");
tasks = [];
final treeData = await drive.getFlatFolderTree(itemInfo['uuid']);
final rawFolders = treeData['folders'] as List? ?? [];
final rawFiles =
(treeData['files'] as List?) ?? (treeData['uploads'] as List?) ?? [];
final folderMap = <String, Map<String, dynamic>>{};
for (var f in rawFolders) {
try {
String uuid, encName, parent;
if (f is List) {
if (f.length < 3) continue;
uuid = f[0];
encName = f[1];
parent = f[2];
} else {
if (f['deleted'] == true || f['trash'] == true) continue;
uuid = f['uuid'];
encName = f['name'];
parent = f['parent'];
}
var decName = await crypto.tryDecrypt(encName, masterKeys);
if (decName.startsWith('{')) {
decName = json.decode(decName)['name'];
}
folderMap[uuid] = {'name': decName, 'parent': parent};
} catch (_) {}
}
String? getRelPath(String? parentUuid) {
var parts = <String>[];
var curr = parentUuid;
var seen = <String>{};
while (curr != null && curr != itemInfo['uuid']) {
if (seen.contains(curr)) return null;
seen.add(curr);
if (!folderMap.containsKey(curr)) return null;
final f = folderMap[curr]!;
parts.add(f['name']);
curr = f['parent'];
}
if (curr == null && itemInfo['uuid'] != 'root') return null;
return parts.reversed.join(Platform.pathSeparator);
}
for (var f in rawFiles) {
try {
String uuid, encMeta, parent;
if (f is List) {
if (f.length < 6) continue;
uuid = f[0];
parent = f[4];
encMeta = f[5];
} else {
if (f['deleted'] == true || f['trash'] == true) continue;
uuid = f['uuid'];
parent = f['parent'];
encMeta = f['metadata'];
}
final decMeta = await crypto.tryDecrypt(encMeta, masterKeys);
final meta = json.decode(decMeta);
final filename = meta['name'];
final lastMod = meta['lastModified'] ?? 0;
if (!shouldIncludeFile(filename, include, exclude)) continue;
var relDir = getRelPath(parent);
if (parent == itemInfo['uuid'])
relDir = '';
else if (relDir == null) continue;
final localPath = p.join(baseDestPath, relDir, filename);
tasks.add({
'remoteUuid': uuid,
'localPath': localPath,
'status': 'pending',
'remoteModificationTime': lastMod,
});
} catch (e) {
api.log("File parse error: $e");
}
}
batchState = {
'operationType': 'download',
'remotePath': remotePath,
'localDestination': baseDestPath,
'tasks': tasks
};
await saveStateCallback(batchState);
print("๐ Task list: ${tasks.length} files");
}
// Execution
int successCount = 0;
int skippedCount = 0;
int errorCount = 0;
int completedPreviously = 0;
int processed = 0;
final totalTasks = tasks.length;
// Step 2: whole FILES downloaded at once. Capped at pending count, floored
// at 1 (single file / maxWorkers<=1 โ sequential path).
final pending = [
for (final t in tasks)
if ((t as Map<String, dynamic>)['status'] != 'completed') t
];
final effectiveWorkers =
max(1, min(maxWorkers, pending.isEmpty ? 1 : pending.length));
// Serialize the shared (whole-batchState) async saves under a 1-permit mutex.
final saveMutex = ChunkSemaphore(1);
Future<void> saveState() async {
await saveMutex.acquire();
try {
await saveStateCallback(batchState);
} finally {
saveMutex.release();
}
}
void tally(String token) {
switch (token) {
case 'completed':
successCount++;
break;
case 'skipped':
skippedCount++;
break;
case 'error':
errorCount++;
break;
case 'already':
completedPreviously++;
break;
}
processed++;
if (!api.debugMode) {
final pct = totalTasks > 0
? (processed / totalTasks * 100).toStringAsFixed(1)
: '0.0';
stdout.write('\rDown: $processed/$totalTasks files ($pct%) ');
}
}
if (effectiveWorkers <= 1) {
for (final t in tasks) {
tally(await _downloadTask(
t as Map<String, dynamic>,
onConflict: onConflict,
preserveTimestamps: preserveTimestamps,
saveState: saveState,
globalChunkSlots: null,
));
}
} else {
final globalChunkSlots = ChunkSemaphore(kGlobalMaxInflightChunks);
print(
" ๐งต Downloading ${pending.length} file(s) with $effectiveWorkers worker(s)");
await runWithConcurrency(tasks, effectiveWorkers, (t) async {
tally(await _downloadTask(
t as Map<String, dynamic>,
onConflict: onConflict,
preserveTimestamps: preserveTimestamps,
saveState: saveState,
globalChunkSlots: globalChunkSlots,
));
});
}
print('\n' + '=' * 40);
print('๐ Download Summary:');
if (completedPreviously > 0) print(' โ
Previous: $completedPreviously');
print(' โ
Downloaded: $successCount');
print(' โญ๏ธ Skipped: $skippedCount');
print(' โ Errors: $errorCount');
print('=' * 40);
}