diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..bc2dae2a --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Vydia, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/android/src/main/java/com/vydia/UploaderModule.java b/android/src/main/java/com/vydia/UploaderModule.java index 2c7b9f70..95a4f159 100644 --- a/android/src/main/java/com/vydia/UploaderModule.java +++ b/android/src/main/java/com/vydia/UploaderModule.java @@ -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; @@ -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) { @@ -108,17 +100,35 @@ 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")); } @@ -126,48 +136,72 @@ public void startUpload(ReadableMap options, final Promise promise) { 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(); @@ -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) { diff --git a/index.js b/index.js index fc96d15e..709c3e84 100644 --- a/index.js +++ b/index.js @@ -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 } diff --git a/ios/VydiaRNFileUploader.m b/ios/VydiaRNFileUploader.m index a218252a..744bb4fe 100644 --- a/ios/VydiaRNFileUploader.m +++ b/ios/VydiaRNFileUploader.m @@ -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: @@ -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]; @@ -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); } @@ -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];