download method
Future<Response>
download(
- String urlPath,
- dynamic savePath, {
- ProgressCallback? onReceiveProgress,
- Map<
String, dynamic> ? queryParameters, - CancelToken? cancelToken,
- bool deleteOnError = true,
- String lengthHeader = Headers.contentLengthHeader,
- dynamic data,
- 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:
- A path with String type, eg "xs.jpg"
- 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:
- If this value is 'content-length', the
total
argument ofonProgress
will be -1 - If this value is not 'content-length', maybe a custom header indicates the original
file size , the
total
argument ofonProgress
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,
);
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));
} 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) await file.delete();
}
}
subscription = stream.listen(
(data) {
subscription.pause();
// Write file asynchronously
asyncWrite = raf.writeFrom(data).then((_raf) {
// Notify progress
received += data.length;
if (onReceiveProgress != null) {
onReceiveProgress(received, total);
}
raf = _raf;
if (cancelToken == null || !cancelToken.isCancelled) {
subscription.resume();
}
}).catchError((err, stackTrace) async {
try {
await subscription.cancel();
} finally {
completer.completeError(DioMixin.assureDioError(
err, response.requestOptions, stackTrace));
}
});
},
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((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);
}