Merge branch 'ptelad-master'

* ptelad-master:
  completeHandlerIOS now return a promise so you can await for it
  Update RNFSManager.m
  better background downloads
  readme update to reflect that resume is only supported on iOS
  Added resumeDownload(), isResumable() and the ‘resume’ callback to downloadFile
This commit is contained in:
Hagen Hübel 2017-12-10 12:32:08 +01:00
commit 50090489db
6 changed files with 180 additions and 21 deletions

View File

@ -4,6 +4,7 @@ typedef void (^DownloadCompleteCallback)(NSNumber*, NSNumber*);
typedef void (^ErrorCallback)(NSError*);
typedef void (^BeginCallback)(NSNumber*, NSNumber*, NSDictionary*);
typedef void (^ProgressCallback)(NSNumber*, NSNumber*);
typedef void (^ResumableCallback)();
@interface RNFSDownloadParams : NSObject
@ -14,6 +15,7 @@ typedef void (^ProgressCallback)(NSNumber*, NSNumber*);
@property (copy) ErrorCallback errorCallback; // Something went wrong
@property (copy) BeginCallback beginCallback; // Download has started (headers received)
@property (copy) ProgressCallback progressCallback; // Download is progressing
@property (copy) ResumableCallback resumableCallback; // Download has stopped but is resumable
@property bool background; // Whether to continue download when app is in background
@property bool discretionary; // Whether the file may be downloaded at the OS's discretion (iOS only)
@property (copy) NSNumber* progressDivider;
@ -24,7 +26,9 @@ typedef void (^ProgressCallback)(NSNumber*, NSNumber*);
@interface RNFSDownloader : NSObject <NSURLSessionDelegate, NSURLSessionDownloadDelegate>
- (void)downloadFile:(RNFSDownloadParams*)params;
- (NSString *)downloadFile:(RNFSDownloadParams*)params;
- (void)stopDownload;
- (void)resumeDownload;
- (BOOL)isResumable;
@end

View File

@ -9,11 +9,12 @@
@property (copy) RNFSDownloadParams* params;
@property (retain) NSURLSession* session;
@property (retain) NSURLSessionTask* task;
@property (retain) NSURLSessionDownloadTask* task;
@property (retain) NSNumber* statusCode;
@property (retain) NSNumber* lastProgressValue;
@property (retain) NSNumber* contentLength;
@property (retain) NSNumber* bytesWritten;
@property (retain) NSData* resumeData;
@property (retain) NSFileHandle* fileHandle;
@ -21,9 +22,11 @@
@implementation RNFSDownloader
- (void)downloadFile:(RNFSDownloadParams*)params
- (NSString *)downloadFile:(RNFSDownloadParams*)params
{
_params = params;
NSString *uuid = nil;
_params = params;
_bytesWritten = 0;
@ -36,14 +39,15 @@
NSError* error = [NSError errorWithDomain:@"Downloader" code:NSURLErrorFileDoesNotExist
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"Failed to create target file at path: %@", _params.toFile]}];
return _params.errorCallback(error);
_params.errorCallback(error);
return nil;
} else {
[_fileHandle closeFile];
}
NSURLSessionConfiguration *config;
if (_params.background) {
NSString *uuid = [[NSUUID UUID] UUIDString];
uuid = [[NSUUID UUID] UUIDString];
config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:uuid];
config.discretionary = _params.discretionary;
} else {
@ -56,6 +60,8 @@
_session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
_task = [_session downloadTaskWithURL:url];
[_task resume];
return uuid;
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
@ -109,23 +115,48 @@
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
if (error && error.code != -999) {
_params.errorCallback(error);
_resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData];
if (_resumeData != nil) {
_params.resumableCallback();
} else {
_params.errorCallback(error);
}
}
}
- (void)stopDownload
{
if (_task.state == NSURLSessionTaskStateRunning) {
[_task cancel];
[_task cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
if (resumeData != nil) {
self.resumeData = resumeData;
_params.resumableCallback();
} else {
NSError *error = [NSError errorWithDomain:@"RNFS"
code:@"Aborted"
userInfo:@{
NSLocalizedDescriptionKey: @"Download has been aborted"
}];
_params.errorCallback(error);
}
}];
NSError *error = [NSError errorWithDomain:@"RNFS"
code:@"Aborted"
userInfo:@{
NSLocalizedDescriptionKey: @"Download has been aborted"
}];
return _params.errorCallback(error);
}
}
- (void)resumeDownload
{
if (_resumeData != nil) {
_task = [_session downloadTaskWithResumeData:_resumeData];
[_task resume];
_resumeData = nil;
}
}
- (BOOL)isResumable
{
return _resumeData != nil;
}
@end

