Skip to content

Commit d69c72b

Browse files
authored
Merge pull request #198 from capriza/master
Adding iOS background upload support
2 parents c95ef1c + c01e158 commit d69c72b

File tree

4 files changed

+109
-59
lines changed

4 files changed

+109
-59
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ A project committed to making file access and data transfer easier and more effi
2929
* [Multipart/form upload](#user-content-multipartform-data-example--post-form-data-with-file-and-data)
3030
* [Upload/Download progress](#user-content-uploaddownload-progress)
3131
* [Cancel HTTP request](#user-content-cancel-request)
32+
* [iOS Background Uploading](#user-content-ios-background-uploading)
3233
* [Android Media Scanner, and Download Manager Support](#user-content-android-media-scanner-and-download-manager-support)
3334
* [Self-Signed SSL Server](#user-content-self-signed-ssl-server)
3435
* [Transfer Encoding](#user-content-transfer-encoding)
@@ -475,6 +476,34 @@ If you have existing code that uses `whatwg-fetch`(the official **fetch**), it's
475476

476477
[See document and examples](https://github.com/joltup/rn-fetch-blob/wiki/Fetch-API#fetch-replacement)
477478

479+
### iOS Background Uploading
480+
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.
481+
482+
First add the following property to your AppDelegate.h:
483+
```
484+
@property (nonatomic, copy) void(^backgroundTransferCompletionHandler)();
485+
```
486+
Then add the following to your AppDelegate.m:
487+
```
488+
- (void)application:(UIApplication *)application
489+
handleEventsForBackgroundURLSession:(NSString *)identifier
490+
completionHandler:(void (^)(void))completionHandler {
491+
self.backgroundTransferCompletionHandler = completionHandler;
492+
}
493+
```
494+
The following example shows how to upload a file in the background:
495+
```js
496+
RNFetchBlob
497+
.config({
498+
IOSBackgroundTask: true, // required for both upload
499+
IOSUploadTask: true, // Use instead of IOSDownloadTask if uploading
500+
uploadFilePath : 'file://' + filePath
501+
})
502+
.fetch('PUT', url, {
503+
'Content-Type': mediaType
504+
}, RNFetchBlob.wrap(filePath));
505+
```
506+
478507
### Android Media Scanner, and Download Manager Support
479508

480509
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`.

ios/RNFetchBlobNetwork.m

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ - (id)init {
6363
+ (RNFetchBlobNetwork* _Nullable)sharedInstance {
6464
static id _sharedInstance = nil;
6565
static dispatch_once_t onceToken;
66-
66+
6767
dispatch_once(&onceToken, ^{
6868
_sharedInstance = [[self alloc] init];
6969
});
@@ -135,14 +135,8 @@ - (void) enableUploadProgress:(NSString *) taskId config:(RNFetchBlobProgress *)
135135

136136
- (void) cancelRequest:(NSString *)taskId
137137
{
138-
NSURLSessionDataTask * task;
139-
140138
@synchronized ([RNFetchBlobNetwork class]) {
141-
task = [self.requestsTable objectForKey:taskId].task;
142-
}
143-
144-
if (task && task.state == NSURLSessionTaskStateRunning) {
145-
[task cancel];
139+
[[self.requestsTable objectForKey:taskId] cancelRequest:taskId];
146140
}
147141
}
148142

ios/RNFetchBlobRequest.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
@property (nullable, nonatomic) NSError * error;
3333
@property (nullable, nonatomic) RNFetchBlobProgress *progressConfig;
3434
@property (nullable, nonatomic) RNFetchBlobProgress *uploadProgressConfig;
35-
@property (nullable, nonatomic, weak) NSURLSessionDataTask *task;
35+
//@property (nullable, nonatomic, weak) NSURLSessionDataTask *task;
36+
@property (nonatomic, strong) __block NSURLSession * session;
3637

3738
- (void) sendRequest:(NSDictionary * _Nullable )options
3839
contentLength:(long)contentLength
@@ -42,6 +43,8 @@
4243
taskOperationQueue:(NSOperationQueue * _Nonnull)operationQueue
4344
callback:(_Nullable RCTResponseSenderBlock) callback;
4445

46+
- (void) cancelRequest:(NSString *)taskId;
47+
4548
@end
4649

4750
#endif /* RNFetchBlobRequest_h */

ios/RNFetchBlobRequest.m

Lines changed: 74 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,24 @@
1111
#import "RNFetchBlobFS.h"
1212
#import "RNFetchBlobConst.h"
1313
#import "RNFetchBlobReqBuilder.h"
14+
#if __has_include(<React/RCTLog.h>)
15+
#import <React/RCTLog.h>
16+
#else
17+
#import "RCTLog.h"
18+
#endif
1419

1520
#import "IOS7Polyfill.h"
1621
#import <CommonCrypto/CommonDigest.h>
1722

23+
NSMapTable * taskTable;
24+
25+
__attribute__((constructor))
26+
static void initialize_tables() {
27+
if(taskTable == nil)
28+
{
29+
taskTable = [[NSMapTable alloc] init];
30+
}
31+
}
1832

1933
typedef NS_ENUM(NSUInteger, ResponseFormat) {
2034
UTF8,
@@ -36,6 +50,7 @@ @interface RNFetchBlobRequest ()
3650
ResponseFormat responseFormat;
3751
BOOL followRedirect;
3852
BOOL backgroundTask;
53+
BOOL uploadTask;
3954
}
4055

4156
@end
@@ -82,6 +97,16 @@ - (void) sendRequest:(__weak NSDictionary * _Nullable )options
8297
self.options = options;
8398

8499
backgroundTask = [[options valueForKey:@"IOSBackgroundTask"] boolValue];
100+
uploadTask = [options valueForKey:@"IOSUploadTask"] == nil ? NO : [[options valueForKey:@"IOSUploadTask"] boolValue];
101+
102+
NSString * filepath = [options valueForKey:@"uploadFilePath"];
103+
104+
if (uploadTask && ![[NSFileManager defaultManager] fileExistsAtPath:[NSURL URLWithString:filepath].path]) {
105+
RCTLog(@"[RNFetchBlobRequest] sendRequest uploadTask file doesn't exist %@", filepath);
106+
callback(@[@"uploadTask file doesn't exist", @"", [NSNull null]]);
107+
return;
108+
}
109+
85110
// when followRedirect not set in options, defaults to TRUE
86111
followRedirect = [options valueForKey:@"followRedirect"] == nil ? YES : [[options valueForKey:@"followRedirect"] boolValue];
87112
isIncrement = [[options valueForKey:@"increment"] boolValue];
@@ -104,7 +129,6 @@ - (void) sendRequest:(__weak NSDictionary * _Nullable )options
104129

105130
NSString * path = [self.options valueForKey:CONFIG_FILE_PATH];
106131
NSString * key = [self.options valueForKey:CONFIG_KEY];
107-
NSURLSession * session;
108132

109133
bodyLength = contentLength;
110134

@@ -117,6 +141,7 @@ - (void) sendRequest:(__weak NSDictionary * _Nullable )options
117141
defaultConfigObject = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:taskId];
118142
}
119143

144+
120145
// request timeout, -1 if not set in options
121146
float timeout = [options valueForKey:@"timeout"] == nil ? -1 : [[options valueForKey:@"timeout"] floatValue];
122147

@@ -125,7 +150,7 @@ - (void) sendRequest:(__weak NSDictionary * _Nullable )options
125150
}
126151

127152
defaultConfigObject.HTTPMaximumConnectionsPerHost = 10;
128-
session = [NSURLSession sessionWithConfiguration:defaultConfigObject delegate:self delegateQueue:operationQueue];
153+
_session = [NSURLSession sessionWithConfiguration:defaultConfigObject delegate:self delegateQueue:operationQueue];
129154

130155
if (path || [self.options valueForKey:CONFIG_USE_TEMP]) {
131156
respFile = YES;
@@ -157,8 +182,19 @@ - (void) sendRequest:(__weak NSDictionary * _Nullable )options
157182
respFile = NO;
158183
}
159184

160-
self.task = [session dataTaskWithRequest:req];
161-
[self.task resume];
185+
__block NSURLSessionTask * task;
186+
187+
if(uploadTask)
188+
{
189+
task = [_session uploadTaskWithRequest:req fromFile:[NSURL URLWithString:filepath]];
190+
}
191+
else
192+
{
193+
task = [_session dataTaskWithRequest:req];
194+
}
195+
196+
[taskTable setObject:task forKey:taskId];
197+
[task resume];
162198

163199
// network status indicator
164200
if ([[options objectForKey:CONFIG_INDICATOR] boolValue]) {
@@ -182,6 +218,7 @@ - (void) sendRequest:(__weak NSDictionary * _Nullable )options
182218
// set expected content length on response received
183219
- (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
184220
{
221+
NSLog(@"sess didReceiveResponse");
185222
expectedBytes = [response expectedContentLength];
186223

187224
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
@@ -207,7 +244,7 @@ - (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dat
207244

208245
partBuffer = [[NSMutableData alloc] init];
209246
completionHandler(NSURLSessionResponseAllow);
210-
247+
211248
return;
212249
} else {
213250
self.isServerPush = [[respCType lowercaseString] RNFBContainsString:@"multipart/x-mixed-replace;"];
@@ -269,42 +306,6 @@ - (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dat
269306
NSLog(@"oops");
270307
}
271308

272-
if (respFile)
273-
{
274-
@try{
275-
NSFileManager * fm = [NSFileManager defaultManager];
276-
NSString * folder = [destPath stringByDeletingLastPathComponent];
277-
278-
if (![fm fileExistsAtPath:folder]) {
279-
[fm createDirectoryAtPath:folder withIntermediateDirectories:YES attributes:NULL error:nil];
280-
}
281-
282-
// if not set overwrite in options, defaults to TRUE
283-
BOOL overwrite = [options valueForKey:@"overwrite"] == nil ? YES : [[options valueForKey:@"overwrite"] boolValue];
284-
BOOL appendToExistingFile = [destPath RNFBContainsString:@"?append=true"];
285-
286-
appendToExistingFile = !overwrite;
287-
288-
// For solving #141 append response data if the file already exists
289-
// base on PR#139 @kejinliang
290-
if (appendToExistingFile) {
291-
destPath = [destPath stringByReplacingOccurrencesOfString:@"?append=true" withString:@""];
292-
}
293-
294-
if (![fm fileExistsAtPath:destPath]) {
295-
[fm createFileAtPath:destPath contents:[[NSData alloc] init] attributes:nil];
296-
}
297-
298-
writeStream = [[NSOutputStream alloc] initToFileAtPath:destPath append:appendToExistingFile];
299-
[writeStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
300-
[writeStream open];
301-
}
302-
@catch(NSException * ex)
303-
{
304-
NSLog(@"write file error");
305-
}
306-
}
307-
308309
completionHandler(NSURLSessionResponseAllow);
309310
}
310311

@@ -328,11 +329,7 @@ - (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dat
328329
chunkString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
329330
}
330331

331-
if (respFile) {
332-
[writeStream write:[data bytes] maxLength:[data length]];
333-
} else {
334-
[respData appendData:data];
335-
}
332+
[respData appendData:data];
336333

337334
if (expectedBytes == 0) {
338335
return;
@@ -353,8 +350,16 @@ - (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dat
353350
}
354351
}
355352

353+
- (void) cancelRequest:(NSString *)taskId
354+
{
355+
NSURLSessionDataTask * task = [taskTable objectForKey:taskId];
356+
if(task != nil && task.state == NSURLSessionTaskStateRunning)
357+
[task cancel];
358+
}
359+
356360
- (void) URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error
357361
{
362+
RCTLog(@"[RNFetchBlobRequest] session didBecomeInvalidWithError %@", [error description]);
358363
if ([session isEqual:session]) {
359364
session = nil;
360365
}
@@ -363,7 +368,7 @@ - (void) URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable
363368

364369
- (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
365370
{
366-
371+
RCTLog(@"[RNFetchBlobRequest] session didCompleteWithError %@", [error description]);
367372
self.error = error;
368373
NSString * errMsg;
369374
NSString * respStr;
@@ -416,10 +421,17 @@ - (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCom
416421
respStr ?: [NSNull null]
417422
]);
418423

424+
@synchronized(taskTable)
425+
{
426+
if([taskTable objectForKey:taskId] == nil)
427+
NSLog(@"object released by ARC.");
428+
else
429+
[taskTable removeObjectForKey:taskId];
430+
}
431+
419432
respData = nil;
420433
receivedBytes = 0;
421434
[session finishTasksAndInvalidate];
422-
423435
}
424436

425437
// upload progress handler
@@ -430,7 +442,7 @@ - (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSen
430442
}
431443

432444
NSNumber * now = [NSNumber numberWithFloat:((float)totalBytesWritten/(float)totalBytesExpectedToWrite)];
433-
445+
434446
if ([self.uploadProgressConfig shouldReport:now]) {
435447
[self.bridge.eventDispatcher
436448
sendDeviceEventWithName:EVENT_PROGRESS_UPLOAD
@@ -456,7 +468,19 @@ - (void) URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthentica
456468

457469
- (void) URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
458470
{
459-
NSLog(@"sess done in background");
471+
RCTLog(@"[RNFetchBlobRequest] session done in background");
472+
dispatch_async(dispatch_get_main_queue(), ^{
473+
id<UIApplicationDelegate> appDelegate = [UIApplication sharedApplication].delegate;
474+
SEL selector = NSSelectorFromString(@"backgroundTransferCompletionHandler");
475+
if ([appDelegate respondsToSelector:selector]) {
476+
void(^completionHandler)() = [appDelegate performSelector:selector];
477+
if (completionHandler != nil) {
478+
completionHandler();
479+
completionHandler = nil;
480+
}
481+
}
482+
483+
});
460484
}
461485

462486
- (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler

0 commit comments

Comments
 (0)