download method
- String urlPath,
- dynamic savePath, {
- ProgressCallback? onReceiveProgress,
- Map<
String, dynamic> ? queryParameters, - CancelToken? cancelToken,
- bool deleteOnError = true,
- FileAccessMode fileAccessMode = FileAccessMode.write,
- String lengthHeader = Headers.contentLengthHeader,
- Object? data,
- Options? options,
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'; }, );
On Web, browsers do not allow writing to arbitrary local file paths.
In that environment, savePath is used as the suggested filename for the
browser download. The browser decides the actual saved location, and the
returned Response only means the response was fetched and the download
was triggered. Web downloads load the whole response into memory before
triggering the browser download, remain subject to CORS, do not support
FileAccessMode.append, and ignore deleteOnError because there is no
local file managed by Dio. The lengthHeader override is also not used
on Web; progress totals come from the browser response progress event.
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.
fileAccessMode
{@macro dio.options.FileAccessMode}
lengthHeader : The real size of original file (not compressed).
When file is compressed:
- If this value is 'content-length', the
totalargument ofonReceiveProgresswill be -1. - If this value is not 'content-length', maybe a custom header indicates
the original file size, the
totalargument ofonReceiveProgresswill 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,
FileAccessMode fileAccessMode = FileAccessMode.write,
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: fileAccessMode == FileAccessMode.write
? FileMode.write
: FileMode.append,
);
// 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);
}