Skip to content

Commit

Permalink
Merge pull request #36 from ovr/multipart-form-support
Browse files Browse the repository at this point in the history
Support multipart form upload, refs #33
  • Loading branch information
StevePotter committed Oct 4, 2017
2 parents c11191c + dc453ef commit 8576fbb
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 55 deletions.
133 changes: 84 additions & 49 deletions android/src/main/java/com/vydia/UploaderModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import com.facebook.react.modules.core.DeviceEventManagerModule;

import net.gotev.uploadservice.BinaryUploadRequest;
import net.gotev.uploadservice.HttpUploadRequest;
import net.gotev.uploadservice.MultipartUploadRequest;
import net.gotev.uploadservice.ServerResponse;
import net.gotev.uploadservice.UploadInfo;
import net.gotev.uploadservice.UploadNotificationConfig;
Expand Down Expand Up @@ -83,18 +85,8 @@ public void getFileInfo(String path, final Promise promise) {
}

/*
* Starts a file upload.
* Options are passed in as the first argument as a js hash:
* {
* url: string. url to post to.
* path: string. path to the file on the device
* headers: hash of name/value header pairs
* method: HTTP method to use. Default is "POST"
* notification: hash for customizing tray notifiaction
* enabled: boolean to enable/disabled notifications, true by default.
* }
*
* Returns a promise with the string ID of the upload.
* Starts a file upload.
* Returns a promise with the string ID of the upload.
*/
@ReactMethod
public void startUpload(ReadableMap options, final Promise promise) {
Expand All @@ -108,66 +100,108 @@ public void startUpload(ReadableMap options, final Promise promise) {
return;
}
}

if (options.hasKey("headers") && options.getType("headers") != ReadableType.Map) {
promise.reject(new IllegalArgumentException("headers must be a hash."));
return;
}

if (options.hasKey("notification") && options.getType("notification") != ReadableType.Map) {
promise.reject(new IllegalArgumentException("notification must be a hash."));
return;
}

String requestType = "raw";

if (options.hasKey("type")) {
requestType = options.getString("type");
if (requestType == null) {
promise.reject(new IllegalArgumentException("type must be string."));
return;
}

if (!requestType.equals("raw") && !requestType.equals("multipart")) {
promise.reject(new IllegalArgumentException("type should be string: raw or multipart."));
return;
}
}

WritableMap notification = new WritableNativeMap();
notification.putBoolean("enabled", true);

if (options.hasKey("notification")) {
notification.merge(options.getMap("notification"));
}

String url = options.getString("url");
String filePath = options.getString("path");
String method = options.hasKey("method") && options.getType("method") == ReadableType.String ? options.getString("method") : "POST";

final String customUploadId = options.hasKey("customUploadId") && options.getType("method") == ReadableType.String ? options.getString("customUploadId") : null;

