downloadFile function
Downloads file from url
with headers
and saves to filepath
. Here cacheManager
defaults to
DefaultCacheManager()
, and cacheKey
defaults to url
, and option
defines detailed downloading
option, such as downloading behaviors.
Note that this function will only throw exceptions with type of DownloadException, you can perform the corresponding operations by checking DownloadException.type.
Implementation
Future<File> downloadFile({
required String url,
required String filepath,
Map<String, String>? headers,
BaseCacheManager? cacheManager,
String? cacheKey,
DownloadOption? option,
}) async {
cacheManager ??= DefaultCacheManager();
cacheKey ??= url;
option ??= const DownloadOption();
// 1. parse given url
Uri uri;
try {
uri = Uri.parse(url);
} catch (e, s) {
throw DownloadException._fromError(e, s); // FormatException
}
// 2. make http HEAD request asynchronously
Future<TaskResult<String, DownloadException>> filepathFuture;
if (option.redecideHandler == null) {
filepathFuture = Future.value(Ok(filepath));
} else {
filepathFuture = Future<TaskResult<String, DownloadException>>.microtask(() async {
http.Response resp;
try {
var future = http.head(uri, headers: headers);
if (option!.headTimeout != null) {
future = future.timeout(option.headTimeout!);
}
resp = await future;
} on TimeoutException catch (e, s) {
throw DownloadException._head('Failed to make http HEAD request to "$url": timed out.', e, s);
} catch (e, s) {
throw DownloadException._head('Failed to make http HEAD request to "$url": $e.', e, s);
}
if (resp.statusCode != 200 && resp.statusCode != 201) {
throw DownloadException._head('Got invalid status code ${resp.statusCode} from "$url".');
}
var filepath = option.redecideHandler!.call(resp.headers, resp.headers['content-type'] ?? '');
return Ok(filepath);
}).onError((e, s) {
if (!option!.ignoreHeadError) {
var err = DownloadException._fromError(e!, s);
return Future.value(Err(err));
}
var filepath = option.redecideHandler!.call(null, null);
return Future.value(Ok(filepath));
});
}
// 3. check file existence asynchronously
var fileFuture = filepathFuture.then<TaskResult<File, DownloadException>>((result) async {
var filepath = result.unwrap();
var newFile = File(filepath);
if (await newFile.exists()) {
var behavior = await option!.conflictHandler.call(filepath);
switch (behavior) {
case DownloadConflictBehavior.notAllow:
throw DownloadException._conflict('File "$filepath" has already existed before saving.');
case DownloadConflictBehavior.overwrite:
await newFile.delete();
break;
case DownloadConflictBehavior.addSuffix:
for (var i = 2;; i++) {
var basename = path_.withoutExtension(filepath);
var extension = path_.extension(filepath); // .xxx
var fallbackFile = File('$basename${option.suffixBuilder(i)}$extension');
if (!(await fallbackFile.exists())) {
newFile = fallbackFile;
break;
}
}
break;
}
}
await newFile.create(recursive: true);
return Ok(newFile);
}).onError((e, s) {
var err = DownloadException._fromError(e!, s);
return Future.value(Err(err));
});
try {
// 4. check cache, save cached data to file
if (option.behavior != DownloadBehavior.forceDownload) {
var cached = await cacheManager.getFileFromCache(cacheKey);
if (cached == null || cached.validTill.isBefore(DateTime.now())) {
if (option.behavior == DownloadBehavior.mustUseCache) {
throw DownloadException._cacheMiss('There is no valid data for "$cacheKey" in cache.');
}
} else {
var destination = (await fileFuture).unwrap();
return await cached.file.copy(destination.path);
}
}
// 5. download data from url
http.Response resp;
try {
var future = http.get(uri, headers: headers);
if (option.downloadTimeout != null) {
future = future.timeout(option.downloadTimeout!);
}
resp = await future;
} on TimeoutException catch (e, s) {
throw DownloadException._download('Failed to make http GET request to "$url": timed out.', e, s);
} catch (e, s) {
throw DownloadException._download('Failed to make http GET request to "$url": $e.', e, s);
}
if (resp.statusCode != 200 && resp.statusCode != 201) {
throw DownloadException._download('Got invalid status code ${resp.statusCode} from "$url".');
}
// 6. save downloaded data to file, update cache
var data = resp.bodyBytes;
if (option.alsoUpdateCache) {
unawaited(cacheManager.putFile(url, data, key: cacheKey, maxAge: option.maxAgeForCache));
}
var targetFile = (await fileFuture).unwrap();
return await targetFile.writeAsBytes(data, flush: true);
} catch (e, s) {
try {
await (await fileFuture).data?.delete(); // clear failed file
} catch (_) {}
throw DownloadException._fromError(e, s);
}
}