Browse Source

Merge branch 'master' into 0.10.0

Ben Hsieh 8 years ago
parent
commit
4944e39f70

+ 2
- 2
README.md View File

38
 
38
 
39
 ## About
39
 ## About
40
 
40
 
41
-This project was initially for solving the issue [facebook/react-native#854](https://github.com/facebook/react-native/issues/854), because React Native lack of `Blob` implementation and it will cause some problem when transfering binary data. Now, this project is committed to make file access and transfer more easier, efficient for React Native developers. We've implemented highly customizable filesystem and network module which plays well together. For example, upload and download data directly from/to storage which is much more efficient in some cases(especially for large ones). The file system supports file stream, so you don't have to worry about OOM problem when accessing large files.
41
+This project was initially for solving the issue [facebook/react-native#854](https://github.com/facebook/react-native/issues/854), because React Native lack of `Blob` implementation and it will cause some problem when transferring binary data. Now, this project is committed to make file access and transfer more easier, efficient for React Native developers. We've implemented highly customizable filesystem and network module which plays well together. For example, upload and download data directly from/to storage which is much more efficient in some cases(especially for large ones). The file system supports file stream, so you don't have to worry about OOM problem when accessing large files.
42
 
42
 
43
-In `0.8.0` we introduced experimential Web API polyfills that make it possible to use browser-based libraries in React Native, for example, [FireBase JS SDK](https://github.com/wkh237/rn-firebase-storage-upload-sample)
43
+In `0.8.0` we introduced experimental Web API polyfills that make it possible to use browser-based libraries in React Native, such as, [FireBase JS SDK](https://github.com/wkh237/rn-firebase-storage-upload-sample)
44
 
44
 
45
 
45
 
46
 ## Installation
46
 ## Installation

+ 1
- 2
package.json View File

1
 {
1
 {
2
   "name": "fetchblob-dev",
2
   "name": "fetchblob-dev",
3
-  "author": "wkh237 <xeiyan@gmail.com>",
4
-  "version": "0.9.2",
3
+  "version": "0.9.3",
5
   "private": true,
4
   "private": true,
6
   "scripts": {
5
   "scripts": {
7
     "start": "node node_modules/react-native/local-cli/cli.js start",
6
     "start": "node node_modules/react-native/local-cli/cli.js start",

+ 76
- 98
src/android/src/main/java/com/RNFetchBlob/RNFetchBlobBody.java View File

1
 package com.RNFetchBlob;
1
 package com.RNFetchBlob;
2
 
2
 
3
 import android.util.Base64;
3
 import android.util.Base64;
4
-import android.util.Log;
5
 
4
 
6
 import com.facebook.react.bridge.Arguments;
5
 import com.facebook.react.bridge.Arguments;
7
 import com.facebook.react.bridge.ReactApplicationContext;
6
 import com.facebook.react.bridge.ReactApplicationContext;
13
 import java.io.ByteArrayInputStream;
12
 import java.io.ByteArrayInputStream;
14
 import java.io.File;
13
 import java.io.File;
15
 import java.io.FileInputStream;
14
 import java.io.FileInputStream;
16
-import java.io.FileNotFoundException;
17
 import java.io.FileOutputStream;
15
 import java.io.FileOutputStream;
18
 import java.io.IOException;
16
 import java.io.IOException;
19
 import java.io.InputStream;
17
 import java.io.InputStream;
20
-import java.nio.ByteBuffer;
21
-import java.nio.MappedByteBuffer;
22
 import java.util.ArrayList;
18
 import java.util.ArrayList;
23
-import java.util.HashMap;
24
 
19
 
25
 import okhttp3.MediaType;
20
 import okhttp3.MediaType;
26
 import okhttp3.RequestBody;
21
 import okhttp3.RequestBody;
27
-import okhttp3.FormBody;
28
-import okio.Buffer;
29
 import okio.BufferedSink;
22
 import okio.BufferedSink;
30
-import okio.ForwardingSink;
31
-import okio.Okio;
32
-import okio.Sink;
33
 
23
 
34
 /**
24
 /**
35
  * Created by wkh237 on 2016/7/11.
25
  * Created by wkh237 on 2016/7/11.
38
 
28
 
39
     InputStream requestStream;
29
     InputStream requestStream;
40
     long contentLength = 0;
30
     long contentLength = 0;
41
-    long bytesWritten = 0;
42
     ReadableArray form;
31
     ReadableArray form;
43
     String mTaskId;
32
     String mTaskId;
44
     String rawBody;
33
     String rawBody;
47
     File bodyCache;
36
     File bodyCache;
48
 
37
 
49
 
38
 
39
+    /**
40
+     * Single file or raw content request constructor
41
+     * @param taskId
42
+     * @param type
43
+     * @param form
44
+     * @param contentType
45
+     */
50
     public RNFetchBlobBody(String taskId, RNFetchBlobReq.RequestType type, ReadableArray form, MediaType contentType) {
46
     public RNFetchBlobBody(String taskId, RNFetchBlobReq.RequestType type, ReadableArray form, MediaType contentType) {
51
         this.mTaskId = taskId;
47
         this.mTaskId = taskId;
52
         this.form = form;
48
         this.form = form;
54
         mime = contentType;
50
         mime = contentType;
55
         try {
51
         try {
56
             bodyCache = createMultipartBodyCache();
52
             bodyCache = createMultipartBodyCache();
53
+            requestStream = new FileInputStream(bodyCache);
57
             contentLength = bodyCache.length();
54
             contentLength = bodyCache.length();
58
-        } catch (IOException e) {
59
-            e.printStackTrace();
55
+        } catch(Exception ex) {
56
+            ex.printStackTrace();
57
+            RNFetchBlobUtils.emitWarningEvent("RNFetchBlob failed to create request multipart body :" + ex.getLocalizedMessage());
60
         }
58
         }
61
     }
59
     }
62
 
60
 
61
+    /**
62
+     * Multipart request constructor
63
+     * @param taskId
64
+     * @param type
65
+     * @param rawBody
66
+     * @param contentType
67
+     */
63
     public RNFetchBlobBody(String taskId, RNFetchBlobReq.RequestType type, String rawBody, MediaType contentType) {
68
     public RNFetchBlobBody(String taskId, RNFetchBlobReq.RequestType type, String rawBody, MediaType contentType) {
64
         this.mTaskId = taskId;
69
         this.mTaskId = taskId;
65
         requestType = type;
70
         requestType = type;
66
         this.rawBody = rawBody;
71
         this.rawBody = rawBody;
67
         mime = contentType;
72
         mime = contentType;
68
-        if(rawBody != null) {
69
-            if(requestType == RNFetchBlobReq.RequestType.AsIs)
70
-                contentLength = rawBody.length();
71
-            else
72
-                contentLength = caculateOctetContentLength();
73
+        if(rawBody == null) {
74
+            this.rawBody = "";
75
+            requestType = RNFetchBlobReq.RequestType.AsIs;
76
+        }
77
+        try {
78
+            switch (requestType) {
79
+                case SingleFile:
80
+                    requestStream = getReuqestStream();
81
+                    contentLength = requestStream.available();
82
+                    break;
83
+                case AsIs:
84
+                    contentLength = this.rawBody.getBytes().length;
85
+                    break;
86
+                case Others:
87
+                    break;
88
+            }
89
+        } catch(Exception ex) {
90
+            ex.printStackTrace();
91
+            RNFetchBlobUtils.emitWarningEvent("RNFetchBlob failed to create single content request body :" + ex.getLocalizedMessage() + "\r\n");
73
         }
92
         }
93
+
74
     }
94
     }
75
 
95
 
76
-    // ${RN 0.26+ ONLY} @Override
77
-    // ${RN 0.26+ ONLY} public long contentLength() { return contentLength; }
96
+    @Override
97
+    public long contentLength() {
98
+        return contentLength;
99
+    }
78
 
100
 
79
     @Override
101
     @Override
80
     public MediaType contentType() {
102
     public MediaType contentType() {
82
     }
104
     }
83
 
105
 
84
     @Override
106
     @Override
85
-    public void writeTo(BufferedSink sink) throws IOException {
86
-
87
-        ProgressReportingSource source = new ProgressReportingSource(sink, mTaskId);
88
-        BufferedSink buffer = Okio.buffer(source);
89
-        switch (requestType) {
90
-            case Form:
91
-                pipeStreamToSink(new FileInputStream(bodyCache), sink);
92
-                break;
93
-            case SingleFile:
94
-                if(requestStream != null)
95
-                    pipeStreamToSink(requestStream, sink);
96
-                break;
97
-            case AsIs:
98
-				writeRawData(sink);
99
-				break;
107
+    public void writeTo(BufferedSink sink) {
108
+        try {
109
+            if (requestType == RNFetchBlobReq.RequestType.AsIs)
110
+                sink.write(rawBody.getBytes());
111
+            else
112
+                pipeStreamToSink(requestStream, sink);
113
+        } catch(Exception ex) {
114
+            RNFetchBlobUtils.emitWarningEvent(ex.getLocalizedMessage());
115
+            ex.printStackTrace();
100
         }
116
         }
101
-        buffer.flush();
102
     }
117
     }
103
 
118
 
104
     boolean clearRequestBody() {
119
     boolean clearRequestBody() {
113
         return true;
128
         return true;
114
     }
129
     }
115
 
130
 
116
-    private long caculateOctetContentLength() {
117
-        long total = 0;
131
+    private InputStream getReuqestStream() throws Exception {
132
+
118
         // upload from storage
133
         // upload from storage
119
         if (rawBody.startsWith(RNFetchBlobConst.FILE_PREFIX)) {
134
         if (rawBody.startsWith(RNFetchBlobConst.FILE_PREFIX)) {
120
             String orgPath = rawBody.substring(RNFetchBlobConst.FILE_PREFIX.length());
135
             String orgPath = rawBody.substring(RNFetchBlobConst.FILE_PREFIX.length());
123
             if (RNFetchBlobFS.isAsset(orgPath)) {
138
             if (RNFetchBlobFS.isAsset(orgPath)) {
124
                 try {
139
                 try {
125
                     String assetName = orgPath.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, "");
140
                     String assetName = orgPath.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, "");
126
-                    total += RNFetchBlob.RCTContext.getAssets().openFd(assetName).getLength();
127
-                    requestStream = RNFetchBlob.RCTContext.getAssets().open(assetName);
128
-                } catch (IOException e) {
129
-                    RNFetchBlobUtils.emitWarningEvent(e.getLocalizedMessage());
141
+                    return RNFetchBlob.RCTContext.getAssets().open(assetName);
142
+                } catch (Exception e) {
143
+                    throw new Exception("error when getting request stream from asset : " +e.getLocalizedMessage());
130
                 }
144
                 }
131
             } else {
145
             } else {
132
                 File f = new File(RNFetchBlobFS.normalizePath(orgPath));
146
                 File f = new File(RNFetchBlobFS.normalizePath(orgPath));
133
                 try {
147
                 try {
134
                     if(!f.exists())
148
                     if(!f.exists())
135
                         f.createNewFile();
149
                         f.createNewFile();
136
-                    total += f.length();
137
-                    requestStream = new FileInputStream(f);
150
+                    return new FileInputStream(f);
138
                 } catch (Exception e) {
151
                 } catch (Exception e) {
139
-                    RNFetchBlobUtils.emitWarningEvent("RNetchBlob error when counting content length: " +e.getLocalizedMessage());
152
+                    throw new Exception("error when getting request stream: " +e.getLocalizedMessage());
140
                 }
153
                 }
141
             }
154
             }
142
-        } else {
155
+        }
156
+        // base 64 encoded
157
+        else {
143
             try {
158
             try {
144
                 byte[] bytes = Base64.decode(rawBody, 0);
159
                 byte[] bytes = Base64.decode(rawBody, 0);
145
-                requestStream = new ByteArrayInputStream(bytes);
146
-                total += requestStream.available();
160
+                return  new ByteArrayInputStream(bytes);
147
             } catch(Exception ex) {
161
             } catch(Exception ex) {
148
-                RNFetchBlobUtils.emitWarningEvent("RNetchBlob error when counting content length: " +ex.getLocalizedMessage());
162
+                throw new Exception("error when getting request stream: " + ex.getLocalizedMessage());
149
             }
163
             }
150
         }
164
         }
151
-        return total;
152
     }
165
     }
153
 
166
 
154
     /**
167
     /**
191
                             InputStream in = ctx.getAssets().open(assetName);
204
                             InputStream in = ctx.getAssets().open(assetName);
192
                             pipeStreamToFileStream(in, os);
205
                             pipeStreamToFileStream(in, os);
193
                         } catch (IOException e) {
206
                         } catch (IOException e) {
194
-                            RNFetchBlobUtils.emitWarningEvent("RNFetchBlob Failed to create form data asset :" + orgPath + ", " + e.getLocalizedMessage() );
207
+                            RNFetchBlobUtils.emitWarningEvent("Failed to create form data asset :" + orgPath + ", " + e.getLocalizedMessage() );
195
                         }
208
                         }
196
                     }
209
                     }
197
                     // data from normal files
210
                     // data from normal files
202
                             pipeStreamToFileStream(fs, os);
215
                             pipeStreamToFileStream(fs, os);
203
                         }
216
                         }
204
                         else {
217
                         else {
205
-                            RNFetchBlobUtils.emitWarningEvent("RNFetchBlob Failed to create form data from path :" + orgPath + ", file not exists.");
218
+                            RNFetchBlobUtils.emitWarningEvent("Failed to create form data from path :" + orgPath + ", file not exists.");
206
                         }
219
                         }
207
                     }
220
                     }
208
                 }
221
                 }
210
                 else {
223
                 else {
211
                     byte[] b = Base64.decode(data, 0);
224
                     byte[] b = Base64.decode(data, 0);
212
                     os.write(b);
225
                     os.write(b);
213
-                    bytesWritten += b.length;
214
-                    emitUploadProgress();
215
                 }
226
                 }
216
 
227
 
217
             }
228
             }
221
                 header += "Content-Type: " + field.mime + "\r\n\r\n";
232
                 header += "Content-Type: " + field.mime + "\r\n\r\n";
222
                 os.write(header.getBytes());
233
                 os.write(header.getBytes());
223
                 byte[] fieldData = field.data.getBytes();
234
                 byte[] fieldData = field.data.getBytes();
224
-                bytesWritten += fieldData.length;
225
                 os.write(fieldData);
235
                 os.write(fieldData);
226
             }
236
             }
227
             // form end
237
             // form end
235
         return outputFile;
245
         return outputFile;
236
     }
246
     }
237
 
247
 
238
-	/**
239
-     * Write data to request body as-is
240
-     * @param sink
241
-     */
242
-	private void writeRawData(BufferedSink sink) throws IOException {
243
-        byte[] bytes = rawBody.getBytes();
244
-        contentLength = bytes.length;
245
-		sink.write(bytes);
246
-	}
247
-
248
     /**
248
     /**
249
      * Pipe input stream to request body output stream
249
      * Pipe input stream to request body output stream
250
      * @param stream    The input stream
250
      * @param stream    The input stream
251
      * @param sink      The request body buffer sink
251
      * @param sink      The request body buffer sink
252
      * @throws IOException
252
      * @throws IOException
253
      */
253
      */
254
-    private void pipeStreamToSink(InputStream stream, BufferedSink sink) throws IOException {
254
+    private void pipeStreamToSink(InputStream stream, BufferedSink sink) throws Exception {
255
+
255
         byte [] chunk = new byte[10240];
256
         byte [] chunk = new byte[10240];
257
+        int totalWritten = 0;
256
         int read;
258
         int read;
257
         while((read = stream.read(chunk, 0, 10240)) > 0) {
259
         while((read = stream.read(chunk, 0, 10240)) > 0) {
258
             if(read > 0) {
260
             if(read > 0) {
259
                 sink.write(chunk, 0, read);
261
                 sink.write(chunk, 0, read);
262
+                totalWritten += read;
263
+                emitUploadProgress(totalWritten);
260
             }
264
             }
261
         }
265
         }
262
         stream.close();
266
         stream.close();
355
         }
359
         }
356
     }
360
     }
357
 
361
 
358
-    private void emitUploadProgress() {
362
+    /**
363
+     * Emit progress event
364
+     * @param written
365
+     */
366
+    private void emitUploadProgress(int written) {
359
         WritableMap args = Arguments.createMap();
367
         WritableMap args = Arguments.createMap();
360
         args.putString("taskId", mTaskId);
368
         args.putString("taskId", mTaskId);
361
-        args.putString("written", String.valueOf(bytesWritten));
369
+        args.putString("written", String.valueOf(written));
362
         args.putString("total", String.valueOf(contentLength));
370
         args.putString("total", String.valueOf(contentLength));
363
 
371
 
364
         // emit event to js context
372
         // emit event to js context
366
                 .emit(RNFetchBlobConst.EVENT_UPLOAD_PROGRESS, args);
374
                 .emit(RNFetchBlobConst.EVENT_UPLOAD_PROGRESS, args);
367
     }
375
     }
368
 
376
 
369
-    private final class ProgressReportingSource extends ForwardingSink {
370
-
371
-        private long bytesWritten = 0;
372
-        private String mTaskId;
373
-        private Sink delegate;
374
-
375
-        public ProgressReportingSource (Sink delegate, String taskId) {
376
-            super(delegate);
377
-            this.mTaskId = taskId;
378
-            this.delegate = delegate;
379
-        }
380
-
381
-        @Override
382
-        public void write(Buffer source, long byteCount) throws IOException {
383
-            delegate.write(source, byteCount);
384
-            // on progress, emit RNFetchBlobProgress upload progress event with ticketId,
385
-            // bytesWritten, and totalSize
386
-            bytesWritten += byteCount;
387
-            WritableMap args = Arguments.createMap();
388
-            args.putString("taskId", mTaskId);
389
-            args.putString("written", String.valueOf(bytesWritten));
390
-            args.putString("total", String.valueOf(contentLength));
391
-
392
-            if(RNFetchBlobReq.isReportUploadProgress(mTaskId)) {
393
-                // emit event to js context
394
-                RNFetchBlob.RCTContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
395
-                        .emit(RNFetchBlobConst.EVENT_UPLOAD_PROGRESS, args);
396
-            }
397
-        }
398
-    }
399
 }
377
 }

+ 1
- 1
src/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java View File

332
             clientBuilder.retryOnConnectionFailure(false);
332
             clientBuilder.retryOnConnectionFailure(false);
333
             clientBuilder.followRedirects(true);
333
             clientBuilder.followRedirects(true);
334
 
334
 
335
-            OkHttpClient client = clientBuilder.build();
335
+            OkHttpClient client = clientBuilder.retryOnConnectionFailure(true).build();
336
             Call call =  client.newCall(req);
336
             Call call =  client.newCall(req);
337
             taskTable.put(taskId, call);
337
             taskTable.put(taskId, call);
338
             call.enqueue(new okhttp3.Callback() {
338
             call.enqueue(new okhttp3.Callback() {

+ 8
- 0
src/class/StatefulPromise.js View File

1
+// Copyright 2016 wkh237@github. All rights reserved.
2
+// Use of this source code is governed by a MIT-style license that can be
3
+// found in the LICENSE file.
4
+// @flow
5
+
6
+export default class StatefulPromise extends Promise {
7
+
8
+}

+ 8
- 0
src/index.js View File

16
   RNFetchBlobStream,
16
   RNFetchBlobStream,
17
   RNFetchBlobResponseInfo
17
   RNFetchBlobResponseInfo
18
 } from './types'
18
 } from './types'
19
+import StatefulPromise from './class/StatefulPromise.js'
19
 import fs from './fs'
20
 import fs from './fs'
20
 import getUUID from './utils/uuid'
21
 import getUUID from './utils/uuid'
21
 import base64 from 'base-64'
22
 import base64 from 'base-64'
167
       subscription.remove()
168
       subscription.remove()
168
       subscriptionUpload.remove()
169
       subscriptionUpload.remove()
169
       stateEvent.remove()
170
       stateEvent.remove()
171
+      delete promise['progress']
172
+      delete promise['uploadProgress']
173
+      delete promise['stateChange']
174
+      delete promise['cancel']
175
+      promise.cancel = () => {
176
+        console.warn('finished request could not be canceled')
177
+      }
170
 
178
 
171
       if(err)
179
       if(err)
172
         reject(new Error(err, respInfo))
180
         reject(new Error(err, respInfo))

+ 0
- 15
src/ios/RNFetchBlobNetwork.m View File

408
         [task cancel];
408
         [task cancel];
409
 }
409
 }
410
 
410
 
411
-//- (void) application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
412
-//
413
-//}
414
-
415
-//- (void) URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
416
-//{
417
-//    if(self.dataTaskCompletionHandler != nil)
418
-//    {
419
-//        dataTaskCompletionHandler(self.respData, nil, error);
420
-//    }
421
-//    else if(self.fileTaskCompletionHandler != nil)
422
-//    {
423
-//        fileTaskCompletionHandler(nil, nil, self.error);
424
-//    }
425
-//}
426
 
411
 
427
 - (void) URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable credantial))completionHandler
