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:
commit
50090489db
|
@ -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
|
||||
|
|
59
Downloader.m
59
Downloader.m
|
@ -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
|
||||
|
|
16
FS.common.js
16
FS.common.js
|
@ -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,
|
||||
|
|
59
README.md
59
README.md
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue