Browse Source

#15 file stream reader IOS implementation

Ben Hsieh 8 years ago
parent
commit
25a5f9a6bf
4 changed files with 168 additions and 88 deletions
  1. 62
    59
      src/index.js
  2. 1
    1
      src/ios/RNFetchBlob/RNFetchBlob.h
  3. 58
    28
      src/ios/RNFetchBlob/RNFetchBlob.m
  4. 47
    0
      src/types.js

+ 62
- 59
src/index.js View File

10
   NativeAppEventEmitter,
10
   NativeAppEventEmitter,
11
   Platform,
11
   Platform,
12
 } from 'react-native'
12
 } from 'react-native'
13
-
14
-
15
-type fetchConfig = {
16
-  fileCache : bool,
17
-  path : string,
18
-  appendExt : string
19
-};
20
-
21
-type RNFetchBlobNative = {
22
-  fetchBlob : (
23
-    options:fetchConfig,
24
-    taskId:string,
25
-    method:string,
26
-    url:string,
27
-    headers:any,
28
-    body:any,
29
-    callback:(err:any, ...data:any) => void
30
-  ) => void,
31
-  fetchBlobForm : (
32
-    options:fetchConfig,
33
-    taskId:string,
34
-    method:string,
35
-    url:string,
36
-    headers:any,
37
-    form:Array<any>,
38
-    callback:(err:any, ...data:any) => void
39
-  ) => void,
40
-  readStream : (
41
-    path:string,
42
-    encode: 'utf8' | 'ascii' | 'base64'
43
-  ) => void,
44
-  getEnvironmentDirs : (dirs:any) => void,
45
-  flush : () => void
46
-};
47
-
13
+import type {
14
+  RNFetchBlobNative,
15
+  RNFetchBlobConfig,
16
+  RNFetchBlobStream
17
+}from './types'
48
 import base64 from 'base-64'
18
 import base64 from 'base-64'
49
-const emitter = (Platform.OS === 'android' ? DeviceEventEmitter : NativeAppEventEmitter)
19
+
20
+// const emitter = (Platform.OS === 'android' ? DeviceEventEmitter : NativeAppEventEmitter)
21
+const emitter = DeviceEventEmitter
50
 const RNFetchBlob:RNFetchBlobNative = NativeModules.RNFetchBlob
22
 const RNFetchBlob:RNFetchBlobNative = NativeModules.RNFetchBlob
51
 
23
 
52
 emitter.addListener("RNFetchBlobMessage", (e) => {
24
 emitter.addListener("RNFetchBlobMessage", (e) => {
53
-
25
+  console.log(e)
54
   if(e.event === 'warn') {
26
   if(e.event === 'warn') {
55
     console.warn(e.detail)
27
     console.warn(e.detail)
56
   }
28
   }
86
 
58
 
87
 }
59
 }
88
 
60
 
89
-function config (options:fetchConfig) {
61
+function config (options:RNFetchBlobConfig) {
90
   return { fetch : fetch.bind(options) }
62
   return { fetch : fetch.bind(options) }
91
 }
63
 }
92
 
64
 
93
 // Promise wrapper function
65
 // Promise wrapper function
94
 function fetch(...args:any) {
66
 function fetch(...args:any) {
95
 
67
 
96
-  let options = this || {}
97
-
98
   // create task ID for receiving progress event
68
   // create task ID for receiving progress event
99
   let taskId = getUUID()
69
   let taskId = getUUID()
70
+  let options = this || {}
100
 
71
 
101
   let promise = new Promise((resolve, reject) => {
72
   let promise = new Promise((resolve, reject) => {
102
 
73
 
123
           respType = 'path'
94
           respType = 'path'
124
         resolve(new FetchBlobResponse(taskId, respType, ...data))
95
         resolve(new FetchBlobResponse(taskId, respType, ...data))
125
       }
96
       }
126
-
127
     })
97
     })
128
 
98
 
129
   })
99
   })
137
 
107
 
138
 }
108
 }
139
 
109
 
