Refactored downloadFile, added stopDownload

This commit is contained in:
Chris Dell 2015-11-23 16:29:25 +00:00
parent de4f913bff
commit b603084ce7
10 changed files with 302 additions and 120 deletions

View File

@ -1,10 +1,24 @@
#import <Foundation/Foundation.h>
typedef void (^DownloaderCallback)(NSNumber*, NSNumber*);
typedef void (^ErrorCallback)(NSError*);
typedef void (^DownloaderCallback)(NSNumber*, NSNumber*, NSNumber*);
typedef void (^BeginCallback)(NSNumber*, NSNumber*, NSDictionary*);
typedef void (^ProgressCallback)(NSNumber*, NSNumber*);
@interface DownloadParams : NSObject
@property (copy) NSString* fromUrl;
@property (copy) NSString* toFile;
@property (copy) DownloaderCallback callback; // Download has finished (data written)
@property (copy) ErrorCallback errorCallback; // Something went wrong
@property (copy) BeginCallback beginCallback; // Download has started (headers received)
@property (copy) ProgressCallback progressCallback; // Download is progressing
@end
@interface Downloader : NSObject <NSURLConnectionDelegate>
- (void)downloadFile:(NSString*)urlStr toFile:(NSString*)downloadPath callback:(DownloaderCallback)callback errorCallback:(ErrorCallback)errorCallback progressCallback:(DownloaderCallback)progressCallback;
- (void)downloadFile:(DownloadParams*)params;
- (void)stopDownload;
@end

View File

@ -1,11 +1,14 @@
#import "Downloader.h"
@implementation DownloadParams
@end
@interface Downloader()
@property (copy) DownloaderCallback callback;
@property (copy) ErrorCallback errorCallback;
@property (copy) DownloaderCallback progressCallback;
@property (copy) DownloadParams* params;
@property (retain) NSURLConnection* connection;
@property (retain) NSNumber* statusCode;
@property (retain) NSNumber* contentLength;
@property (retain) NSNumber* bytesWritten;
@ -16,42 +19,40 @@
@implementation Downloader
- (void)downloadFile:(NSString*)urlStr toFile:(NSString*)downloadPath callback:(DownloaderCallback)callback errorCallback:(ErrorCallback)errorCallback progressCallback:(DownloaderCallback)progressCallback
- (void)downloadFile:(DownloadParams*)params
{
_callback = callback;
_errorCallback = errorCallback;
_progressCallback = progressCallback;
_params = params;
_bytesWritten = 0;
NSURL* url = [NSURL URLWithString:urlStr];
NSURL* url = [NSURL URLWithString:_params.fromUrl];
NSMutableURLRequest* downloadRequest = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:30];
[[NSFileManager defaultManager] createFileAtPath:downloadPath contents:nil attributes:nil];
[[NSFileManager defaultManager] createFileAtPath:_params.toFile contents:nil attributes:nil];
_fileHandle = [NSFileHandle fileHandleForWritingAtPath:downloadPath];
_fileHandle = [NSFileHandle fileHandleForWritingAtPath:_params.toFile];
if (!_fileHandle) {
NSError* error = [NSError errorWithDomain:@"Downloader" code:NSURLErrorFileDoesNotExist userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"Failed to create target file at path: %@", downloadPath]}];
NSError* error = [NSError errorWithDomain:@"Downloader" code:NSURLErrorFileDoesNotExist userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"Failed to create target file at path: %@", _params.toFile]}];
return _errorCallback(error);
return _params.errorCallback(error);
}
NSURLConnection* connection = [[NSURLConnection alloc] initWithRequest:downloadRequest delegate:self startImmediately:NO];
_connection = [[NSURLConnection alloc] initWithRequest:downloadRequest delegate:self startImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[_connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[connection start];
[_connection start];
}
- (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error
{
[_fileHandle closeFile];
return _errorCallback(error);
return _params.errorCallback(error);
}
- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response
@ -60,6 +61,8 @@
_statusCode = [NSNumber numberWithLong:httpUrlResponse.statusCode];
_contentLength = [NSNumber numberWithLong: httpUrlResponse.expectedContentLength];
return _params.beginCallback(_statusCode, _contentLength, httpUrlResponse.allHeaderFields);
}
- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data
@ -69,7 +72,7 @@
_bytesWritten = [NSNumber numberWithUnsignedInteger:[_bytesWritten unsignedIntegerValue] + data.length];
return _progressCallback(_statusCode, _contentLength, _bytesWritten);
return _params.progressCallback(_contentLength, _bytesWritten);
}
}
@ -77,7 +80,12 @@
{
[_fileHandle closeFile];
return _callback(_statusCode, _contentLength, _bytesWritten);
return _params.callback(_statusCode, _bytesWritten);
}
- (void)stopDownload
{
[_connection cancel];
}
@end

