download method

  1. @override
Future<Response> download(
  1. String urlPath,
  2. dynamic savePath, {
  3. ProgressCallback? onReceiveProgress,
  4. Map<String, dynamic>? queryParameters,
  5. CancelToken? cancelToken,
  6. bool deleteOnError = true,
  7. String lengthHeader = Headers.contentLengthHeader,
  8. dynamic data,
  9. Options? options,
})
override

Download the file and save it in local. The default http method is "GET", you can custom it by Options.method.

urlPath: The file url.

savePath: The path to save the downloading file later. it can be a String or a callback String Function(Headers):

  1. A path with String type, eg "xs.jpg"
  2. A callback String Function(Headers); for example:
 await dio.download(url,(Headers headers){
      // Extra info: redirect counts
      print(headers.value('redirects'));
      // Extra info: real uri
      print(headers.value('uri'));
    ...
    return "...";
  });

onReceiveProgress: The callback to listen downloading progress. please refer to ProgressCallback.

deleteOnError Whether delete the file when error occurs. The default value is true.

lengthHeader : The real size of original file (not compressed). When file is compressed:

  1. If this value is 'content-length', the total argument of onProgress will be -1
  2. If this value is not 'content-length', maybe a custom header indicates the original file size , the total argument of onProgress will be this header value.

you can also disable the compression by specifying the 'accept-encoding' header value as '*' to assure the value of total argument of onProgress is not -1. for example:

await dio.download(url, "./example/flutter.svg",
options: Options(headers: {HttpHeaders.acceptEncodingHeader: "*"}),  // disable gzip
onProgress: (received, total) {
  if (total != -1) {
   print((received / total * 100).toStringAsFixed(0) + "%");
  }
});

Implementation

@override
Future<Response> download(
  String urlPath,
  savePath, {
  ProgressCallback? onReceiveProgress,
  Map<String, dynamic>? queryParameters,
  CancelToken? cancelToken,
  bool deleteOnError = true,
  String lengthHeader = Headers.contentLengthHeader,
  data,
  Options? options,
}) async {
  // We set the `responseType` to [ResponseType.STREAM] to retrieve the
  // response stream.
  options ??= DioMixin.checkOptions('GET', options);

  // Receive data with stream.
  options.responseType = ResponseType.stream;
  Response<ResponseBody> response;
  try {
    response = await request<ResponseBody>(
      urlPath,
      data: data,
      options: options,
      queryParameters: queryParameters,
      cancelToken: cancelToken ?? CancelToken(),
    );
  } on DioError catch (e) {
    if (e.type == DioErrorType.response) {
      if (e.response!.requestOptions.receiveDataWhenStatusError == true) {
        var res = await transformer.transformResponse(
          e.response!.requestOptions..responseType = ResponseType.json,
          e.response!.data as ResponseBody,
        );
        e.response!.data = res;
      } else {
        e.response!.data = null;
      }
    }
    rethrow;
  }

  response.headers = Headers.fromMap(response.data!.headers);

  File file;
  if (savePath is Function) {
    assert(savePath is String Function(Headers),
        'savePath callback type must be `String Function(HttpHeaders)`');

    // Add real uri and redirect information to headers
    response.headers
      ..add('redirects', response.redirects.length.toString())
      ..add('uri', response.realUri.toString());

    file = File(savePath(response.headers) as String);
  } else {
    file = File(savePath.toString());
  }

  //If directory (or file) doesn't exist yet, the entire method fails
  file.createSync(recursive: true);

  // Shouldn't call file.writeAsBytesSync(list, flush: flush),
  // because it can write all bytes by once. Consider that the
  // file with a very big size(up 1G), it will be expensive in memory.
  var raf = file.openSync(mode: FileMode.write);

  //Create a Completer to notify the success/error state.
  var completer = Completer<Response>();
  var future = completer.future;
  var received = 0;

  // Stream<Uint8List>
  var stream = response.data!.stream;
  var compressed = false;
  var total = 0;
  var contentEncoding = response.headers.value(Headers.contentEncodingHeader);
  if (contentEncoding != null) {
    compressed = ['gzip', 'deflate', 'compress'].contains(contentEncoding);
  }
  if (lengthHeader == Headers.contentLengthHeader && compressed) {
    total = -1;
  } else {
    total = int.parse(response.headers.value(lengthHeader) ?? '-1');
  }

  late StreamSubscription subscription;
  Future? asyncWrite;
  var closed = false;
  Future _closeAndDelete() async {
    if (!closed) {
      closed = true;
      await asyncWrite;
      await raf.close();
      if (deleteOnError && file.existsSync()) {
        await file.delete();
      }
    }
  }

  subscription = stream.listen(
    (data) {
      subscription.pause();
      // Write file asynchronously
      asyncWrite = raf.writeFrom(data).then((_raf) {
        // Notify progress
        received += data.length;

        onReceiveProgress?.call(received, total);

        raf = _raf;
        if (cancelToken == null || !cancelToken.isCancelled) {
          subscription.resume();
        }
      }).catchError((err, StackTrace stackTrace) async {
        try {
          await subscription.cancel();
        } finally {
          completer.completeError(DioMixin.assureDioError(
            err,
            response.requestOptions,
          ));
        }
      });
    },
    onDone: () async {
      try {
        await asyncWrite;
        closed = true;
        await raf.close();
        completer.complete(response);
      } catch (e) {
        completer.completeError(DioMixin.assureDioError(
          e,
          response.requestOptions,
        ));
      }
    },
    onError: (e) async {
      try {
        await _closeAndDelete();
      } finally {
        completer.completeError(DioMixin.assureDioError(
          e,
          response.requestOptions,
        ));
      }
    },
    cancelOnError: true,
  );
  // ignore: unawaited_futures
  cancelToken?.whenCancel.then((_) async {
    await subscription.cancel();
    await _closeAndDelete();
  });

  if (response.requestOptions.receiveTimeout > 0) {
    future = future
        .timeout(Duration(
      milliseconds: response.requestOptions.receiveTimeout,
    ))
        .catchError((Object err) async {
      await subscription.cancel();
      await _closeAndDelete();
      if (err is TimeoutException) {
        throw DioError(
          requestOptions: response.requestOptions,
          error:
              'Receiving data timeout[${response.requestOptions.receiveTimeout}ms]',
          type: DioErrorType.receiveTimeout,
        );
      } else {
        throw err;
      }
    });
  }
  return DioMixin.listenCancelForAsyncTask(cancelToken, future);
}