110
+function openReadStream(path:string, encoding:'utf8' | 'ascii' | 'base64'):RNFetchBlobStream {
111
+
112
+  if(!path)
113
+    throw Error('RNFetchBlob could not open file stream with empty `path`')
114
+
115
+  let stream:RNFetchBlobStream = {
116
+    onData : function(fn) {
117
+      this._onData = fn
118
+    },
119
+    onError : function(fn) {
120
+      this._onError = fn
121
+    },
122
+    onEnd : function(fn) {
123
+      this._onEnd = fn
124
+    },
125
+  }
126
+
127
+  // register for file stream event
128
+  let subscription = emitter.addListener(`RNFetchBlobStream+${path}`, (e) => {
129
+    let {event, detail} = e
130
+    if(stream._onData && event === 'data')
131
+      stream._onData(detail)
132
+    else if (stream._onEnd && event === 'end') {
133
+      stream._onEnd(detail)
134
+    }
135
+    else {
136
+      stream._onError(detail)
137
+    }
138
+    // when stream closed or error, remove event handler
139
+    if (event === 'error' || event === 'end') {
140
+      subscription.remove()
141
+    }
142
+
143
+  })
144
+
145
+  RNFetchBlob.readStream(path, encoding)
146
+
147
+  return stream
148
+
149
+}
150
+
140
 /**
151
 /**
141
  * RNFetchBlob response object class.
152
  * RNFetchBlob response object class.
142
  */
153
  */
153
   flush : () => void;
164
   flush : () => void;
154
   readStream : (
165
   readStream : (
155
     encode: 'utf8' | 'ascii' | 'base64',
166
     encode: 'utf8' | 'ascii' | 'base64',
156
-    fn:(event : 'data' | 'end', chunk:any) => void
157
-  ) => void;
167
+  ) => RNFetchBlobStream | null;
158
 
168
 
159
   constructor(taskId:string, type:'base64' | 'path', data:any) {
169
   constructor(taskId:string, type:'base64' | 'path', data:any) {
160
     this.data = data
170
     this.data = data
198
     this.flush = () => {
208
     this.flush = () => {
199
       RNFetchBlob.flush(this.path())
209
       RNFetchBlob.flush(this.path())
200
     }
210
     }
201
-
211
+    /**
212
+     * get path of response temp file
213
+     * @return {string} File path of temp file.
214
+     */
202
     this.path = () => {
215
     this.path = () => {
203
       if(this.type === 'path')
216
       if(this.type === 'path')
204
         return this.data
217
         return this.data
205
       return null
218
       return null
206
     }
219
     }
207
-
208
     /**
220
     /**
209
      * Start read stream from cached file
221
      * Start read stream from cached file
210
      * @param  {String} encoding Encode type, should be one of `base64`, `ascrii`, `utf8`.
222
      * @param  {String} encoding Encode type, should be one of `base64`, `ascrii`, `utf8`.
211
      * @param  {Function} fn On data event handler
223
      * @param  {Function} fn On data event handler
212
      * @return {void}
224
      * @return {void}
213
      */
225
      */
214
-    this.readStream = (encode: 'base64' | 'utf8' | 'ascii', fn) => {
215
-
216
-      // register for file stream event
217
-      let subscription = emitter.addListener(`RNFetchBlobStream${this.taskId}`, (event, chunk) => {
218
-        fn(event, chunk)
219
-        // when stream closed, remove event handler
220
-        if(event === 'end')
221
-          subscription()
222
-      })
223
-
226
+    this.readStream = (encode: 'base64' | 'utf8' | 'ascii'):RNFetchBlobStream | null => {
224
       if(this.type === 'path') {
227
       if(this.type === 'path') {
225
-        RNFetchBlob.readStream(this.data, encode)
228
+        return openReadStream(this.data, encode)
226
       }
229
       }
227
       else {
230
       else {
228
         console.warn('RNFetchblob', 'this response data does not contains any available stream')
231
         console.warn('RNFetchblob', 'this response data does not contains any available stream')
232
+        return null
229
       }
233
       }
230
-
231
     }
234
     }
232
 
235
 
233
   }
236
   }
234
 
237
 
235
 }
238
 }
236
 
239
 
237
-function getUUID(){
240
+function getUUID() {
238
   return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
241
   return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
239
     let r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
242
     let r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
240
     return v.toString(16);
243
     return v.toString(16);
242
 }
245
 }
243
 
246
 
244
 export default {
247
 export default {
245
-  fetch, base64, config, getSystemDirs
248
+  fetch, base64, config, getSystemDirs, openReadStream
246
 }
249
 }

