Line data Source code
1 : import 'dart:async';
2 : import 'dart:io';
3 : import '../adapter.dart';
4 : import '../cancel_token.dart';
5 : import '../dio_mixin.dart';
6 : import '../response.dart';
7 : import '../dio.dart';
8 : import '../headers.dart';
9 : import '../options.dart';
10 : import '../dio_error.dart';
11 : import '../adapters/io_adapter.dart';
12 :
13 16 : Dio createDio([BaseOptions? baseOptions]) => DioForNative(baseOptions);
14 :
15 : class DioForNative with DioMixin implements Dio {
16 : /// Create Dio instance with default [BaseOptions].
17 : /// It is recommended that an application use only the same DIO singleton.
18 8 : DioForNative([BaseOptions? baseOptions]) {
19 16 : options = baseOptions ?? BaseOptions();
20 16 : httpClientAdapter = DefaultHttpClientAdapter();
21 : }
22 :
23 : /// Download the file and save it in local. The default http method is "GET",
24 : /// you can custom it by [Options.method].
25 : ///
26 : /// [urlPath]: The file url.
27 : ///
28 : /// [savePath]: The path to save the downloading file later. it can be a String or
29 : /// a callback [String Function(Headers)]:
30 : /// 1. A path with String type, eg "xs.jpg"
31 : /// 2. A callback `String Function(Headers)`; for example:
32 : /// ```dart
33 : /// await dio.download(url,(Headers headers){
34 : /// // Extra info: redirect counts
35 : /// print(headers.value('redirects'));
36 : /// // Extra info: real uri
37 : /// print(headers.value('uri'));
38 : /// ...
39 : /// return "...";
40 : /// });
41 : /// ```
42 : ///
43 : /// [onReceiveProgress]: The callback to listen downloading progress.
44 : /// please refer to [ProgressCallback].
45 : ///
46 : /// [deleteOnError] Whether delete the file when error occurs. The default value is [true].
47 : ///
48 : /// [lengthHeader] : The real size of original file (not compressed).
49 : /// When file is compressed:
50 : /// 1. If this value is 'content-length', the `total` argument of `onProgress` will be -1
51 : /// 2. If this value is not 'content-length', maybe a custom header indicates the original
52 : /// file size , the `total` argument of `onProgress` will be this header value.
53 : ///
54 : /// you can also disable the compression by specifying the 'accept-encoding' header value as '*'
55 : /// to assure the value of `total` argument of `onProgress` is not -1. for example:
56 : ///
57 : /// await dio.download(url, "./example/flutter.svg",
58 : /// options: Options(headers: {HttpHeaders.acceptEncodingHeader: "*"}), // disable gzip
59 : /// onProgress: (received, total) {
60 : /// if (total != -1) {
61 : /// print((received / total * 100).toStringAsFixed(0) + "%");
62 : /// }
63 : /// });
64 : @override
65 1 : Future<Response> download(
66 : String urlPath,
67 : savePath, {
68 : ProgressCallback? onReceiveProgress,
69 : Map<String, dynamic>? queryParameters,
70 : CancelToken? cancelToken,
71 : bool deleteOnError = true,
72 : String lengthHeader = Headers.contentLengthHeader,
73 : data,
74 : Options? options,
75 : }) async {
76 : // We set the `responseType` to [ResponseType.STREAM] to retrieve the
77 : // response stream.
78 1 : options ??= DioMixin.checkOptions('GET', options);
79 :
80 : // Receive data with stream.
81 1 : options.responseType = ResponseType.stream;
82 : Response<ResponseBody> response;
83 : try {
84 2 : response = await request<ResponseBody>(
85 : urlPath,
86 : data: data,
87 : options: options,
88 : queryParameters: queryParameters,
89 1 : cancelToken: cancelToken ?? CancelToken(),
90 : );
91 1 : } on DioError catch (e) {
92 2 : if (e.type == DioErrorType.response) {
93 4 : if (e.response!.requestOptions.receiveDataWhenStatusError == true) {
94 3 : var res = await transformer.transformResponse(
95 3 : e.response!.requestOptions..responseType = ResponseType.json,
96 2 : e.response!.data as ResponseBody,
97 : );
98 2 : e.response!.data = res;
99 : } else {
100 2 : e.response!.data = null;
101 : }
102 : }
103 : rethrow;
104 : }
105 :
106 4 : response.headers = Headers.fromMap(response.data!.headers);
107 :
108 : File file;
109 1 : if (savePath is Function) {
110 1 : assert(savePath is String Function(Headers),
111 : 'savePath callback type must be `String Function(HttpHeaders)`');
112 :
113 : // Add real uri and redirect information to headers
114 1 : response.headers
115 4 : ..add('redirects', response.redirects.length.toString())
116 3 : ..add('uri', response.realUri.toString());
117 :
118 3 : file = File(savePath(response.headers) as String);
119 : } else {
120 2 : file = File(savePath.toString());
121 : }
122 :
123 : //If directory (or file) doesn't exist yet, the entire method fails
124 1 : file.createSync(recursive: true);
125 :
126 : // Shouldn't call file.writeAsBytesSync(list, flush: flush),
127 : // because it can write all bytes by once. Consider that the
128 : // file with a very big size(up 1G), it will be expensive in memory.
129 1 : var raf = file.openSync(mode: FileMode.write);
130 :
131 : //Create a Completer to notify the success/error state.
132 1 : var completer = Completer<Response>();
133 1 : var future = completer.future;
134 : var received = 0;
135 :
136 : // Stream<Uint8List>
137 2 : var stream = response.data!.stream;
138 : var compressed = false;
139 : var total = 0;
140 2 : var contentEncoding = response.headers.value(Headers.contentEncodingHeader);
141 : if (contentEncoding != null) {
142 2 : compressed = ['gzip', 'deflate', 'compress'].contains(contentEncoding);
143 : }
144 1 : if (lengthHeader == Headers.contentLengthHeader && compressed) {
145 0 : total = -1;
146 : } else {
147 3 : total = int.parse(response.headers.value(lengthHeader) ?? '-1');
148 : }
149 :
150 : late StreamSubscription subscription;
151 : Future? asyncWrite;
152 : var closed = false;
153 1 : Future _closeAndDelete() async {
154 : if (!closed) {
155 : closed = true;
156 1 : await asyncWrite;
157 2 : await raf.close();
158 2 : if (deleteOnError) await file.delete();
159 : }
160 : }
161 :
162 1 : subscription = stream.listen(
163 1 : (data) {
164 1 : subscription.pause();
165 : // Write file asynchronously
166 3 : asyncWrite = raf.writeFrom(data).then((_raf) {
167 : // Notify progress
168 2 : received += data.length;
169 :
170 : onReceiveProgress?.call(received, total);
171 :
172 : raf = _raf;
173 0 : if (cancelToken == null || !cancelToken.isCancelled) {
174 1 : subscription.resume();
175 : }
176 1 : }).catchError((err, StackTrace stackTrace) async {
177 : try {
178 0 : await subscription.cancel();
179 : } finally {
180 0 : completer.completeError(DioMixin.assureDioError(
181 : err,
182 0 : response.requestOptions,
183 : ));
184 : }
185 : });
186 : },
187 1 : onDone: () async {
188 : try {
189 1 : await asyncWrite;
190 : closed = true;
191 2 : await raf.close();
192 1 : completer.complete(response);
193 : } catch (e) {
194 0 : completer.completeError(DioMixin.assureDioError(
195 : e,
196 0 : response.requestOptions,
197 : ));
198 : }
199 : },
200 0 : onError: (e) async {
201 : try {
202 0 : await _closeAndDelete();
203 : } finally {
204 0 : completer.completeError(DioMixin.assureDioError(
205 : e,
206 0 : response.requestOptions,
207 : ));
208 : }
209 : },
210 : cancelOnError: true,
211 : );
212 : // ignore: unawaited_futures
213 3 : cancelToken?.whenCancel.then((_) async {
214 2 : await subscription.cancel();
215 1 : await _closeAndDelete();
216 : });
217 :
218 3 : if (response.requestOptions.receiveTimeout > 0) {
219 : future = future
220 2 : .timeout(Duration(
221 2 : milliseconds: response.requestOptions.receiveTimeout,
222 : ))
223 2 : .catchError((Object err) async {
224 2 : await subscription.cancel();
225 1 : await _closeAndDelete();
226 1 : if (err is TimeoutException) {
227 1 : throw DioError(
228 1 : requestOptions: response.requestOptions,
229 : error:
230 3 : 'Receiving data timeout[${response.requestOptions.receiveTimeout}ms]',
231 : type: DioErrorType.receiveTimeout,
232 : );
233 : } else {
234 : throw err;
235 : }
236 : });
237 : }
238 1 : return DioMixin.listenCancelForAsyncTask(cancelToken, future);
239 : }
240 :
241 : /// Download the file and save it in local. The default http method is "GET",
242 : /// you can custom it by [Options.method].
243 : ///
244 : /// [uri]: The file url.
245 : ///
246 : /// [savePath]: The path to save the downloading file later. it can be a String or
247 : /// a callback:
248 : /// 1. A path with String type, eg "xs.jpg"
249 : /// 2. A callback `String Function(Headers)`; for example:
250 : /// ```dart
251 : /// await dio.downloadUri(uri,(Headers headers){
252 : /// // Extra info: redirect counts
253 : /// print(headers.value('redirects'));
254 : /// // Extra info: real uri
255 : /// print(headers.value('uri'));
256 : /// ...
257 : /// return "...";
258 : /// });
259 : /// ```
260 : ///
261 : /// [onReceiveProgress]: The callback to listen downloading progress.
262 : /// please refer to [ProgressCallback].
263 : ///
264 : /// [lengthHeader] : The real size of original file (not compressed).
265 : /// When file is compressed:
266 : /// 1. If this value is 'content-length', the `total` argument of `onProgress` will be -1
267 : /// 2. If this value is not 'content-length', maybe a custom header indicates the original
268 : /// file size , the `total` argument of `onProgress` will be this header value.
269 : ///
270 : /// you can also disable the compression by specifying the 'accept-encoding' header value as '*'
271 : /// to assure the value of `total` argument of `onProgress` is not -1. for example:
272 : ///
273 : /// await dio.downloadUri(uri, "./example/flutter.svg",
274 : /// options: Options(headers: {HttpHeaders.acceptEncodingHeader: "*"}), // disable gzip
275 : /// onProgress: (received, total) {
276 : /// if (total != -1) {
277 : /// print((received / total * 100).toStringAsFixed(0) + "%");
278 : /// }
279 : /// });
280 1 : @override
281 : Future<Response> downloadUri(
282 : Uri uri,
283 : savePath, {
284 : ProgressCallback? onReceiveProgress,
285 : CancelToken? cancelToken,
286 : bool deleteOnError = true,
287 : lengthHeader = Headers.contentLengthHeader,
288 : data,
289 : Options? options,
290 : }) {
291 1 : return download(
292 1 : uri.toString(),
293 : savePath,
294 : onReceiveProgress: onReceiveProgress,
295 : lengthHeader: lengthHeader,
296 : deleteOnError: deleteOnError,
297 : cancelToken: cancelToken,
298 : data: data,
299 : options: options,
300 : );
301 : }
302 : }
|