download method
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,
inherited
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.
The file will be saved to the path specified by savePath
.
The following two types are accepted:
String
: A path, eg "xs.jpg"FutureOr<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:
- If this value is 'content-length', the
total
argument ofonReceiveProgress
will be -1. - If this value is not 'content-length', maybe a custom header indicates
the original file size, the
total
argument ofonReceiveProgress
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
onReceiveProgress
is not -1. For example:
await dio.download(
url,
(await getTemporaryDirectory()).path + 'flutter.svg',
options: Options(
headers: {HttpHeaders.acceptEncodingHeader: '*'}, // Disable gzip
),
onReceiveProgress: (received, total) {
if (total <= 0) return;
print('percentage: ${(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 {
options ??= DioMixin.checkOptions('GET', options);
// Manually set the `responseType` to [ResponseType.stream]
// to retrieve the response stream.
// Do not modify previous options.
options = options.copyWith(responseType: ResponseType.stream);
final Response<ResponseBody> response;
try {
response = await request<ResponseBody>(
urlPath,
data: data,
options: options,
queryParameters: queryParameters,
cancelToken: cancelToken,
);
} on DioException catch (e) {
if (e.type == DioExceptionType.badResponse) {
final response = e.response!;
if (response.requestOptions.receiveDataWhenStatusError == true) {
final ResponseType implyResponseType;
final contentType = response.headers.value(Headers.contentTypeHeader);
if (contentType != null && contentType.startsWith('text/')) {
implyResponseType = ResponseType.plain;
} else {
implyResponseType = ResponseType.json;
}
final res = await transformer.transformResponse(
response.requestOptions.copyWith(responseType: implyResponseType),
response.data as ResponseBody,
);
response.data = res;
} else {
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 file already exists, the 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>();
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().catchError((_) => raf);
if (deleteOnError && file.existsSync()) {
await file.delete().catchError((_) => file);
}
}
}
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((Object e) async {
try {
await subscription.cancel().catchError((_) {});
closed = true;
await raf.close().catchError((_) => raf);
if (deleteOnError && file.existsSync()) {
await file.delete().catchError((_) => file);
}
} finally {
completer.completeError(
DioMixin.assureDioException(e, response.requestOptions),
);
}
});
},
onDone: () async {
try {
await asyncWrite;
closed = true;
await raf.close().catchError((_) => raf);
completer.complete(response);
} catch (e) {
completer.completeError(
DioMixin.assureDioException(e, response.requestOptions),
);
}
},
onError: (e) async {
try {
await closeAndDelete();
} finally {
completer.completeError(
DioMixin.assureDioException(e, response.requestOptions),
);
}
},
cancelOnError: true,
);
cancelToken?.whenCancel.then((_) async {
await subscription.cancel();
await closeAndDelete();
});
return DioMixin.listenCancelForAsyncTask(cancelToken, completer.future);
}