View File

@ -213,10 +213,22 @@ var RNFS = {
RNFSManager.stopDownload(jobId);
},
resumeDownload(jobId: number): void {
RNFSManager.resumeDownload(jobId);
},
isResumable(jobId: number): Promise<bool> {
return RNFSManager.isResumable(jobId);
},
stopUpload(jobId: number): void {
RNFSManager.stopUpload(jobId);
},
completeHandlerIOS(jobId: number): void {
return RNFSManager.completeHandlerIOS(jobId);
},
readDir(dirpath: string): Promise<ReadDirItem[]> {
return readDirGeneric(dirpath, RNFSManager.readDir);
},
@ -444,6 +456,10 @@ var RNFS = {
subscriptions.push(NativeAppEventEmitter.addListener('DownloadProgress-' + jobId, options.progress));
}
if (options.resumable) {
subscriptions.push(NativeAppEventEmitter.addListener('DownloadResumable-' + jobId, options.resumable));
}
var bridgeOptions = {
jobId: jobId,
fromUrl: options.fromUrl,

View File

@ -507,6 +507,7 @@ type DownloadFileOptions = {
progressDivider?: number;
begin?: (res: DownloadBeginCallbackResult) => void;
progress?: (res: DownloadProgressCallbackResult) => void;
resumable?: () => void; // only supported on iOS yet
connectionTimeout?: number // only supported on Android yet
readTimeout?: number // supported on Android and iOS
};
@ -549,17 +550,36 @@ Use it for performance issues.
If `progressDivider` = 0, you will receive all `progressCallback` calls, default value is 0.
(IOS only): `options.background` (`Boolean`) - Whether to continue downloads when the app is not focused (default: `false`)
This option is currently only available for iOS, and you must [enable
background fetch](https://www.objc.io/issues/5-ios7/multitasking/#background-fetch<Paste>)
for your project in XCode. You only need to enable background fetch in `Info.plist` and set
the fetch interval in `didFinishLaunchingWithOptions`. The `performFetchWithCompletionHandler`
callback is handled by RNFS.
This option is currently only available for iOS, see the [Background Downloads Tutorial (iOS)](#background-downloads-tutorial-ios) section.
(IOS only): If `options.resumable` is provided, it will be invoked when the download has stopped and and can be resumed using `resumeDownload()`.
### `stopDownload(jobId: number): void`
Abort the current download job with this ID. The partial file will remain on the filesystem.
### (iOS only) `resumeDownload(jobId: number): void`
Resume the current download job with this ID.
### (iOS only) `isResumable(jobId: number): Promise<bool>`
Check if the the download job with this ID is resumable with `resumeDownload()`.
Example:
```
if (await RNFS.isResumable(jobId) {
RNFS.resumeDownload(jobId)
}
```
### (iOS only) `completeHandlerIOS(jobId: number): void`
For use when using background downloads, tell iOS you are done handling a completed download.
Read more about background donwloads in the [Background Downloads Tutorial (iOS)](#background-downloads-tutorial-ios) section.
### (iOS only) `uploadFiles(options: UploadFileOptions): { jobId: number, promise: Promise<UploadResult> }`
`options` (`Object`) - An object containing named parameters
@ -642,6 +662,35 @@ Invalid group identifier will cause a rejection.
For more information read the [Adding an App to an App Group](https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/EnablingAppSandbox.html#//apple_ref/doc/uid/TP40011195-CH4-SW19) section.
## Background Downloads Tutorial (iOS)
Background downloads in iOS require a bit of a setup.
First, in your `AppDelegate.m` file add the following:
```
#import <RNFSManager.h>
...
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
{
[RNFSManager setCompletionHandlerForIdentifier:identifier completionHandler:completionHandler];
}
```
The `handleEventsForBackgroundURLSession` method is called when a background download is done and your app is not in the foreground.
We need to pass the `completionHandler` to RNFS along with its `identifier`.
The JavaScript will continue to work as usual when the download is done but now you must call `RNFS.completeHandlerIOS(jobId)` when you're done handling the download (show a notification etc.)
**BE AWARE!** iOS will give about 30 sec. to run your code after `handleEventsForBackgroundURLSession` is called and until `completionHandler`
is triggered so don't do anything that might take a long time (like unzipping), you will be able to do it after the user re-launces the app,
otherwide iOS will terminate your app.
## Test / Demo app
Test app to demostrate the use of the module. Useful for testing and developing the module:

View File

@ -9,6 +9,10 @@
#import <React/RCTBridgeModule.h>
#import <React/RCTLog.h>
typedef void (^CompletionHandler)();
@interface RNFSManager : NSObject <RCTBridgeModule>
+(void)setCompletionHandlerForIdentifier: (NSString *)identifier completionHandler: (CompletionHandler)completionHandler;
@end

View File

@ -23,12 +23,15 @@
@interface RNFSManager()
@property (retain) NSMutableDictionary* downloaders;
@property (retain) NSMutableDictionary* uuids;
@property (retain) NSMutableDictionary* uploaders;
@end
@implementation RNFSManager
static NSMutableDictionary *completionHandlers;
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE();
@ -474,14 +477,22 @@ RCT_EXPORT_METHOD(downloadFile:(NSDictionary *)options
@"contentLength": contentLength,
@"bytesWritten": bytesWritten}];
};
params.resumableCallback = ^() {
[self.bridge.eventDispatcher sendAppEventWithName:[NSString stringWithFormat:@"DownloadResumable-%@", jobId] body:nil];
};
if (!self.downloaders) self.downloaders = [[NSMutableDictionary alloc] init];
RNFSDownloader* downloader = [RNFSDownloader alloc];
[downloader downloadFile:params];
NSString *uuid = [downloader downloadFile:params];
[self.downloaders setValue:downloader forKey:[jobId stringValue]];
if (uuid) {
if (!self.uuids) self.uuids = [[NSMutableDictionary alloc] init];
[self.uuids setValue:uuid forKey:[jobId stringValue]];
}
}
RCT_EXPORT_METHOD(stopDownload:(nonnull NSNumber *)jobId)
@ -493,6 +504,44 @@ RCT_EXPORT_METHOD(stopDownload:(nonnull NSNumber *)jobId)
}
}
RCT_EXPORT_METHOD(resumeDownload:(nonnull NSNumber *)jobId)
{
RNFSDownloader* downloader = [self.downloaders objectForKey:[jobId stringValue]];
if (downloader != nil) {
[downloader resumeDownload];
}
}
RCT_EXPORT_METHOD(isResumable:(nonnull NSNumber *)jobId
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject
)
{
RNFSDownloader* downloader = [self.downloaders objectForKey:[jobId stringValue]];
if (downloader != nil) {
resolve([NSNumber numberWithBool:[downloader isResumable]]);
} else {
resolve([NSNumber numberWithBool:NO]);
}
}
RCT_EXPORT_METHOD(completeHandlerIOS:(nonnull NSNumber *)jobId
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
if (self.uuids) {
NSString *uuid = [self.uuids objectForKey:[jobId stringValue]];
CompletionHandler completionHandler = [completionHandlers objectForKey:uuid];
if (completionHandler) {
completionHandler();
[completionHandlers removeObjectForKey:uuid];
}
}
resolve(nil);
}
RCT_EXPORT_METHOD(uploadFiles:(NSDictionary *)options
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
@ -819,4 +868,10 @@ RCT_EXPORT_METHOD(touch:(NSString*)filepath
};
}
+(void)setCompletionHandlerForIdentifier: (NSString *)identifier completionHandler: (CompletionHandler)completionHandler
{
if (!completionHandlers) completionHandlers = [[NSMutableDictionary alloc] init];
[completionHandlers setValue:completionHandler forKey:identifier];
}
@end