+ 1
- 1
src/ios/RNFetchBlob/RNFetchBlob.h View File

46
 @property (nonatomic) NSString * path;
46
 @property (nonatomic) NSString * path;
47
 
47
 
48
 + (NSString *) getTempPath;
48
 + (NSString *) getTempPath;
49
+- (id) init;
49
 - (void) initWithCallback;
50
 - (void) initWithCallback;
50
 - (void) initWithBridgeRef;
51
 - (void) initWithBridgeRef;
51
 - (void) openWithDestination;
52
 - (void) openWithDestination;
78
 @property (nonatomic) FetchBlobFS * fileStream;
79
 @property (nonatomic) FetchBlobFS * fileStream;
79
 
80
 
80
 - (id) init;
81
 - (id) init;
81
-- (id) delegate;
82
 - (void) sendRequest;
82
 - (void) sendRequest;
83
 
83
 
84
 + (NSMutableDictionary *) normalizeHeaders;
84
 + (NSMutableDictionary *) normalizeHeaders;

+ 58
- 28
src/ios/RNFetchBlob/RNFetchBlob.m View File

42
 
42
 
43
 
43
 
44
 
44
 
45
-+ (NSString *) getCacheDir {
46
 
45
 
46
++ (NSString *) getCacheDir {
47
     return [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
47
     return [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
48
 }
48
 }
49
 
49
 
50
 + (NSString *) getDocumentDir {
50
 + (NSString *) getDocumentDir {
51
-
52
     return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
51
     return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
53
 }
52
 }
54
 
53
 
75
     return tempPath;
74
     return tempPath;
76
 }
75
 }
77
 
76
 