412
 - (void) URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable credantial))completionHandler
428
 {
413
 {

+ 1
- 1
src/package.json View File

1
 {
1
 {
2
   "name": "react-native-fetch-blob",
2
   "name": "react-native-fetch-blob",
3
-  "version": "0.9.2-beta.4",
3
+  "version": "0.9.3",
4
   "description": "A module provides upload, download, and files access API. Supports file stream read/write for process large files.",
4
   "description": "A module provides upload, download, and files access API. Supports file stream read/write for process large files.",
5
   "main": "index.js",
5
   "main": "index.js",
6
   "scripts": {
6
   "scripts": {

+ 1
- 1
src/types.js View File

47
   state : number,
47
   state : number,
48
   headers : any,
48
   headers : any,
49
   status : number,
49
   status : number,
50
-  respType : 'text' | 'blob' | '' | 'json'
50
+  respType : 'text' | 'blob' | '' | 'json',
51
   rnfbEncode : 'path' | 'base64' | 'ascii' | 'utf8'
51
   rnfbEncode : 'path' | 'base64' | 'ascii' | 'utf8'
52
 }
52
 }
53
 
53
 

BIN
test-server/public/img-large-dummy.JPG View File


+ 27
- 1
test/test-0.7.0.js View File

33
   let begin = -1
33
   let begin = -1
34
   let begin2 = -1
34
   let begin2 = -1
35
   let deb = Date.now()
35
   let deb = Date.now()
36
+  let download = false, upload = false
36
   RNFetchBlob.config({
37
   RNFetchBlob.config({
37
     fileCache : true
38
     fileCache : true
38
   })
39
   })
39
-  .fetch('GET', `${TEST_SERVER_URL}/public/1mb-dummy`)
40
+  .fetch('GET', `${TEST_SERVER_URL}/public/2mb-dummy`)
40
   .progress((now, total) => {
41
   .progress((now, total) => {
42
+    download = true
41
     if(begin === -1)
43
     if(begin === -1)
42
       begin = Date.now()
44
       begin = Date.now()
43
     if(Date.now() - deb < 1000)
45
     if(Date.now() - deb < 1000)
57
     report(<Info key="big file stat">
59
     report(<Info key="big file stat">
58
       <Text>{JSON.stringify(stat)}</Text>
60
       <Text>{JSON.stringify(stat)}</Text>
59
     </Info>)
61
     </Info>)
62
+    let task = RNFetchBlob.fetch('POST', 'https://content.dropboxapi.com/2/files/upload', {
63
+      Authorization : `Bearer ${DROPBOX_TOKEN}`,
64
+      'Dropbox-API-Arg': '{\"path\": \"/rn-upload/'+filename+Date.now()+'\",\"mode\": \"add\",\"autorename\": true,\"mute\": false}',
65
+      'Content-Type' : 'application/octet-stream',
66
+    }, RNFetchBlob.wrap(bigfile))
67
+    begin = -1
68
+    task.uploadProgress((now, total) => {
69
+      upload = true
70
+      if(begin === -1)
71
+        begin = Date.now()
72
+      if(Date.now() - deb < 1000)
73
+        return
74
+      deb = Date.now()
75
+      report(<Info uid="300" key="upload progress">
76
+        <Text>
77
+          {`upload ${now} / ${total} bytes (${Math.floor(now / (Date.now() - begin))} kb/s) ${(100*now/total).toFixed(2)}%`}
78
+        </Text>
79
+      </Info>)
80
+    })
81
+    return task
82
+  })
83
+  .then(() => {
84
+    report(<Assert key="upload and download event triggered" expect={true} actual={download && upload}/>)
60
     done()
85
     done()
61
   })
86
   })
62
 })
87
 })
87
       </Text>
112
       </Text>
88
     </Info>)
113
     </Info>)
89
   })
114
   })
115
+
90
   let checkpoint1 = 0
116
   let checkpoint1 = 0
91
   Timer.setTimeout(() => {
117
   Timer.setTimeout(() => {
92
     task.cancel()
118
     task.cancel()