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