View File

@ -138,19 +138,30 @@ var RNFS = {
.catch(convertError);
},
downloadFile(url, filepath, progress) {
downloadFile(fromUrl, toFile, begin, progress) {
var jobId = getJobId();
var subscriptionIos, subscriptionAndroid;
if (progress) {
if (!begin) begin = (info) => {
console.log('Download begun:', info);
};
if (begin) {
// Two different styles of subscribing to events for different platforms, hmmm....
if (NativeAppEventEmitter.addListener)
subscriptionIos = NativeAppEventEmitter.addListener('DownloadProgress-' + jobId, progress);
subscriptionIos = NativeAppEventEmitter.addListener('DownloadBegin-' + jobId, begin);
if (DeviceEventEmitter.addListener)
subscriptionAndroid = DeviceEventEmitter.addListener('DownloadBegin-' + jobId, begin);
}
if (progress) {
if (NativeAppEventEmitter.addListener)
subscriptionIos = NativeAppEventEmitter.addListener('DownloadProgress-' + jobId, progress);
if (DeviceEventEmitter.addListener)
subscriptionAndroid = DeviceEventEmitter.addListener('DownloadProgress-' + jobId, progress);
}
return _downloadFile(url, filepath, jobId)
return _downloadFile(fromUrl, toFile, jobId)
.then(res => {
if (subscriptionIos) subscriptionIos.remove();
if (subscriptionAndroid) subscriptionAndroid.remove();

View File

@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
8BF740771C033A2E0057A1E7 /* Downloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF740761C033A2E0057A1E7 /* Downloader.m */; };
F1E59BDF1ADD662800ACA28A /* RNFSManager.m in Sources */ = {isa = PBXBuildFile; fileRef = F1E59BDE1ADD662800ACA28A /* RNFSManager.m */; };
F1EB08BB1AFD0E6A008F8F2B /* NSArray+Map.m in Sources */ = {isa = PBXBuildFile; fileRef = F1EB08BA1AFD0E6A008F8F2B /* NSArray+Map.m */; };
/* End PBXBuildFile section */
@ -24,6 +25,8 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
8BF740751C033A2E0057A1E7 /* Downloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Downloader.h; sourceTree = "<group>"; };
8BF740761C033A2E0057A1E7 /* Downloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Downloader.m; sourceTree = "<group>"; };
F12AFB9B1ADAF8F800E0535D /* libRNFS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNFS.a; sourceTree = BUILT_PRODUCTS_DIR; };
F1E59BDD1ADD662800ACA28A /* RNFSManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNFSManager.h; sourceTree = "<group>"; };
F1E59BDE1ADD662800ACA28A /* RNFSManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFSManager.m; sourceTree = "<group>"; };
@ -49,6 +52,8 @@
F1EB08BA1AFD0E6A008F8F2B /* NSArray+Map.m */,
F1E59BDD1ADD662800ACA28A /* RNFSManager.h */,
F1E59BDE1ADD662800ACA28A /* RNFSManager.m */,
8BF740751C033A2E0057A1E7 /* Downloader.h */,
8BF740761C033A2E0057A1E7 /* Downloader.m */,
F12AFB9C1ADAF8F800E0535D /* Products */,
);
sourceTree = "<group>";
@ -119,6 +124,7 @@
files = (
F1E59BDF1ADD662800ACA28A /* RNFSManager.m in Sources */,
F1EB08BB1AFD0E6A008F8F2B /* NSArray+Map.m in Sources */,
8BF740771C033A2E0057A1E7 /* Downloader.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -12,6 +12,12 @@
#import "Downloader.h"
#import "RCTEventDispatcher.h"
@interface RNFSManager()
@property (retain) NSMutableDictionary* downloaders;
@end
@implementation RNFSManager
@synthesize bridge = _bridge;
@ -147,25 +153,52 @@ RCT_EXPORT_METHOD(downloadFile:(NSString *)urlStr
jobId:(nonnull NSNumber *)jobId
callback:(RCTResponseSenderBlock)callback)
{
DownloadParams* params = [DownloadParams alloc];
params.fromUrl = urlStr;
params.toFile = filepath;
DownloaderCallback downloaderSuccessCallback = ^(NSNumber* statusCode, NSNumber* contentLength, NSNumber* bytesWritten) {
return callback(@[[NSNull null], [NSNumber numberWithBool:YES], filepath]);
params.callback = ^(NSNumber* statusCode, NSNumber* bytesWritten) {
return callback(@[[NSNull null], @{@"jobId": jobId,
@"statusCode": statusCode,
@"bytesWritten": bytesWritten}]);
};
ErrorCallback downloaderErrorCallback = ^(NSError* error) {
params.errorCallback = ^(NSError* error) {
return callback([self makeErrorPayload:error]);
};
DownloaderCallback downloaderProgressCallback = ^(NSNumber* statusCode, NSNumber* contentLength, NSNumber* bytesWritten) {
[self.bridge.eventDispatcher sendAppEventWithName:[NSString stringWithFormat:@"DownloadProgress-%@", jobId]
body:@{@"statusCode": statusCode,
params.beginCallback = ^(NSNumber* statusCode, NSNumber* contentLength, NSDictionary* headers) {
[self.bridge.eventDispatcher sendAppEventWithName:[NSString stringWithFormat:@"DownloadBegin-%@", jobId]
body:@{@"jobId": jobId,
@"statusCode": statusCode,
@"contentLength": contentLength,
@"headers": headers}];
};
params.progressCallback = ^(NSNumber* contentLength, NSNumber* bytesWritten) {
[self.bridge.eventDispatcher sendAppEventWithName:[NSString stringWithFormat:@"DownloadProgress-%@", jobId]
body:@{@"contentLength": contentLength,
@"bytesWritten": bytesWritten}];
};
if (self.downloaders) self.downloaders = [NSMutableDictionary alloc];
Downloader* downloader = [Downloader alloc];
[downloader downloadFile:urlStr toFile:filepath callback:downloaderSuccessCallback errorCallback:downloaderErrorCallback progressCallback:downloaderProgressCallback];
[downloader downloadFile:params];
[self.downloaders setValue:downloader forKey:[jobId stringValue]];
}
RCT_EXPORT_METHOD(stopDownload:(NSNumber *)jobId)
{
Downloader* downloader = [self.downloaders objectForKey:[jobId stringValue]];
if (downloader != nil) {
[downloader stopDownload];
}
}
RCT_EXPORT_METHOD(pathForBundle:(NSString *)bundleNamed

View File

@ -0,0 +1,25 @@
package com.rnfs;
import java.io.File;
import java.net.URL;
import java.util.*;
public class DownloadParams {
public interface OnTaskCompleted {
void onTaskCompleted(DownloadResult res);
}
public interface OnDownloadBegin {
void onDownloadBegin(int statusCode, int contentLength, Map<String, String> headers);
}
public interface OnDownloadProgress {
void onDownloadProgress(int contentLength, int bytesWritten);
}
public URL src;
public File dest;
public OnTaskCompleted onTaskCompleted;
public OnDownloadBegin onDownloadBegin;
public OnDownloadProgress onDownloadProgress;
}

View File

@ -0,0 +1,7 @@
package com.rnfs;
public class DownloadResult {
public int statusCode;
public int bytesWritten;
public Exception exception;
}

View File

@ -0,0 +1,107 @@
package com.rnfs;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.net.HttpURLConnection;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import android.os.AsyncTask;
public class Downloader extends AsyncTask<DownloadParams, int[], DownloadResult> {
private DownloadParams mParam;
private AtomicBoolean mAbort = new AtomicBoolean(false);
protected DownloadResult doInBackground(DownloadParams... params) {
mParam = params[0];
DownloadResult res = new DownloadResult();
try {
this.download(mParam, res);
mParam.onTaskCompleted.onTaskCompleted(res);
} catch (Exception ex) {
res.exception = ex;
mParam.onTaskCompleted.onTaskCompleted(res);
return res;
}
return res;
}
private void download(DownloadParams param, DownloadResult res) throws IOException {
InputStream input = null;
OutputStream output = null;
try {
HttpURLConnection connection = (HttpURLConnection)param.src.openConnection();
connection.setConnectTimeout(5000);
connection.connect();
int statusCode = connection.getResponseCode();
int lengthOfFile = connection.getContentLength();
Map<String, List<String>> headers = connection.getHeaderFields();
Map<String, String> headersFlat = new HashMap<String, String>();
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
String headerKey = entry.getKey();
String valueKey = entry.getValue().get(0);
if (headerKey != null && valueKey != null) {
headersFlat.put(headerKey, valueKey);
}
}
mParam.onDownloadBegin.onDownloadBegin(statusCode, lengthOfFile, headersFlat);
input = new BufferedInputStream(param.src.openStream(), 8 * 1024);
output = new FileOutputStream(param.dest);
byte data[] = new byte[8 * 1024];
int total = 0;
int count;
while ((count = input.read(data)) != -1) {
if (mAbort.get()) {
break;
}
total += count;
publishProgress(new int[] { lengthOfFile, total });
output.write(data, 0, count);
}
output.flush();
res.statusCode = statusCode;
res.bytesWritten = total;
} finally {
if (output != null) output.close();
if (input != null) input.close();
}
}
protected void stop() {
mAbort.set(true);
}
@Override
protected void onProgressUpdate(int[]... values) {
super.onProgressUpdate(values);
mParam.onDownloadProgress.onDownloadProgress(values[0][0], values[0][1]);
}
protected void onPostExecute(Exception ex) {
}
}

View File

@ -1,5 +1,6 @@
package com.rnfs;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
@ -9,6 +10,7 @@ import android.os.AsyncTask;
import android.util.Base64;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.SparseArray;
import java.io.File;
import java.io.FileOutputStream;
@ -31,7 +33,6 @@ import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.modules.core.DeviceEventManagerModule;
public class RNFSManager extends ReactContextBaseJavaModule {
@ -41,6 +42,8 @@ public class RNFSManager extends ReactContextBaseJavaModule {
private static final String NSFileTypeRegular = "NSFileTypeRegular";
private static final String NSFileTypeDirectory = "NSFileTypeDirectory";
private SparseArray<Downloader> downloaders = new SparseArray<Downloader>();
public RNFSManager(ReactApplicationContext reactContext) {
super(reactContext);
@ -182,8 +185,8 @@ public class RNFSManager extends ReactContextBaseJavaModule {
private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
@ReactMethod
@ -193,110 +196,72 @@ public class RNFSManager extends ReactContextBaseJavaModule {
URL url = new URL(urlStr);
DownloadParams params = new DownloadParams();
params.src = url;
params.dest = file;
params.onTaskCompleted = new OnTaskCompleted() {
public void onTaskCompleted(Exception ex) {
if (ex == null) {
boolean success = true;
callback.invoke(null, success, filepath);
params.onTaskCompleted = new DownloadParams.OnTaskCompleted() {
public void onTaskCompleted(DownloadResult res) {
if (res.exception == null) {
WritableMap infoMap = Arguments.createMap();
infoMap.putInt("jobId", jobId);
infoMap.putInt("statusCode", res.statusCode);
infoMap.putInt("bytesWritten", res.bytesWritten);
callback.invoke(null, infoMap);
} else {
callback.invoke(makeErrorPayload(ex));
callback.invoke(makeErrorPayload(res.exception));
}
}
};
params.onDownloadProgress = new OnDownloadProgress() {
public void onDownloadProgress(int statusCode, int contentLength, int bytesWritten) {
params.onDownloadBegin = new DownloadParams.OnDownloadBegin() {
public void onDownloadBegin(int statusCode, int contentLength, Map<String, String> headers) {
WritableMap headersMap = Arguments.createMap();
for (Map.Entry<String, String> entry : headers.entrySet()) {
headersMap.putString(entry.getKey(), entry.getValue());
}
WritableMap data = Arguments.createMap();
data.putInt("jobId", jobId);
data.putInt("statusCode", statusCode);
data.putInt("contentLength", contentLength);
data.putMap("headers", headersMap);
sendEvent(getReactApplicationContext(), "DownloadBegin-" + jobId, data);
}
};
params.onDownloadProgress = new DownloadParams.OnDownloadProgress() {
public void onDownloadProgress(int contentLength, int bytesWritten) {
WritableMap data = Arguments.createMap();
data.putInt("contentLength", contentLength);
data.putInt("bytesWritten", bytesWritten);
sendEvent(getReactApplicationContext(), "DownloadProgress-" + jobId, data);
}
};
DownloadTask downloadTask = new DownloadTask();
downloadTask.execute(params);
Downloader downloader = new Downloader();
downloader.execute(params);
this.downloaders.put(jobId, downloader);
} catch (Exception ex) {
ex.printStackTrace();
callback.invoke(makeErrorPayload(ex));
}
}
private class DownloadParams {
public URL src;
public File dest;
public OnTaskCompleted onTaskCompleted;
public OnDownloadProgress onDownloadProgress;
}
public interface OnTaskCompleted {
void onTaskCompleted(Exception ex);
}
public interface OnDownloadProgress {
void onDownloadProgress(int statusCode, int contentLength, int bytesWritten);
}
private class DownloadTask extends AsyncTask<DownloadParams, int[], Exception> {
private DownloadParams mParam;
protected Exception doInBackground(DownloadParams... params) {
mParam = params[0];
try {
this.download(mParam);
mParam.onTaskCompleted.onTaskCompleted(null);
} catch (Exception ex) {
mParam.onTaskCompleted.onTaskCompleted(ex);
return ex;
}
return null;
}
private void download(DownloadParams param) throws IOException {
InputStream input = null;
OutputStream output = null;
try {
HttpURLConnection connection = (HttpURLConnection)param.src.openConnection();
connection.setConnectTimeout(5000);
connection.connect();
int statusCode = connection.getResponseCode();
int lengthOfFile = connection.getContentLength();
input = new BufferedInputStream(param.src.openStream(), 8 * 1024);
output = new FileOutputStream(param.dest);
byte data[] = new byte[8 * 1024];
int total = 0;
int count;
while ((count = input.read(data)) != -1) {
total += count;
publishProgress(new int[] { statusCode, lengthOfFile, total });
output.write(data, 0, count);
}
output.flush();
} finally {
if (output != null) output.close();
if (input != null) input.close();
}
}
@Override
protected void onProgressUpdate(int[]... values) {
super.onProgressUpdate(values);
mParam.onDownloadProgress.onDownloadProgress(values[0][0], values[0][1], values[0][2]);
}
protected void onPostExecute(Exception ex) {
@ReactMethod
public void stopDownload(int jobId) {
Downloader downloader = this.downloaders.get(jobId);
if (downloader != null) {
downloader.stop();
}
}

6
jsconfig.json Normal file
View File

@ -0,0 +1,6 @@
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs"
}
}