try {
final BinaryUploadRequest request = (BinaryUploadRequest) new BinaryUploadRequest(this.getReactApplicationContext(), url)
.setMethod(method)
.setFileToUpload(filePath)
.setMaxRetries(2)
.setDelegate(new UploadStatusDelegate() {
@Override
public void onProgress(Context context, UploadInfo uploadInfo) {
WritableMap params = Arguments.createMap();
params.putString("id", customUploadId != null ? customUploadId : uploadInfo.getUploadId());
params.putInt("progress", uploadInfo.getProgressPercent()); //0-100
sendEvent("progress", params);
}

@Override
public void onError(Context context, UploadInfo uploadInfo, Exception exception) {
WritableMap params = Arguments.createMap();
params.putString("id", customUploadId != null ? customUploadId : uploadInfo.getUploadId());
params.putString("error", exception.getMessage());
sendEvent("error", params);
}

@Override
public void onCompleted(Context context, UploadInfo uploadInfo, ServerResponse serverResponse) {
WritableMap params = Arguments.createMap();
params.putString("id", customUploadId != null ? customUploadId : uploadInfo.getUploadId());
params.putInt("responseCode", serverResponse.getHttpCode());
params.putString("responseBody", serverResponse.getBodyAsString());
sendEvent("completed", params);
}

@Override
public void onCancelled(Context context, UploadInfo uploadInfo) {
WritableMap params = Arguments.createMap();
params.putString("id", customUploadId != null ? customUploadId : uploadInfo.getUploadId());
sendEvent("cancelled", params);
}
});
UploadStatusDelegate statusDelegate = new UploadStatusDelegate() {
@Override
public void onProgress(Context context, UploadInfo uploadInfo) {
WritableMap params = Arguments.createMap();
params.putString("id", customUploadId != null ? customUploadId : uploadInfo.getUploadId());
params.putInt("progress", uploadInfo.getProgressPercent()); //0-100
sendEvent("progress", params);
}

@Override
public void onError(Context context, UploadInfo uploadInfo, Exception exception) {
WritableMap params = Arguments.createMap();
params.putString("id", customUploadId != null ? customUploadId : uploadInfo.getUploadId());
params.putString("error", exception.getMessage());
sendEvent("error", params);
}

@Override
public void onCompleted(Context context, UploadInfo uploadInfo, ServerResponse serverResponse) {
WritableMap params = Arguments.createMap();
params.putString("id", customUploadId != null ? customUploadId : uploadInfo.getUploadId());
params.putInt("responseCode", serverResponse.getHttpCode());
params.putString("responseBody", serverResponse.getBodyAsString());
sendEvent("completed", params);
}

@Override
public void onCancelled(Context context, UploadInfo uploadInfo) {
WritableMap params = Arguments.createMap();
params.putString("id", customUploadId != null ? customUploadId : uploadInfo.getUploadId());
sendEvent("cancelled", params);
}
};

HttpUploadRequest<?> request;

if (requestType.equals("raw")) {
request = new BinaryUploadRequest(this.getReactApplicationContext(), customUploadId, url)
.setFileToUpload(filePath);
} else {
if (!options.hasKey("field")) {
promise.reject(new IllegalArgumentException("field is required field for multipart type."));
return;
}

if (options.getType("field") != ReadableType.String) {
promise.reject(new IllegalArgumentException("field must be string."));
return;
}

request = new MultipartUploadRequest(this.getReactApplicationContext(), customUploadId, url)
.addFileToUpload(filePath, options.getString("field"));
}

request.setMethod(method)
.setMaxRetries(2)
.setDelegate(statusDelegate);

if (notification.getBoolean("enabled")) {
request.setNotificationConfig(new UploadNotificationConfig());
}

if (options.hasKey("headers")) {
ReadableMap headers = options.getMap("headers");
ReadableMapKeySetIterator keys = headers.keySetIterator();
Expand All @@ -180,6 +214,7 @@ public void onCancelled(Context context, UploadInfo uploadInfo) {
request.addHeader(key, headers.getString(key));
}
}

String uploadId = request.startUpload();
promise.resolve(customUploadId != null ? customUploadId : uploadId);
} catch (Exception exc) {
Expand Down
5 changes: 5 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ export type NotificationArgs = {
export type StartUploadArgs = {
url: string,
path: string,
// Optional, because raw is default
type?: 'raw' | 'multipart',
// This option is needed for multipart type
field?: string,
customUploadId?: string,
headers?: Object,
notification?: NotificationArgs
}
Expand Down
57 changes: 51 additions & 6 deletions ios/VydiaRNFileUploader.m
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ - (NSString *)guessMIMETypeFromFileName: (NSString *)fileName {
return (__bridge NSString *)(MIMEType);
}


/*
* Starts a file upload.
* Options are passed in as the first argument as a js hash:
Expand All @@ -113,15 +112,19 @@ - (NSString *)guessMIMETypeFromFileName: (NSString *)fileName {
{
thisUploadId = uploadId++;
}

NSString *uploadUrl = options[@"url"];
NSString *fileURI = options[@"path"];
NSString *method = options[@"method"];
NSString *customUploadId = options[@"customUploadId"];
NSString *method = options[@"method"] ?: @"POST";
NSString *uploadType = options[@"type"] ?: @"raw";
NSString *fieldName = options[@"field"];
NSString *customUploadId = options[@"customUploadId"];
NSDictionary *headers = options[@"headers"];

@try {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString: uploadUrl]];
request.HTTPMethod = method ? method : @"POST";
[request setHTTPMethod: method];

[headers enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull val, BOOL * _Nonnull stop) {
if ([val respondsToSelector:@selector(stringValue)]) {
val = [val stringValue];
Expand All @@ -130,8 +133,24 @@ - (NSString *)guessMIMETypeFromFileName: (NSString *)fileName {
[request setValue:val forHTTPHeaderField:key];
}
}];
NSURLSessionDataTask *uploadTask = [[self urlSession:thisUploadId] uploadTaskWithRequest:request fromFile:[NSURL URLWithString: fileURI]];

NSURLSessionDataTask *uploadTask;

if ([uploadType isEqualToString:@"multipart"]) {
NSString *uuidStr = [[NSUUID UUID] UUIDString];
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", uuidStr] forHTTPHeaderField:@"Content-Type"];

NSData *httpBody = [self createBodyWithBoundary:uuidStr path:fileURI fieldName:fieldName];
[request setHTTPBody: httpBody];

// I am sorry about warning, but Upload tasks from NSData are not supported in background sessions.
uploadTask = [[self urlSession:thisUploadId] uploadTaskWithRequest:request fromData: nil];
} else {
uploadTask = [[self urlSession:thisUploadId] uploadTaskWithRequest:request fromFile:[NSURL URLWithString: fileURI]];
}

uploadTask.taskDescription = customUploadId ? customUploadId : [NSString stringWithFormat:@"%i", thisUploadId];

[uploadTask resume];
resolve(uploadTask.taskDescription);
}
Expand All @@ -140,6 +159,32 @@ - (NSString *)guessMIMETypeFromFileName: (NSString *)fileName {
}
}

- (NSData *)createBodyWithBoundary:(NSString *)boundary
path:(NSString *)path
fieldName:(NSString *)fieldName {

NSMutableData *httpBody = [NSMutableData data];

// resolve path
NSURL *fileUri = [NSURL URLWithString: path];
NSString *pathWithoutProtocol = [fileUri path];

NSData *data = [[NSFileManager defaultManager] contentsAtPath:pathWithoutProtocol];

NSString *filename = [path lastPathComponent];
NSString *mimetype = [self guessMIMETypeFromFileName:path];

[httpBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[httpBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", fieldName, filename] dataUsingEncoding:NSUTF8StringEncoding]];
[httpBody appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", mimetype] dataUsingEncoding:NSUTF8StringEncoding]];
[httpBody appendData:data];
[httpBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];

[httpBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];

return httpBody;
}

- (NSURLSession *)urlSession: (int) thisUploadId{
if(_urlSession == nil) {
NSURLSessionConfiguration *sessionConfigurationt = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:BACKGROUND_SESSION_ID];
Expand Down

0 comments on commit 8576fbb

Please sign in to comment.