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. Object? 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 is the file url.

savePath is the path to save the downloading file later. it can be a String or a callback:

  1. A path with String type, eg "xs.jpg"
  2. A callback String Function(Headers 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 (await getTemporaryDirectory()).path + 'file_name';
  },
);

onReceiveProgress is 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,
  (await getTemporaryDirectory()).path + '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,
  dynamic savePath, {
  ProgressCallback? onReceiveProgress,
  Map<String, dynamic>? queryParameters,
  CancelToken? cancelToken,
  bool deleteOnError = true,
  String lengthHeader = Headers.contentLengthHeader,
  Object? 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.badResponse) {
      if (e.response!.requestOptions.receiveDataWhenStatusError == true) {
        final 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;
  }
  final File file;
  if (savePath is FutureOr<String> Function(Headers)) {
    // Add real Uri and redirect information to headers.
    response.headers
      ..add('redirects', response.redirects.length.toString())
      ..add('uri', response.realUri.toString());
    file = File(await savePath(response.headers));
  } else if (savePath is String) {
    file = File(savePath);
  } else {
    throw ArgumentError.value(
      savePath.runtimeType,
      'savePath',
      'The type must be `String` or `FutureOr<String> Function(Headers)`.',
    );
  }

  // If the 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 is
  // a very big size (up to 1 Gigabytes), it will be expensive in memory.
  RandomAccessFile raf = file.openSync(mode: FileMode.write);

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

  // Stream<Uint8List>
  final stream = response.data!.stream;
  bool compressed = false;
  int total = 0;
  final 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');
  }

  Future<void>? asyncWrite;
  bool closed = false;
  Future<void> closeAndDelete() async {
    if (!closed) {
      closed = true;
      await asyncWrite;
      await raf.close();
      if (deleteOnError && file.existsSync()) {
        await file.delete();
      }
    }
  }

  late StreamSubscription subscription;
  subscription = stream.listen(
    (data) {
      subscription.pause();
      // Write file asynchronously
      asyncWrite = raf.writeFrom(data).then((result) {
        // Notify progress
        received += data.length;
        onReceiveProgress?.call(received, total);
        raf = result;
        if (cancelToken == null || !cancelToken.isCancelled) {
          subscription.resume();
        }
      }).catchError((dynamic e, StackTrace s) async {
        try {
          await subscription.cancel();
        } finally {
          completer.completeError(
            DioMixin.assureDioError(e, response.requestOptions, s),
          );
        }
      });
    },
    onDone: () async {
      try {
        await asyncWrite;
        closed = true;
        await raf.close();
        completer.complete(response);
      } catch (e, s) {
        completer.completeError(
          DioMixin.assureDioError(e, response.requestOptions, s),
        );
      }
    },
    onError: (e, s) async {
      try {
        await closeAndDelete();
      } finally {
        completer.completeError(
          DioMixin.assureDioError(e, response.requestOptions, s),
        );
      }
    },
    cancelOnError: true,
  );
  cancelToken?.whenCancel.then((_) async {
    await subscription.cancel();
    await closeAndDelete();
  });

  final timeout = response.requestOptions.receiveTimeout;
  if (timeout != null) {
    future = future.timeout(timeout).catchError(
      (dynamic e, StackTrace s) async {
        await subscription.cancel();
        await closeAndDelete();
        if (e is TimeoutException) {
          throw DioError.receiveTimeout(
            timeout: timeout,
            requestOptions: response.requestOptions,
            stackTrace: s,
          );
        } else {
          throw e;
        }
      },
    );
  }
  return DioMixin.listenCancelForAsyncTask(cancelToken, future);
}