77
+- (id)init {
78
+    self = [super init];
79
+    return self;
80
+}
81
+
82
+
78
 - (id)initWithCallback:(RCTResponseSenderBlock)callback {
83
 - (id)initWithCallback:(RCTResponseSenderBlock)callback {
79
     self = [super init];
84
     self = [super init];
80
     self.callback = callback;
85
     self.callback = callback;
83
 
88
 
84
 - (id)initWithBridgeRef:(RCTBridge *)bridgeRef {
89
 - (id)initWithBridgeRef:(RCTBridge *)bridgeRef {
85
     self = [super init];
90
     self = [super init];
86
-    self.callback = callback;
91
+    self.bridge = bridgeRef;
87
     return self;
92
     return self;
88
 }
93
 }
89
 
94
 
90
 - (void)openWithPath:(NSString *)destPath {
95
 - (void)openWithPath:(NSString *)destPath {
91
     self.outStream = [[NSOutputStream alloc] initToFileAtPath:destPath append:YES];
96
     self.outStream = [[NSOutputStream alloc] initToFileAtPath:destPath append:YES];
97
+    [self.outStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
92
     [self.outStream open];
98
     [self.outStream open];
93
 }
99
 }
94
 
100
 
108
 }
114
 }
109
 
115
 
110
 - (void)readWithPath:(NSString *)path useEncoding:(NSString *)encoding {
116
 - (void)readWithPath:(NSString *)path useEncoding:(NSString *)encoding {
111
-    self.inStream = [[NSInputStream alloc]init];
117
+    
118
+    self.inStream = [[NSInputStream alloc] initWithFileAtPath:path];
119
+    self.inStream.delegate = self;
112
     self.encoding = encoding;
120
     self.encoding = encoding;
113
-    [self.inStream setDelegate:self];
121
+    self.path = path;
122
+    
123
+    // NSStream needs a runloop so let's create a run loop for it
124
+    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
125
+    // start NSStream is a runloop
126
+    dispatch_async(queue, ^ {
127
+        [inStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
128
+                             forMode:NSDefaultRunLoopMode];
129
+        [inStream open];
130
+        [[NSRunLoop currentRunLoop] run];
131
+    
132
+    });
114
     
133
     
115
 }
134
 }
116
 
135
 
117
-
118
 // close file write stream
136
 // close file write stream
119
 - (void)closeOutStream {
137
 - (void)closeOutStream {
120
     if(self.outStream != nil) {
138
     if(self.outStream != nil) {
133
     
151
     
134
 }
152
 }
135
 
153
 
154
+void runOnMainQueueWithoutDeadlocking(void (^block)(void))
155
+{
156
+    if ([NSThread isMainThread])
157
+    {
158
+        block();
159
+    }
160
+    else
161
+    {
162
+        dispatch_sync(dispatch_get_main_queue(), block);
163
+    }
164
+}
165
+
166
+
136
 #pragma mark RNFetchBlobFS read stream delegate
167
 #pragma mark RNFetchBlobFS read stream delegate
137
 
168
 
138
 - (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
169
 - (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
139
 
170
 
171
+    NSString * streamEventCode = [NSString stringWithFormat:@"RNFetchBlobStream+%@", self.path];
172
+    
140
     switch(eventCode) {
173
     switch(eventCode) {
141
             
174
             
142
         // write stream event
175
         // write stream event
149
         // read stream incoming chunk
182
         // read stream incoming chunk
150
         case NSStreamEventHasBytesAvailable:
183
         case NSStreamEventHasBytesAvailable:
151
         {
184
         {
152
-            
153
-            
154
             NSMutableData * chunkData = [[NSMutableData data] init];
185
             NSMutableData * chunkData = [[NSMutableData data] init];
155
-            
156
             uint8_t buf[1024];
186
             uint8_t buf[1024];
157
             unsigned int len = 0;
187
             unsigned int len = 0;
158
             len = [(NSInputStream *)stream read:buf maxLength:1024];
188
             len = [(NSInputStream *)stream read:buf maxLength:1024];
160
             if(len) {
190
             if(len) {
161
                 [chunkData appendBytes:(const void *)buf length:len];
191
                 [chunkData appendBytes:(const void *)buf length:len];
162
                 // TODO : file read progress ?
192
                 // TODO : file read progress ?
163
-//                [bytesRead setIntValue:[bytesRead intValue]+len];
164
-                
165
                 // dispatch data event
193
                 // dispatch data event
166
-                NSString * encodedChunk = [NSString alloc];
167
-                if( [self.encoding caseInsensitiveCompare:@"utf8"] ) {
194
+                NSString * encodedChunk;
195
+                if( [[self.encoding lowercaseString] isEqualToString:@"utf8"] ) {
168
                     encodedChunk = [encodedChunk initWithData:chunkData encoding:NSUTF8StringEncoding];
196
                     encodedChunk = [encodedChunk initWithData:chunkData encoding:NSUTF8StringEncoding];
169
                 }
197
                 }
170
-                else if ( [self.encoding caseInsensitiveCompare:@"ascii"] ) {
198
+                else if ( [[self.encoding lowercaseString] isEqualToString:@"ascii"] ) {
171
                     encodedChunk = [encodedChunk initWithData:chunkData encoding:NSASCIIStringEncoding];
199
                     encodedChunk = [encodedChunk initWithData:chunkData encoding:NSASCIIStringEncoding];
172
                 }
200
                 }
173
-                else if ( [self.encoding caseInsensitiveCompare:@"base64"] ) {
201
+                else if ( [[self.encoding lowercaseString] isEqualToString:@"base64"] ) {
174
                     encodedChunk = [chunkData base64EncodedStringWithOptions:0];
202
                     encodedChunk = [chunkData base64EncodedStringWithOptions:0];
175
                 }
203
                 }
176
                 else {
204
                 else {
177
                     [self.bridge.eventDispatcher
205
                     [self.bridge.eventDispatcher
178
-                     sendAppEventWithName: [NSString stringWithFormat:@"RNFetchBlobStream%s", self.taskId]
179
-                     body:@{
206
+                        sendDeviceEventWithName:streamEventCode
207
+                        body:@{
180
                             @"event": FS_EVENT_ERROR,
208
                             @"event": FS_EVENT_ERROR,
181
                             @"detail": @"unrecognized encoding"
209
                             @"detail": @"unrecognized encoding"
182
                         }
210
                         }
183
                      ];
211
                      ];
184
                     return;
212
                     return;
185
                 }
213
                 }
186
-                [self.bridge.eventDispatcher
187
-                 sendAppEventWithName: [NSString stringWithFormat:@"RNFetchBlobStream%s", self.taskId]
188
-                 body:@{
189
-                        @"event": FS_EVENT_DATA,
190
-                        @"detail": encodedChunk
191
-                    }
192
-                 ];
214
+                runOnMainQueueWithoutDeadlocking(^{
215
+                    [self.bridge.eventDispatcher
216
+                     sendDeviceEventWithName:streamEventCode
217
+                     body:@{
218
+                            @"event": FS_EVENT_DATA,
219
+                            @"detail": encodedChunk
220
+                            }
221
+                     ];
222
+                });
193
             }
223
             }
194
             // end of stream
224
             // end of stream
195
             else {
225
             else {
196
                 [self.bridge.eventDispatcher
226
                 [self.bridge.eventDispatcher
197
-                sendAppEventWithName: [NSString stringWithFormat:@"RNFetchBlobStream%s", self.taskId]
227
+                sendDeviceEventWithName:streamEventCode
198
                 body:@{
228
                 body:@{
199
                        @"event": FS_EVENT_END,
229
                        @"event": FS_EVENT_END,
200
                        @"detail": @""
230
                        @"detail": @""
208
         case NSStreamEventErrorOccurred:
238
         case NSStreamEventErrorOccurred:
209
         {
239
         {
210
             [self.bridge.eventDispatcher
240
             [self.bridge.eventDispatcher
211
-             sendAppEventWithName: [NSString stringWithFormat:@"RNFetchBlobStream%s", self.taskId]
241
+             sendDeviceEventWithName:streamEventCode
212
              body:@{
242
              body:@{
213
                     @"event": FS_EVENT_ERROR,
243
                     @"event": FS_EVENT_ERROR,
214
                     @"detail": @"error when read file with stream"
244
                     @"detail": @"error when read file with stream"
318
     }
348
     }
319
     
349
     
320
     [self.bridge.eventDispatcher
350
     [self.bridge.eventDispatcher
321
-        sendAppEventWithName:@"RNFetchBlobProgress"
351
+        sendDeviceEventWithName:@"RNFetchBlobProgress"
322
         body:@{
352
         body:@{
323
             @"taskId": taskId,
353
             @"taskId": taskId,
324
             @"written": [NSString stringWithFormat:@"%d", receivedBytes],
354
             @"written": [NSString stringWithFormat:@"%d", receivedBytes],
332
     expectedBytes = totalBytesExpectedToWrite;
362
     expectedBytes = totalBytesExpectedToWrite;
333
     receivedBytes += totalBytesWritten;
363
     receivedBytes += totalBytesWritten;
334
     [self.bridge.eventDispatcher
364
     [self.bridge.eventDispatcher
335
-        sendAppEventWithName:@"RNFetchBlobProgress"
365
+        sendDeviceEventWithName:@"RNFetchBlobProgress"
336
             body:@{
366
             body:@{
337
                     @"taskId": taskId,
367
                     @"taskId": taskId,
338
                     @"written": [NSString stringWithFormat:@"%d", receivedBytes],
368
                     @"written": [NSString stringWithFormat:@"%d", receivedBytes],

+ 47
- 0
src/types.js View File

1
+
2
+type RNFetchBlobConfig = {
3
+  fileCache : bool,
4
+  path : string,
5
+  appendExt : string
6
+};
7
+
8
+type RNFetchBlobNative = {
9
+  // API for fetch octet-stream data
10
+  fetchBlob : (
11
+    options:fetchConfig,
12
+    taskId:string,
13
+    method:string,
14
+    url:string,
15
+    headers:any,
16
+    body:any,
17
+    callback:(err:any, ...data:any) => void
18
+  ) => void,
19
+  // API for fetch form data
20
+  fetchBlobForm : (
21
+    options:fetchConfig,
22
+    taskId:string,
23
+    method:string,
24
+    url:string,
25
+    headers:any,
26
+    form:Array<any>,
27
+    callback:(err:any, ...data:any) => void
28
+  ) => void,
29
+  // open file stream
30
+  readStream : (
31
+    path:string,
32
+    encode: 'utf8' | 'ascii' | 'base64'
33
+  ) => void,
34
+  // get system folders
35
+  getEnvironmentDirs : (dirs:any) => void,
36
+  // unlink file by path
37
+  flush : (path:string) => void
38
+};
39
+
40
+type RNFetchBlobStream = {
41
+  onData : () => void,
42
+  onError : () => void,
43
+  onEnd : () => void,
44
+  _onData : () => void,
45
+  _onEnd : () => void,
46
+  _onError : () => void,
47
+}