downloadFile function

Future<File> downloadFile({
  1. required String url,
  2. required String filepath,
  3. Map<String, String>? headers,
  4. BaseCacheManager? cacheManager,
  5. String? cacheKey,
  6. DownloadOption? option,
})

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);
  }
}