diff --git a/README.md b/README.md index a8414f045..6644f08f7 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ A project committed to making file access and data transfer easier and more effi * [Multipart/form upload](#user-content-multipartform-data-example--post-form-data-with-file-and-data) * [Upload/Download progress](#user-content-uploaddownload-progress) * [Cancel HTTP request](#user-content-cancel-request) + * [iOS Background Uploading](#user-content-ios-background-uploading) * [Android Media Scanner, and Download Manager Support](#user-content-android-media-scanner-and-download-manager-support) * [Self-Signed SSL Server](#user-content-self-signed-ssl-server) * [Transfer Encoding](#user-content-transfer-encoding) @@ -475,6 +476,34 @@ If you have existing code that uses `whatwg-fetch`(the official **fetch**), it's [See document and examples](https://github.com/joltup/rn-fetch-blob/wiki/Fetch-API#fetch-replacement) +### iOS Background Uploading + Normally, iOS interrupts network connections when an app is moved to the background, and will throw an error 'Lost connection to background transfer service' when the app resumes. To continue the upload of large files even when the app is in the background, you will need to enable IOSUploadTask options. + +First add the following property to your AppDelegate.h: +``` +@property (nonatomic, copy) void(^backgroundTransferCompletionHandler)(); +``` +Then add the following to your AppDelegate.m: +``` +- (void)application:(UIApplication *)application +handleEventsForBackgroundURLSession:(NSString *)identifier + completionHandler:(void (^)(void))completionHandler { + self.backgroundTransferCompletionHandler = completionHandler; +} +``` +The following example shows how to upload a file in the background: + ```js + RNFetchBlob + .config({ + IOSBackgroundTask: true, // required for both upload + IOSUploadTask: true, // Use instead of IOSDownloadTask if uploading + uploadFilePath : 'file://' + filePath + }) + .fetch('PUT', url, { + 'Content-Type': mediaType + }, RNFetchBlob.wrap(filePath)); +``` + ### Android Media Scanner, and Download Manager Support If you want to make a file in `External Storage` becomes visible in Picture, Downloads, or other built-in apps, you will have to use `Media Scanner` or `Download Manager`. diff --git a/ios/RNFetchBlobNetwork.m b/ios/RNFetchBlobNetwork.m index f2e40b6b4..fbb421f38 100644 --- a/ios/RNFetchBlobNetwork.m +++ b/ios/RNFetchBlobNetwork.m @@ -63,7 +63,7 @@ - (id)init { + (RNFetchBlobNetwork* _Nullable)sharedInstance { static id _sharedInstance = nil; static dispatch_once_t onceToken; - + dispatch_once(&onceToken, ^{ _sharedInstance = [[self alloc] init]; }); @@ -135,14 +135,8 @@ - (void) enableUploadProgress:(NSString *) taskId config:(RNFetchBlobProgress *) - (void) cancelRequest:(NSString *)taskId { - NSURLSessionDataTask * task; - @synchronized ([RNFetchBlobNetwork class]) { - task = [self.requestsTable objectForKey:taskId].task; - } - - if (task && task.state == NSURLSessionTaskStateRunning) { - [task cancel]; + [[self.requestsTable objectForKey:taskId] cancelRequest:taskId]; } } diff --git a/ios/RNFetchBlobRequest.h b/ios/RNFetchBlobRequest.h index b550ac22e..7e5776ed3 100644 --- a/ios/RNFetchBlobRequest.h +++ b/ios/RNFetchBlobRequest.h @@ -32,7 +32,8 @@ @property (nullable, nonatomic) NSError * error; @property (nullable, nonatomic) RNFetchBlobProgress *progressConfig; @property (nullable, nonatomic) RNFetchBlobProgress *uploadProgressConfig; -@property (nullable, nonatomic, weak) NSURLSessionDataTask *task; +//@property (nullable, nonatomic, weak) NSURLSessionDataTask *task; +@property (nonatomic, strong) __block NSURLSession * session; - (void) sendRequest:(NSDictionary * _Nullable )options contentLength:(long)contentLength @@ -42,6 +43,8 @@ taskOperationQueue:(NSOperationQueue * _Nonnull)operationQueue callback:(_Nullable RCTResponseSenderBlock) callback; +- (void) cancelRequest:(NSString *)taskId; + @end #endif /* RNFetchBlobRequest_h */ diff --git a/ios/RNFetchBlobRequest.m b/ios/RNFetchBlobRequest.m index a56cc92d0..479d108b8 100644 --- a/ios/RNFetchBlobRequest.m +++ b/ios/RNFetchBlobRequest.m @@ -11,10 +11,24 @@ #import "RNFetchBlobFS.h" #import "RNFetchBlobConst.h" #import "RNFetchBlobReqBuilder.h" +#if __has_include() +#import +#else +#import "RCTLog.h" +#endif #import "IOS7Polyfill.h" #import +NSMapTable * taskTable; + +__attribute__((constructor)) +static void initialize_tables() { + if(taskTable == nil) + { + taskTable = [[NSMapTable alloc] init]; + } +} typedef NS_ENUM(NSUInteger, ResponseFormat) { UTF8, @@ -36,6 +50,7 @@ @interface RNFetchBlobRequest () ResponseFormat responseFormat; BOOL followRedirect; BOOL backgroundTask; + BOOL uploadTask; } @end @@ -82,6 +97,16 @@ - (void) sendRequest:(__weak NSDictionary * _Nullable )options self.options = options; backgroundTask = [[options valueForKey:@"IOSBackgroundTask"] boolValue]; + uploadTask = [options valueForKey:@"IOSUploadTask"] == nil ? NO : [[options valueForKey:@"IOSUploadTask"] boolValue]; + + NSString * filepath = [options valueForKey:@"uploadFilePath"]; + + if (uploadTask && ![[NSFileManager defaultManager] fileExistsAtPath:[NSURL URLWithString:filepath].path]) { + RCTLog(@"[RNFetchBlobRequest] sendRequest uploadTask file doesn't exist %@", filepath); + callback(@[@"uploadTask file doesn't exist", @"", [NSNull null]]); + return; + } + // when followRedirect not set in options, defaults to TRUE followRedirect = [options valueForKey:@"followRedirect"] == nil ? YES : [[options valueForKey:@"followRedirect"] boolValue]; isIncrement = [[options valueForKey:@"increment"] boolValue]; @@ -104,7 +129,6 @@ - (void) sendRequest:(__weak NSDictionary * _Nullable )options NSString * path = [self.options valueForKey:CONFIG_FILE_PATH]; NSString * key = [self.options valueForKey:CONFIG_KEY]; - NSURLSession * session; bodyLength = contentLength; @@ -117,6 +141,7 @@ - (void) sendRequest:(__weak NSDictionary * _Nullable )options defaultConfigObject = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:taskId]; } + // request timeout, -1 if not set in options float timeout = [options valueForKey:@"timeout"] == nil ? -1 : [[options valueForKey:@"timeout"] floatValue]; @@ -125,7 +150,7 @@ - (void) sendRequest:(__weak NSDictionary * _Nullable )options } defaultConfigObject.HTTPMaximumConnectionsPerHost = 10; - session = [NSURLSession sessionWithConfiguration:defaultConfigObject delegate:self delegateQueue:operationQueue]; + _session = [NSURLSession sessionWithConfiguration:defaultConfigObject delegate:self delegateQueue:operationQueue]; if (path || [self.options valueForKey:CONFIG_USE_TEMP]) { respFile = YES; @@ -157,8 +182,19 @@ - (void) sendRequest:(__weak NSDictionary * _Nullable )options respFile = NO; } - self.task = [session dataTaskWithRequest:req]; - [self.task resume]; + __block NSURLSessionTask * task; + + if(uploadTask) + { + task = [_session uploadTaskWithRequest:req fromFile:[NSURL URLWithString:filepath]]; + } + else + { + task = [_session dataTaskWithRequest:req]; + } + + [taskTable setObject:task forKey:taskId]; + [task resume]; // network status indicator if ([[options objectForKey:CONFIG_INDICATOR] boolValue]) { @@ -182,6 +218,7 @@ - (void) sendRequest:(__weak NSDictionary * _Nullable )options // set expected content length on response received - (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler { + NSLog(@"sess didReceiveResponse"); expectedBytes = [response expectedContentLength]; NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response; @@ -207,7 +244,7 @@ - (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dat partBuffer = [[NSMutableData alloc] init]; completionHandler(NSURLSessionResponseAllow); - + return; } else { self.isServerPush = [[respCType lowercaseString] RNFBContainsString:@"multipart/x-mixed-replace;"]; @@ -269,42 +306,6 @@ - (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dat NSLog(@"oops"); } - if (respFile) - { - @try{ - NSFileManager * fm = [NSFileManager defaultManager]; - NSString * folder = [destPath stringByDeletingLastPathComponent]; - - if (![fm fileExistsAtPath:folder]) { - [fm createDirectoryAtPath:folder withIntermediateDirectories:YES attributes:NULL error:nil]; - } - - // if not set overwrite in options, defaults to TRUE - BOOL overwrite = [options valueForKey:@"overwrite"] == nil ? YES : [[options valueForKey:@"overwrite"] boolValue]; - BOOL appendToExistingFile = [destPath RNFBContainsString:@"?append=true"]; - - appendToExistingFile = !overwrite; - - // For solving #141 append response data if the file already exists - // base on PR#139 @kejinliang - if (appendToExistingFile) { - destPath = [destPath stringByReplacingOccurrencesOfString:@"?append=true" withString:@""]; - } - - if (![fm fileExistsAtPath:destPath]) { - [fm createFileAtPath:destPath contents:[[NSData alloc] init] attributes:nil]; - } - - writeStream = [[NSOutputStream alloc] initToFileAtPath:destPath append:appendToExistingFile]; - [writeStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - [writeStream open]; - } - @catch(NSException * ex) - { - NSLog(@"write file error"); - } - } - completionHandler(NSURLSessionResponseAllow); } @@ -328,11 +329,7 @@ - (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dat chunkString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; } - if (respFile) { - [writeStream write:[data bytes] maxLength:[data length]]; - } else { - [respData appendData:data]; - } + [respData appendData:data]; if (expectedBytes == 0) { return; @@ -353,8 +350,16 @@ - (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dat } } +- (void) cancelRequest:(NSString *)taskId +{ + NSURLSessionDataTask * task = [taskTable objectForKey:taskId]; + if(task != nil && task.state == NSURLSessionTaskStateRunning) + [task cancel]; +} + - (void) URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error { + RCTLog(@"[RNFetchBlobRequest] session didBecomeInvalidWithError %@", [error description]); if ([session isEqual:session]) { session = nil; } @@ -363,7 +368,7 @@ - (void) URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable - (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { - + RCTLog(@"[RNFetchBlobRequest] session didCompleteWithError %@", [error description]); self.error = error; NSString * errMsg; NSString * respStr; @@ -416,10 +421,17 @@ - (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCom respStr ?: [NSNull null] ]); + @synchronized(taskTable) + { + if([taskTable objectForKey:taskId] == nil) + NSLog(@"object released by ARC."); + else + [taskTable removeObjectForKey:taskId]; + } + respData = nil; receivedBytes = 0; [session finishTasksAndInvalidate]; - } // upload progress handler @@ -430,7 +442,7 @@ - (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSen } NSNumber * now = [NSNumber numberWithFloat:((float)totalBytesWritten/(float)totalBytesExpectedToWrite)]; - + if ([self.uploadProgressConfig shouldReport:now]) { [self.bridge.eventDispatcher sendDeviceEventWithName:EVENT_PROGRESS_UPLOAD @@ -456,7 +468,19 @@ - (void) URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthentica - (void) URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { - NSLog(@"sess done in background"); + RCTLog(@"[RNFetchBlobRequest] session done in background"); + dispatch_async(dispatch_get_main_queue(), ^{ + id appDelegate = [UIApplication sharedApplication].delegate; + SEL selector = NSSelectorFromString(@"backgroundTransferCompletionHandler"); + if ([appDelegate respondsToSelector:selector]) { + void(^completionHandler)() = [appDelegate performSelector:selector]; + if (completionHandler != nil) { + completionHandler(); + completionHandler = nil; + } + } + + }); } - (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler