@ammarahmed/react-native-upload 6.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,457 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <MobileCoreServices/MobileCoreServices.h>
3
+ #import <React/RCTEventEmitter.h>
4
+ #import <React/RCTBridgeModule.h>
5
+ #import <Photos/Photos.h>
6
+
7
+ #import "RNFileUploader.h"
8
+
9
+ @implementation RNFileUploader
10
+
11
+ RCT_EXPORT_MODULE();
12
+
13
+ @synthesize bridge = _bridge;
14
+ static RNFileUploader* staticInstance = nil;
15
+ static NSString *BACKGROUND_SESSION_ID = @"ReactNativeBackgroundUpload";
16
+ NSMutableDictionary *_responsesData;
17
+ NSURLSession *_urlSession = nil;
18
+ void (^backgroundSessionCompletionHandler)(void) = nil;
19
+ BOOL limitNetwork = NO;
20
+
21
+ + (BOOL)requiresMainQueueSetup {
22
+ return YES;
23
+ }
24
+
25
+ - (dispatch_queue_t)methodQueue
26
+ {
27
+ return dispatch_get_main_queue();
28
+ }
29
+
30
+ -(id) init {
31
+ self = [super init];
32
+ if (self) {
33
+ staticInstance = self;
34
+ _responsesData = [NSMutableDictionary dictionary];
35
+ }
36
+ return self;
37
+ }
38
+
39
+ - (void)_sendEventWithName:(NSString *)eventName body:(id)body {
40
+ if (staticInstance == nil) return;
41
+ [staticInstance sendEventWithName:eventName body:body];
42
+ }
43
+
44
+ - (NSArray<NSString *> *)supportedEvents {
45
+ return @[
46
+ @"RNFileUploader-progress",
47
+ @"RNFileUploader-error",
48
+ @"RNFileUploader-cancelled",
49
+ @"RNFileUploader-completed"
50
+ ];
51
+ }
52
+
53
+ - (void)startObserving {
54
+ // JS side is ready to receive events; create the background url session if necessary
55
+ // iOS will then deliver the tasks completed while the app was dead (if any)
56
+ NSString *appGroup = nil;
57
+ double delayInSeconds = 5;
58
+ dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
59
+ dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
60
+ [self urlSession:appGroup];
61
+ });
62
+ }
63
+
64
+ + (void)setCompletionHandlerWithIdentifier: (NSString *)identifier completionHandler: (void (^)())completionHandler {
65
+ if ([BACKGROUND_SESSION_ID isEqualToString:identifier]) {
66
+ backgroundSessionCompletionHandler = completionHandler;
67
+ }
68
+ }
69
+
70
+ /*
71
+ Gets file information for the path specified. Example valid path is: file:///var/mobile/Containers/Data/Application/3C8A0EFB-A316-45C0-A30A-761BF8CCF2F8/tmp/trim.A5F76017-14E9-4890-907E-36A045AF9436.MOV
72
+ Returns an object such as: {mimeType: "video/quicktime", size: 2569900, exists: true, name: "trim.AF9A9225-FC37-416B-A25B-4EDB8275A625.MOV", extension: "MOV"}
73
+ */
74
+ RCT_EXPORT_METHOD(getFileInfo:(NSString *)path resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
75
+ {
76
+ @try {
77
+ // Escape non latin characters in filename
78
+ NSString *escapedPath = [path stringByAddingPercentEncodingWithAllowedCharacters: NSCharacterSet.URLQueryAllowedCharacterSet];
79
+
80
+ NSURL *fileUri = [NSURL URLWithString:escapedPath];
81
+ NSString *pathWithoutProtocol = [fileUri path];
82
+ NSString *name = [fileUri lastPathComponent];
83
+ NSString *extension = [name pathExtension];
84
+ bool exists = [[NSFileManager defaultManager] fileExistsAtPath:pathWithoutProtocol];
85
+ NSMutableDictionary *params = [NSMutableDictionary dictionaryWithObjectsAndKeys: name, @"name", nil];
86
+ [params setObject:extension forKey:@"extension"];
87
+ [params setObject:[NSNumber numberWithBool:exists] forKey:@"exists"];
88
+
89
+ if (exists)
90
+ {
91
+ [params setObject:[self guessMIMETypeFromFileName:name] forKey:@"mimeType"];
92
+ NSError* error;
93
+ NSDictionary<NSFileAttributeKey, id> *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:pathWithoutProtocol error:&error];
94
+ if (error == nil)
95
+ {
96
+ unsigned long long fileSize = [attributes fileSize];
97
+ [params setObject:[NSNumber numberWithLong:fileSize] forKey:@"size"];
98
+ }
99
+ }
100
+ resolve(params);
101
+ }
102
+ @catch (NSException *exception) {
103
+ reject(@"RN Uploader", exception.name, nil);
104
+ }
105
+ }
106
+
107
+ /*
108
+ Borrowed from http://stackoverflow.com/questions/2439020/wheres-the-iphone-mime-type-database
109
+ */
110
+ - (NSString *)guessMIMETypeFromFileName: (NSString *)fileName {
111
+ CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[fileName pathExtension], NULL);
112
+ CFStringRef MIMEType = UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType);
113
+
114
+ if (UTI) {
115
+ CFRelease(UTI);
116
+ }
117
+
118
+ if (!MIMEType) {
119
+ return @"application/octet-stream";
120
+ }
121
+ return (__bridge NSString *)(MIMEType);
122
+ }
123
+
124
+ /*
125
+ Utility method to copy a PHAsset file into a local temp file, which can then be uploaded.
126
+ */
127
+ - (void)copyAssetToFile: (NSString *)assetUrl completionHandler: (void(^)(NSString *__nullable tempFileUrl, NSError *__nullable error))completionHandler {
128
+ NSURL *url = [NSURL URLWithString:assetUrl];
129
+ PHAsset *asset = [PHAsset fetchAssetsWithALAssetURLs:@[url] options:nil].lastObject;
130
+ if (!asset) {
131
+ NSMutableDictionary* details = [NSMutableDictionary dictionary];
132
+ [details setValue:@"Asset could not be fetched. Are you missing permissions?" forKey:NSLocalizedDescriptionKey];
133
+ completionHandler(nil, [NSError errorWithDomain:@"RNUploader" code:5 userInfo:details]);
134
+ return;
135
+ }
136
+ PHAssetResource *assetResource = [[PHAssetResource assetResourcesForAsset:asset] firstObject];
137
+ NSString *pathToWrite = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]];
138
+ NSURL *pathUrl = [NSURL fileURLWithPath:pathToWrite];
139
+ NSString *fileURI = pathUrl.absoluteString;
140
+
141
+ PHAssetResourceRequestOptions *options = [PHAssetResourceRequestOptions new];
142
+ options.networkAccessAllowed = YES;
143
+
144
+ [[PHAssetResourceManager defaultManager] writeDataForAssetResource:assetResource toFile:pathUrl options:options completionHandler:^(NSError * _Nullable e) {
145
+ if (e == nil) {
146
+ completionHandler(fileURI, nil);
147
+ }
148
+ else {
149
+ completionHandler(nil, e);
150
+ }
151
+ }];
152
+ }
153
+
154
+ /*
155
+ * Starts a file upload.
156
+ * Options are passed in as the first argument as a js hash:
157
+ * {
158
+ * url: string. url to post to.
159
+ * path: string. path to the file on the device
160
+ * headers: hash of name/value header pairs
161
+ * }
162
+ *
163
+ * Returns a promise with the string ID of the upload.
164
+ */
165
+ RCT_EXPORT_METHOD(startUpload:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
166
+ {
167
+ NSString *uploadUrl = options[@"url"];
168
+ __block NSString *fileURI = options[@"path"];
169
+ NSString *method = options[@"method"] ?: @"POST";
170
+ NSString *uploadType = options[@"type"] ?: @"raw";
171
+ NSString *fieldName = options[@"field"];
172
+ NSString *customUploadId = options[@"customUploadId"];
173
+ NSString *appGroup = options[@"appGroup"];
174
+ NSDictionary *headers = options[@"headers"];
175
+ NSDictionary *parameters = options[@"parameters"];
176
+
177
+ @try {
178
+ NSURL *requestUrl = [NSURL URLWithString: uploadUrl];
179
+ if (requestUrl == nil) {
180
+ return reject(@"RN Uploader", @"URL not compliant with RFC 2396", nil);
181
+ }
182
+
183
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestUrl];
184
+ [request setHTTPMethod: method];
185
+
186
+ [headers enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull val, BOOL * _Nonnull stop) {
187
+ if ([val respondsToSelector:@selector(stringValue)]) {
188
+ val = [val stringValue];
189
+ }
190
+ if ([val isKindOfClass:[NSString class]]) {
191
+ [request setValue:val forHTTPHeaderField:key];
192
+ }
193
+ }];
194
+
195
+
196
+ // asset library files have to be copied over to a temp file. they can't be uploaded directly
197
+ if ([fileURI hasPrefix:@"assets-library"]) {
198
+ dispatch_group_t group = dispatch_group_create();
199
+ dispatch_group_enter(group);
200
+ [self copyAssetToFile:fileURI completionHandler:^(NSString * _Nullable tempFileUrl, NSError * _Nullable error) {
201
+ if (error) {
202
+ dispatch_group_leave(group);
203
+ reject(@"RN Uploader", @"Asset could not be copied to temp file.", nil);
204
+ return;
205
+ }
206
+ fileURI = tempFileUrl;
207
+ dispatch_group_leave(group);
208
+ }];
209
+ dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
210
+ }
211
+
212
+ NSString *uploadId = customUploadId ? customUploadId : [[NSUUID UUID] UUIDString];
213
+ NSURLSessionUploadTask *uploadTask;
214
+
215
+ if ([uploadType isEqualToString:@"multipart"]) {
216
+ NSString *uuidStr = [[NSUUID UUID] UUIDString];
217
+ [request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", uuidStr] forHTTPHeaderField:@"Content-Type"];
218
+
219
+ NSData *multipartData = [self createBodyWithBoundary:uuidStr path:fileURI parameters: parameters fieldName:fieldName];
220
+
221
+ NSURL *multipartDataFileUrl = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/%@", [self getTmpDirectory], uploadId]];
222
+ [multipartData writeToURL:multipartDataFileUrl atomically:YES];
223
+
224
+ uploadTask = [[self urlSession: appGroup] uploadTaskWithRequest:request fromFile:multipartDataFileUrl];
225
+ } else {
226
+ if (parameters.count > 0) {
227
+ reject(@"RN Uploader", @"Parameters supported only in multipart type", nil);
228
+ return;
229
+ }
230
+
231
+ uploadTask = [[self urlSession: appGroup] uploadTaskWithRequest:request fromFile:[NSURL URLWithString: fileURI]];
232
+ }
233
+
234
+ uploadTask.taskDescription = uploadId;
235
+
236
+ [uploadTask resume];
237
+ resolve(uploadTask.taskDescription);
238
+ }
239
+ @catch (NSException *exception) {
240
+ reject(@"RN Uploader", exception.name, nil);
241
+ }
242
+ }
243
+
244
+ /*
245
+ * Cancels file upload
246
+ * Accepts upload ID as a first argument, this upload will be cancelled
247
+ * Event "cancelled" will be fired when upload is cancelled.
248
+ */
249
+ RCT_EXPORT_METHOD(cancelUpload: (NSString *)cancelUploadId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
250
+ [_urlSession getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
251
+ for (NSURLSessionTask *uploadTask in uploadTasks) {
252
+ if ([uploadTask.taskDescription isEqualToString:cancelUploadId]){
253
+ // == checks if references are equal, while isEqualToString checks the string value
254
+ [uploadTask cancel];
255
+ }
256
+ }
257
+ }];
258
+ resolve([NSNumber numberWithBool:YES]);
259
+ }
260
+
261
+ RCT_EXPORT_METHOD(canSuspendIfBackground) {
262
+ if (backgroundSessionCompletionHandler) {
263
+ backgroundSessionCompletionHandler();
264
+ backgroundSessionCompletionHandler = nil;
265
+ }
266
+ }
267
+
268
+ RCT_EXPORT_METHOD(shouldLimitNetwork: (BOOL) limit) {
269
+ limitNetwork = limit;
270
+ }
271
+
272
+ RCT_EXPORT_METHOD(getAllUploads:(RCTPromiseResolveBlock)resolve
273
+ reject:(RCTPromiseRejectBlock)reject)
274
+ {
275
+ NSString *appGroup = nil;
276
+ [[self urlSession: appGroup] getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
277
+ NSMutableArray *uploads = [NSMutableArray new];
278
+ for (NSURLSessionUploadTask *uploadTask in uploadTasks) {
279
+ NSString *state;
280
+ switch (uploadTask.state) {
281
+ case NSURLSessionTaskStateRunning:
282
+ state = @"running";
283
+ break;
284
+ case NSURLSessionTaskStateSuspended:
285
+ state = @"pending";
286
+ break;
287
+ case NSURLSessionTaskStateCanceling:
288
+ state = @"cancelled";
289
+ break;
290
+ case NSURLSessionTaskStateCompleted:
291
+ state = @"completed";
292
+ break;
293
+ }
294
+
295
+ NSDictionary *upload = @{
296
+ @"id" : uploadTask.taskDescription,
297
+ @"state" : state
298
+ };
299
+ [uploads addObject:upload];
300
+ }
301
+ resolve(uploads);
302
+ }];
303
+ }
304
+
305
+ - (NSData *)createBodyWithBoundary:(NSString *)boundary
306
+ path:(NSString *)path
307
+ parameters:(NSDictionary *)parameters
308
+ fieldName:(NSString *)fieldName {
309
+
310
+ NSMutableData *httpBody = [NSMutableData data];
311
+
312
+ // Escape non latin characters in filename
313
+ NSString *escapedPath = [path stringByAddingPercentEncodingWithAllowedCharacters: NSCharacterSet.URLQueryAllowedCharacterSet];
314
+
315
+ // resolve path
316
+ NSURL *fileUri = [NSURL URLWithString: escapedPath];
317
+
318
+ NSError* error = nil;
319
+ NSData *data = [NSData dataWithContentsOfURL:fileUri options:NSDataReadingMappedAlways error: &error];
320
+
321
+ if (data == nil) {
322
+ NSLog(@"Failed to read file %@", error);
323
+ }
324
+
325
+ NSString *filename = [path lastPathComponent];
326
+ NSString *mimetype = [self guessMIMETypeFromFileName:path];
327
+
328
+ [parameters enumerateKeysAndObjectsUsingBlock:^(NSString *parameterKey, NSString *parameterValue, BOOL *stop) {
329
+ [httpBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
330
+ [httpBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", parameterKey] dataUsingEncoding:NSUTF8StringEncoding]];
331
+ [httpBody appendData:[[NSString stringWithFormat:@"%@\r\n", parameterValue] dataUsingEncoding:NSUTF8StringEncoding]];
332
+ }];
333
+
334
+ [httpBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
335
+ [httpBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", fieldName, filename] dataUsingEncoding:NSUTF8StringEncoding]];
336
+ [httpBody appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", mimetype] dataUsingEncoding:NSUTF8StringEncoding]];
337
+ [httpBody appendData:data];
338
+ [httpBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
339
+
340
+ [httpBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
341
+
342
+ return httpBody;
343
+ }
344
+
345
+ - (NSURLSession *)urlSession: (NSString *) groupId {
346
+ if (_urlSession == nil) {
347
+ NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:BACKGROUND_SESSION_ID];
348
+ if (groupId != nil && ![groupId isEqualToString:@""]) {
349
+ sessionConfiguration.sharedContainerIdentifier = groupId;
350
+ }
351
+ if (limitNetwork) {
352
+ sessionConfiguration.allowsCellularAccess = NO;
353
+ }
354
+ _urlSession = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];
355
+ }
356
+
357
+ return _urlSession;
358
+ }
359
+
360
+ - (NSString *)getTmpDirectory {
361
+ NSFileManager *manager = [NSFileManager defaultManager];
362
+ NSString *namespace = @"react-native-upload";
363
+ NSString *tmpPath = [NSTemporaryDirectory() stringByAppendingString:namespace];
364
+
365
+ [manager createDirectoryAtPath: tmpPath withIntermediateDirectories:YES attributes:nil error:nil];
366
+
367
+ return tmpPath;
368
+ }
369
+
370
+ #pragma mark - NSURLSessionTaskDelegate
371
+
372
+ - (void)URLSession:(NSURLSession *)session
373
+ task:(NSURLSessionTask *)task
374
+ didCompleteWithError:(NSError *)error {
375
+ NSMutableDictionary *data = [NSMutableDictionary dictionaryWithObjectsAndKeys:task.taskDescription, @"id", nil];
376
+ NSURLSessionDataTask *uploadTask = (NSURLSessionDataTask *)task;
377
+ NSHTTPURLResponse *response = (NSHTTPURLResponse *)uploadTask.response;
378
+ if (response != nil)
379
+ {
380
+ [data setObject:[NSNumber numberWithInteger:response.statusCode] forKey:@"responseCode"];
381
+ }
382
+ //Add data that was collected earlier by the didReceiveData method
383
+ NSMutableData *responseData = _responsesData[@(task.taskIdentifier)];
384
+ if (responseData) {
385
+ [_responsesData removeObjectForKey:@(task.taskIdentifier)];
386
+ NSString *response = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
387
+ [data setObject:response forKey:@"responseBody"];
388
+ } else {
389
+ [data setObject:[NSNull null] forKey:@"responseBody"];
390
+ }
391
+
392
+ if (error == nil)
393
+ {
394
+ [self _sendEventWithName:@"RNFileUploader-completed" body:data];
395
+ }
396
+ else
397
+ {
398
+ [data setObject:error.localizedDescription forKey:@"error"];
399
+ if (error.code == NSURLErrorCancelled) {
400
+ [self _sendEventWithName:@"RNFileUploader-cancelled" body:data];
401
+ } else {
402
+ [self _sendEventWithName:@"RNFileUploader-error" body:data];
403
+ }
404
+ }
405
+
406
+ NSURL *multipartDataFileUrl = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/%@", [self getTmpDirectory], task.taskDescription]];
407
+ [[NSFileManager defaultManager] removeItemAtURL:multipartDataFileUrl error:nil];
408
+ }
409
+
410
+ - (void)URLSession:(NSURLSession *)session
411
+ task:(NSURLSessionTask *)task
412
+ didSendBodyData:(int64_t)bytesSent
413
+ totalBytesSent:(int64_t)totalBytesSent
414
+ totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
415
+ float progress = -1;
416
+ if (totalBytesExpectedToSend > 0) //see documentation. For unknown size it's -1 (NSURLSessionTransferSizeUnknown)
417
+ {
418
+ progress = 100.0 * (float)totalBytesSent / (float)totalBytesExpectedToSend;
419
+ }
420
+ [self _sendEventWithName:@"RNFileUploader-progress" body:@{ @"id": task.taskDescription, @"progress": [NSNumber numberWithFloat:progress] }];
421
+ }
422
+
423
+ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
424
+ if (!data.length) {
425
+ return;
426
+ }
427
+ //Hold returned data so it can be picked up by the didCompleteWithError method later
428
+ NSMutableData *responseData = _responsesData[@(dataTask.taskIdentifier)];
429
+ if (!responseData) {
430
+ responseData = [NSMutableData dataWithData:data];
431
+ _responsesData[@(dataTask.taskIdentifier)] = responseData;
432
+ } else {
433
+ [responseData appendData:data];
434
+ }
435
+ }
436
+
437
+ #pragma mark - NSURLSessionDelegate
438
+
439
+ - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
440
+ if (backgroundSessionCompletionHandler) {
441
+ NSLog(@"RNBU Did Finish Events For Background URLSession (has backgroundSessionCompletionHandler)");
442
+ // This long delay is set as a security if the JS side does not call :canSuspendIfBackground: promptly
443
+ double delayInSeconds = 45.0;
444
+ dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
445
+ dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
446
+ if (backgroundSessionCompletionHandler) {
447
+ backgroundSessionCompletionHandler();
448
+ NSLog(@"RNBU did call backgroundSessionCompletionHandler (timeout)");
449
+ backgroundSessionCompletionHandler = nil;
450
+ }
451
+ });
452
+ } else {
453
+ NSLog(@"RNBU Did Finish Events For Background URLSession (no backgroundSessionCompletionHandler)");
454
+ }
455
+ }
456
+
457
+ @end