diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE index edadaa9b6..cef8ed2af 100644 --- a/.github/PULL_REQUEST_TEMPLATE +++ b/.github/PULL_REQUEST_TEMPLATE @@ -1,5 +1,5 @@ Thank you for making a pull request ! Just a gentle reminder :) 1. If the PR is offering a feature please make the request to our "Feature Branch" 0.11.0 -2. Bug fix request to "Bug Fix Branch" 0.10.7 +2. Bug fix request to "Bug Fix Branch" 0.10.8 3. Correct README.md can directly to master diff --git a/README.md b/README.md index b315848b9..7680fab86 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,13 @@ A project committed to making file access and data transfer easier and more effi * [Android Media Scanner, and Download Manager Support](#user-content-android-media-scanner-and-download-manager-support) * [Self-Signed SSL Server](#user-content-self-signed-ssl-server) * [Transfer Encoding](#user-content-transfer-encoding) - * [RNFetchBlob as Fetch](#user-content-rnfetchblob-as-fetch) + * [Drop-in Fetch Replacement](#user-content-drop-in-fetch-replacement) * [File System](#user-content-file-system) * [File access](#user-content-file-access) * [File stream](#user-content-file-stream) * [Manage cached files](#user-content-cache-file-management) * [Web API Polyfills](#user-content-web-api-polyfills) -* [Performance Tips](#user-content-performance-tipsd) +* [Performance Tips](#user-content-performance-tips) * [API References](https://github.com/wkh237/react-native-fetch-blob/wiki/Fetch-API) * [Caveats](#user-content-caveats) * [Development](#user-content-development) @@ -452,11 +452,11 @@ task.cancel((err) => { ... }) ``` -### RNFetchBlob as Fetch +### Drop-in Fetch Replacement 0.9.0 -If you have existing code that uses `whatwg-fetch`(the official **fetch**), you don't have to change them after 0.9.0, just use fetch replacement. The difference between Official fetch and fetch replacement is, official fetch uses [whatwg-fetch](https://github.com/github/fetch) js library which wraps XMLHttpRequest polyfill under the hood it's a great library for web developers, however that does not play very well with RN. Our implementation is simply a wrapper of RNFetchBlob.fetch and fs APIs, so you can access all the features we provide. +If you have existing code that uses `whatwg-fetch`(the official **fetch**), it's not necessary to replace them with `RNFetchblob.fetch`, you can simply use our **Fetch Replacement**. The difference between Official them is official fetch uses [whatwg-fetch](https://github.com/github/fetch) which wraps XMLHttpRequest polyfill under the hood. It's a great library for web developers, but does not play very well with RN. Our implementation is simply a wrapper of our `fetch` and `fs` APIs, so you can access all the features we provided. [See document and examples](https://github.com/wkh237/react-native-fetch-blob/wiki/Fetch-API#fetch-replacement) @@ -613,6 +613,8 @@ In `v0.5.0` we've added `writeStream` and `readStream`, which allows your app r When calling `readStream` method, you have to `open` the stream, and start to read data. When the file is large, consider using an appropriate `bufferSize` and `interval` to reduce the native event dispatching overhead (see [Performance Tips](#user-content-performance-tips)) +> The file stream event has a default throttle(10ms) and buffer size which preventing it cause too much overhead to main thread, yo can also [tweak these values](#user-content-performance-tips). + ```js let data = '' RNFetchBlob.fs.readStream( diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java index 1537bca71..7a7910546 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java +++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java @@ -138,11 +138,13 @@ static public void writeFile(String path, ReadableArray data, final boolean appe * @param promise */ static public void readFile(String path, String encoding, final Promise promise ) { - path = normalizePath(path); + String resolved = normalizePath(path); + if(resolved != null) + path = resolved; try { byte[] bytes; - if(path.startsWith(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET)) { + if(resolved != null && resolved.startsWith(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET)) { String assetName = path.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, ""); long length = RNFetchBlob.RCTContext.getAssets().openFd(assetName).getLength(); bytes = new byte[(int) length]; @@ -150,6 +152,14 @@ static public void readFile(String path, String encoding, final Promise promise in.read(bytes, 0, (int) length); in.close(); } + // issue 287 + else if(resolved == null) { + InputStream in = RNFetchBlob.RCTContext.getContentResolver().openInputStream(Uri.parse(path)); + int length = (int) in.available(); + bytes = new byte[length]; + in.read(bytes); + in.close(); + } else { File f = new File(path); int length = (int) f.length(); @@ -226,7 +236,9 @@ static public String getTmpPath(ReactApplicationContext ctx, String taskId) { * @param bufferSize Buffer size of read stream, default to 4096 (4095 when encode is `base64`) */ public void readStream(String path, String encoding, int bufferSize, int tick, final String streamId) { - path = normalizePath(path); + String resolved = normalizePath(path); + if(resolved != null) + path = resolved; try { int chunkSize = encoding.equalsIgnoreCase("base64") ? 4095 : 4096; @@ -234,9 +246,14 @@ public void readStream(String path, String encoding, int bufferSize, int tick, f chunkSize = bufferSize; InputStream fs; - if(path.startsWith(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET)) { - fs = RNFetchBlob.RCTContext.getAssets() - .open(path.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, "")); + + if(resolved != null && path.startsWith(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET)) { + fs = RNFetchBlob.RCTContext.getAssets().open(path.replace(RNFetchBlobConst.FILE_PREFIX_BUNDLE_ASSET, "")); + + } + // fix issue 287 + else if(resolved == null) { + fs = RNFetchBlob.RCTContext.getContentResolver().openInputStream(Uri.parse(path)); } else { fs = new FileInputStream(new File(path)); diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobPackage.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobPackage.java index 74e0224a7..48aac7ac3 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobPackage.java +++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobPackage.java @@ -20,7 +20,6 @@ public List createNativeModules(ReactApplicationContext reactConte return modules; } - @Override public List> createJSModules() { return Collections.emptyList(); } diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java index db213c1e8..8a81a832e 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java +++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java @@ -7,10 +7,12 @@ import android.content.IntentFilter; import android.database.Cursor; import android.net.Uri; +import android.os.Build; import android.util.Base64; import com.RNFetchBlob.Response.RNFetchBlobDefaultResp; import com.RNFetchBlob.Response.RNFetchBlobFileResp; +import com.facebook.common.logging.FLog; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactApplicationContext; @@ -21,6 +23,7 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.network.OkHttpClientProvider; +import com.facebook.react.modules.network.TLSSocketFactory; import java.io.File; import java.io.FileOutputStream; @@ -35,11 +38,14 @@ import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.util.ArrayList; +import java.util.List; import java.util.HashMap; + import java.util.concurrent.TimeUnit; import okhttp3.Call; import okhttp3.ConnectionPool; +import okhttp3.ConnectionSpec; import okhttp3.Headers; import okhttp3.Interceptor; import okhttp3.MediaType; @@ -48,6 +54,8 @@ import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; +import okhttp3.TlsVersion; + public class RNFetchBlobReq extends BroadcastReceiver implements Runnable { @@ -148,8 +156,15 @@ public void run() { if(options.addAndroidDownloads.hasKey("path")) { req.setDestinationUri(Uri.parse("file://" + options.addAndroidDownloads.getString("path"))); } + // #391 Add MIME type to the request + if(options.addAndroidDownloads.hasKey("mime")) { + req.setMimeType(options.addAndroidDownloads.getString("mime")); + } // set headers ReadableMapKeySetIterator it = headers.keySetIterator(); + if(options.addAndroidDownloads.hasKey("mediaScannable") && options.addAndroidDownloads.hasKey("mediaScannable") == true ) { + req.allowScanningByMediaScanner(); + } while (it.hasNextKey()) { String key = it.nextKey(); req.addRequestHeader(key, headers.getString(key)); @@ -359,9 +374,10 @@ public Response intercept(Chain chain) throws IOException { clientBuilder.retryOnConnectionFailure(false); clientBuilder.followRedirects(options.followRedirect); clientBuilder.followSslRedirects(options.followRedirect); + clientBuilder.retryOnConnectionFailure(true); + OkHttpClient client = enableTls12OnPreLollipop(clientBuilder).build(); - OkHttpClient client = clientBuilder.retryOnConnectionFailure(true).build(); Call call = client.newCall(req); taskTable.put(taskId, call); call.enqueue(new okhttp3.Callback() { @@ -636,16 +652,20 @@ public void onReceive(Context context, Intent intent) { return; } String contentUri = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)); - if (contentUri != null) { + if ( contentUri != null && + options.addAndroidDownloads.hasKey("mime") && + options.addAndroidDownloads.getString("mime").contains("image")) { Uri uri = Uri.parse(contentUri); Cursor cursor = appCtx.getContentResolver().query(uri, new String[]{android.provider.MediaStore.Images.ImageColumns.DATA}, null, null, null); - // use default destination of DownloadManager + + // use default destination of DownloadManager if (cursor != null) { cursor.moveToFirst(); filePath = cursor.getString(0); } } } + // When the file is not found in media content database, check if custom path exists if (options.addAndroidDownloads.hasKey("path")) { try { @@ -672,5 +692,28 @@ public void onReceive(Context context, Intent intent) { } } + public static OkHttpClient.Builder enableTls12OnPreLollipop(OkHttpClient.Builder client) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { + try { + client.sslSocketFactory(new TLSSocketFactory()); + + ConnectionSpec cs = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .tlsVersions(TlsVersion.TLS_1_2) + .build(); + + List< ConnectionSpec > specs = new ArrayList < > (); + specs.add(cs); + specs.add(ConnectionSpec.COMPATIBLE_TLS); + specs.add(ConnectionSpec.CLEARTEXT); + + client.connectionSpecs(specs); + } catch (Exception exc) { + FLog.e("OkHttpClientProvider", "Error while enabling TLS 1.2", exc); + } + } + + return client; + } + } diff --git a/android/src/main/java/com/RNFetchBlob/Utils/PathResolver.java b/android/src/main/java/com/RNFetchBlob/Utils/PathResolver.java index 381a3f9f3..fef3ada97 100644 --- a/android/src/main/java/com/RNFetchBlob/Utils/PathResolver.java +++ b/android/src/main/java/com/RNFetchBlob/Utils/PathResolver.java @@ -64,8 +64,16 @@ else if (isMediaDocument(uri)) { return getDataColumn(context, contentUri, selection, selectionArgs); } + else if ("content".equalsIgnoreCase(uri.getScheme())) { + + // Return the remote address + if (isGooglePhotosUri(uri)) + return uri.getLastPathSegment(); + + return getDataColumn(context, uri, null, null); + } // Other Providers - else { + else{ try { InputStream attachment = context.getContentResolver().openInputStream(uri); if (attachment != null) { @@ -131,6 +139,7 @@ public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { Cursor cursor = null; + String result = null; final String column = "_data"; final String[] projection = { column @@ -141,13 +150,18 @@ public static String getDataColumn(Context context, Uri uri, String selection, null); if (cursor != null && cursor.moveToFirst()) { final int index = cursor.getColumnIndexOrThrow(column); - return cursor.getString(index); + result = cursor.getString(index); } - } finally { + } + catch (Exception ex) { + ex.printStackTrace(); + return null; + } + finally { if (cursor != null) cursor.close(); } - return null; + return result; } diff --git a/ios/RNFetchBlob.xcodeproj/project.pbxproj b/ios/RNFetchBlob.xcodeproj/project.pbxproj index acd524413..070fcabb1 100644 --- a/ios/RNFetchBlob.xcodeproj/project.pbxproj +++ b/ios/RNFetchBlob.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 8C4801A6200CF71700FED7ED /* RNFetchBlobRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8C4801A5200CF71700FED7ED /* RNFetchBlobRequest.m */; }; A158F4271D052E49006FFD38 /* RNFetchBlobFS.m in Sources */ = {isa = PBXBuildFile; fileRef = A158F4261D052E49006FFD38 /* RNFetchBlobFS.m */; }; A158F42D1D0535BB006FFD38 /* RNFetchBlobConst.m in Sources */ = {isa = PBXBuildFile; fileRef = A158F42C1D0535BB006FFD38 /* RNFetchBlobConst.m */; }; A158F4301D0539DB006FFD38 /* RNFetchBlobNetwork.m in Sources */ = {isa = PBXBuildFile; fileRef = A158F42F1D0539DB006FFD38 /* RNFetchBlobNetwork.m */; }; @@ -29,6 +30,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 8C4801A4200CF71700FED7ED /* RNFetchBlobRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNFetchBlobRequest.h; sourceTree = ""; }; + 8C4801A5200CF71700FED7ED /* RNFetchBlobRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNFetchBlobRequest.m; sourceTree = ""; }; A158F4261D052E49006FFD38 /* RNFetchBlobFS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFetchBlobFS.m; sourceTree = ""; }; A158F4281D052E57006FFD38 /* RNFetchBlobFS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNFetchBlobFS.h; sourceTree = ""; }; A158F4291D0534A9006FFD38 /* RNFetchBlobConst.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNFetchBlobConst.h; sourceTree = ""; }; @@ -71,8 +74,10 @@ A1F950181D7E9134002A95A6 /* IOS7Polyfill.h */, A1AAE2981D300E4D0051D11C /* RNFetchBlobReqBuilder.m */, A1AAE2971D300E3E0051D11C /* RNFetchBlobReqBuilder.h */, - A158F42F1D0539DB006FFD38 /* RNFetchBlobNetwork.m */, A158F42E1D0539CE006FFD38 /* RNFetchBlobNetwork.h */, + A158F42F1D0539DB006FFD38 /* RNFetchBlobNetwork.m */, + 8C4801A4200CF71700FED7ED /* RNFetchBlobRequest.h */, + 8C4801A5200CF71700FED7ED /* RNFetchBlobRequest.m */, A158F42C1D0535BB006FFD38 /* RNFetchBlobConst.m */, A158F4291D0534A9006FFD38 /* RNFetchBlobConst.h */, A158F4281D052E57006FFD38 /* RNFetchBlobFS.h */, @@ -149,6 +154,7 @@ buildActionMask = 2147483647; files = ( A166D1AA1CE0647A00273590 /* RNFetchBlob.h in Sources */, + 8C4801A6200CF71700FED7ED /* RNFetchBlobRequest.m in Sources */, A158F42D1D0535BB006FFD38 /* RNFetchBlobConst.m in Sources */, A158F4271D052E49006FFD38 /* RNFetchBlobFS.m in Sources */, A158F4301D0539DB006FFD38 /* RNFetchBlobNetwork.m in Sources */, diff --git a/ios/RNFetchBlob/RNFetchBlob.h b/ios/RNFetchBlob/RNFetchBlob.h index e6385114f..669a093b5 100644 --- a/ios/RNFetchBlob/RNFetchBlob.h +++ b/ios/RNFetchBlob/RNFetchBlob.h @@ -39,7 +39,6 @@ @property (retain) UIDocumentInteractionController * documentController; + (RCTBridge *)getRCTBridge; -+ (void) checkExpiredSessions; @end diff --git a/ios/RNFetchBlob/RNFetchBlob.m b/ios/RNFetchBlob/RNFetchBlob.m index 246d6707c..8b92431e9 100644 --- a/ios/RNFetchBlob/RNFetchBlob.m +++ b/ios/RNFetchBlob/RNFetchBlob.m @@ -38,7 +38,7 @@ - (dispatch_queue_t) methodQueue { + (RCTBridge *)getRCTBridge { - RCTRootView * rootView = [[UIApplication sharedApplication] keyWindow].rootViewController.view; + RCTRootView * rootView = (RCTRootView*) [[UIApplication sharedApplication] keyWindow].rootViewController.view; return rootView.bridge; } @@ -96,8 +96,12 @@ - (NSDictionary *)constantsToExport // send HTTP request else { - RNFetchBlobNetwork * utils = [[RNFetchBlobNetwork alloc] init]; - [utils sendRequest:options contentLength:bodyLength bridge:self.bridge taskId:taskId withRequest:req callback:callback]; + [[RNFetchBlobNetwork sharedInstance] sendRequest:options + contentLength:bodyLength + bridge:self.bridge + taskId:taskId + withRequest:req + callback:callback]; } }]; @@ -128,8 +132,12 @@ - (NSDictionary *)constantsToExport // send HTTP request else { - __block RNFetchBlobNetwork * utils = [[RNFetchBlobNetwork alloc] init]; - [utils sendRequest:options contentLength:bodyLength bridge:self.bridge taskId:taskId withRequest:req callback:callback]; + [[RNFetchBlobNetwork sharedInstance] sendRequest:options + contentLength:bodyLength + bridge:self.bridge + taskId:taskId + withRequest:req + callback:callback]; } }]; } @@ -488,7 +496,7 @@ - (NSDictionary *)constantsToExport #pragma mark - net.cancelRequest RCT_EXPORT_METHOD(cancelRequest:(NSString *)taskId callback:(RCTResponseSenderBlock)callback) { - [RNFetchBlobNetwork cancelRequest:taskId]; + [[RNFetchBlobNetwork sharedInstance] cancelRequest:taskId]; callback(@[[NSNull null], taskId]); } @@ -498,14 +506,14 @@ - (NSDictionary *)constantsToExport { RNFetchBlobProgress * cfg = [[RNFetchBlobProgress alloc] initWithType:Download interval:interval count:count]; - [RNFetchBlobNetwork enableProgressReport:taskId config:cfg]; + [[RNFetchBlobNetwork sharedInstance] enableProgressReport:taskId config:cfg]; } #pragma mark - net.enableUploadProgressReport RCT_EXPORT_METHOD(enableUploadProgressReport:(NSString *)taskId interval:(nonnull NSNumber*)interval count:(nonnull NSNumber*)count) { RNFetchBlobProgress * cfg = [[RNFetchBlobProgress alloc] initWithType:Upload interval:interval count:count]; - [RNFetchBlobNetwork enableUploadProgress:taskId config:cfg]; + [[RNFetchBlobNetwork sharedInstance] enableUploadProgress:taskId config:cfg]; } #pragma mark - fs.slice diff --git a/ios/RNFetchBlobConst.m b/ios/RNFetchBlobConst.m index 6f7fef4b2..bc9b793a5 100644 --- a/ios/RNFetchBlobConst.m +++ b/ios/RNFetchBlobConst.m @@ -7,38 +7,38 @@ // #import "RNFetchBlobConst.h" -extern NSString *const FILE_PREFIX = @"RNFetchBlob-file://"; -extern NSString *const ASSET_PREFIX = @"bundle-assets://"; -extern NSString *const AL_PREFIX = @"assets-library://"; +NSString *const FILE_PREFIX = @"RNFetchBlob-file://"; +NSString *const ASSET_PREFIX = @"bundle-assets://"; +NSString *const AL_PREFIX = @"assets-library://"; // fetch configs -extern NSString *const CONFIG_USE_TEMP = @"fileCache"; -extern NSString *const CONFIG_FILE_PATH = @"path"; -extern NSString *const CONFIG_FILE_EXT = @"appendExt"; -extern NSString *const CONFIG_TRUSTY = @"trusty"; -extern NSString *const CONFIG_INDICATOR = @"indicator"; -extern NSString *const CONFIG_KEY = @"key"; -extern NSString *const CONFIG_EXTRA_BLOB_CTYPE = @"binaryContentTypes"; +NSString *const CONFIG_USE_TEMP = @"fileCache"; +NSString *const CONFIG_FILE_PATH = @"path"; +NSString *const CONFIG_FILE_EXT = @"appendExt"; +NSString *const CONFIG_TRUSTY = @"trusty"; +NSString *const CONFIG_INDICATOR = @"indicator"; +NSString *const CONFIG_KEY = @"key"; +NSString *const CONFIG_EXTRA_BLOB_CTYPE = @"binaryContentTypes"; -extern NSString *const EVENT_STATE_CHANGE = @"RNFetchBlobState"; -extern NSString *const EVENT_SERVER_PUSH = @"RNFetchBlobServerPush"; -extern NSString *const EVENT_PROGRESS = @"RNFetchBlobProgress"; -extern NSString *const EVENT_PROGRESS_UPLOAD = @"RNFetchBlobProgress-upload"; -extern NSString *const EVENT_EXPIRE = @"RNFetchBlobExpire"; +NSString *const EVENT_STATE_CHANGE = @"RNFetchBlobState"; +NSString *const EVENT_SERVER_PUSH = @"RNFetchBlobServerPush"; +NSString *const EVENT_PROGRESS = @"RNFetchBlobProgress"; +NSString *const EVENT_PROGRESS_UPLOAD = @"RNFetchBlobProgress-upload"; +NSString *const EVENT_EXPIRE = @"RNFetchBlobExpire"; -extern NSString *const MSG_EVENT = @"RNFetchBlobMessage"; -extern NSString *const MSG_EVENT_LOG = @"log"; -extern NSString *const MSG_EVENT_WARN = @"warn"; -extern NSString *const MSG_EVENT_ERROR = @"error"; -extern NSString *const FS_EVENT_DATA = @"data"; -extern NSString *const FS_EVENT_END = @"end"; -extern NSString *const FS_EVENT_WARN = @"warn"; -extern NSString *const FS_EVENT_ERROR = @"error"; +NSString *const MSG_EVENT = @"RNFetchBlobMessage"; +NSString *const MSG_EVENT_LOG = @"log"; +NSString *const MSG_EVENT_WARN = @"warn"; +NSString *const MSG_EVENT_ERROR = @"error"; +NSString *const FS_EVENT_DATA = @"data"; +NSString *const FS_EVENT_END = @"end"; +NSString *const FS_EVENT_WARN = @"warn"; +NSString *const FS_EVENT_ERROR = @"error"; -extern NSString *const KEY_REPORT_PROGRESS = @"reportProgress"; -extern NSString *const KEY_REPORT_UPLOAD_PROGRESS = @"reportUploadProgress"; +NSString *const KEY_REPORT_PROGRESS = @"reportProgress"; +NSString *const KEY_REPORT_UPLOAD_PROGRESS = @"reportUploadProgress"; // response type -extern NSString *const RESP_TYPE_BASE64 = @"base64"; -extern NSString *const RESP_TYPE_UTF8 = @"utf8"; -extern NSString *const RESP_TYPE_PATH = @"path"; +NSString *const RESP_TYPE_BASE64 = @"base64"; +NSString *const RESP_TYPE_UTF8 = @"utf8"; +NSString *const RESP_TYPE_PATH = @"path"; diff --git a/ios/RNFetchBlobFS.h b/ios/RNFetchBlobFS.h index 97386ef9d..c714fa66c 100644 --- a/ios/RNFetchBlobFS.h +++ b/ios/RNFetchBlobFS.h @@ -34,8 +34,8 @@ NSString * streamId; } -@property (nonatomic) NSOutputStream * outStream; -@property (nonatomic) NSInputStream * inStream; +@property (nonatomic) NSOutputStream * _Nullable outStream; +@property (nonatomic) NSInputStream * _Nullable inStream; @property (strong, nonatomic) RCTResponseSenderBlock callback; @property (nonatomic) RCTBridge * bridge; @property (nonatomic) NSString * encoding; diff --git a/ios/RNFetchBlobFS.m b/ios/RNFetchBlobFS.m index 9d4e00b0d..5e102d184 100644 --- a/ios/RNFetchBlobFS.m +++ b/ios/RNFetchBlobFS.m @@ -568,11 +568,11 @@ - (NSString *)openWithPath:(NSString *)destPath encode:(nullable NSString *)enco // Write file chunk into an opened stream - (void)writeEncodeChunk:(NSString *) chunk { - NSMutableData * decodedData = [NSData alloc]; + NSData * decodedData = nil; if([[self.encoding lowercaseString] isEqualToString:@"base64"]) { - decodedData = [[NSData alloc] initWithBase64EncodedData:chunk options:0]; - } - if([[self.encoding lowercaseString] isEqualToString:@"utf8"]) { + decodedData = [[NSData alloc] initWithBase64EncodedString:chunk options: NSDataBase64DecodingIgnoreUnknownCharacters]; + } + else if([[self.encoding lowercaseString] isEqualToString:@"utf8"]) { decodedData = [chunk dataUsingEncoding:NSUTF8StringEncoding]; } else if([[self.encoding lowercaseString] isEqualToString:@"ascii"]) { @@ -793,4 +793,4 @@ + (void) writeAssetToPath:(ALAssetRepresentation * )rep dest:(NSString *)dest return; } -@end +@end \ No newline at end of file diff --git a/ios/RNFetchBlobNetwork.h b/ios/RNFetchBlobNetwork.h index d3b4654a5..1512712af 100644 --- a/ios/RNFetchBlobNetwork.h +++ b/ios/RNFetchBlobNetwork.h @@ -6,9 +6,13 @@ // Copyright © 2016 wkh237. All rights reserved. // +#ifndef RNFetchBlobNetwork_h +#define RNFetchBlobNetwork_h + #import #import "RNFetchBlobProgress.h" #import "RNFetchBlobFS.h" +#import "RNFetchBlobRequest.h" #if __has_include() #import @@ -16,42 +20,26 @@ #import "RCTBridgeModule.h" #endif -#ifndef RNFetchBlobNetwork_h -#define RNFetchBlobNetwork_h - - - -typedef void(^CompletionHander)(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error); -typedef void(^DataTaskCompletionHander) (NSData * _Nullable resp, NSURLResponse * _Nullable response, NSError * _Nullable error); @interface RNFetchBlobNetwork : NSObject -@property (nullable, nonatomic) NSString * taskId; -@property (nonatomic) int expectedBytes; -@property (nonatomic) int receivedBytes; -@property (nonatomic) BOOL isServerPush; -@property (nullable, nonatomic) NSMutableData * respData; -@property (strong, nonatomic) RCTResponseSenderBlock callback; -@property (nullable, nonatomic) RCTBridge * bridge; -@property (nullable, nonatomic) NSDictionary * options; -@property (nullable, nonatomic) RNFetchBlobFS * fileStream; -@property (strong, nonatomic) CompletionHander fileTaskCompletionHandler; -@property (strong, nonatomic) DataTaskCompletionHander dataTaskCompletionHandler; -@property (nullable, nonatomic) NSError * error; - +@property(nonnull, nonatomic) NSOperationQueue *taskQueue; +@property(nonnull, nonatomic) NSMapTable * requestsTable; ++ (RNFetchBlobNetwork* _Nullable)sharedInstance; + (NSMutableDictionary * _Nullable ) normalizeHeaders:(NSDictionary * _Nullable)headers; -+ (void) cancelRequest:(NSString *)taskId; -+ (void) enableProgressReport:(NSString *) taskId; -+ (void) enableUploadProgress:(NSString *) taskId; + (void) emitExpiredTasks; - (nullable id) init; -- (void) sendRequest; -- (void) sendRequest:(NSDictionary * _Nullable )options contentLength:(long)contentLength bridge:(RCTBridge * _Nullable)bridgeRef taskId:(NSString * _Nullable)taskId withRequest:(NSURLRequest * _Nullable)req callback:(_Nullable RCTResponseSenderBlock) callback; -+ (void) enableProgressReport:(NSString *) taskId config:(RNFetchBlobProgress *)config; -+ (void) enableUploadProgress:(NSString *) taskId config:(RNFetchBlobProgress *)config; - +- (void) sendRequest:(NSDictionary * _Nullable )options + contentLength:(long)contentLength + bridge:(RCTBridge * _Nullable)bridgeRef + taskId:(NSString * _Nullable)taskId + withRequest:(NSURLRequest * _Nullable)req + callback:(_Nullable RCTResponseSenderBlock) callback; +- (void) cancelRequest:(NSString * _Nonnull)taskId; +- (void) enableProgressReport:(NSString * _Nonnull) taskId config:(RNFetchBlobProgress * _Nullable)config; +- (void) enableUploadProgress:(NSString * _Nonnull) taskId config:(RNFetchBlobProgress * _Nullable)config; @end diff --git a/ios/RNFetchBlobNetwork.m b/ios/RNFetchBlobNetwork.m index 7be57fc59..4531bf35e 100644 --- a/ios/RNFetchBlobNetwork.m +++ b/ios/RNFetchBlobNetwork.m @@ -8,13 +8,10 @@ #import -#import "RNFetchBlob.h" -#import "RNFetchBlobFS.h" #import "RNFetchBlobNetwork.h" + +#import "RNFetchBlob.h" #import "RNFetchBlobConst.h" -#import "RNFetchBlobReqBuilder.h" -#import "IOS7Polyfill.h" -#import #import "RNFetchBlobProgress.h" #if __has_include() @@ -35,126 +32,43 @@ // //////////////////////////////////////// -NSMapTable * taskTable; NSMapTable * expirationTable; -NSMutableDictionary * progressTable; -NSMutableDictionary * uploadProgressTable; __attribute__((constructor)) static void initialize_tables() { - if(expirationTable == nil) - { + if (expirationTable == nil) { expirationTable = [[NSMapTable alloc] init]; } - if(taskTable == nil) - { - taskTable = [[NSMapTable alloc] init]; - } - if(progressTable == nil) - { - progressTable = [[NSMutableDictionary alloc] init]; - } - if(uploadProgressTable == nil) - { - uploadProgressTable = [[NSMutableDictionary alloc] init]; - } } -typedef NS_ENUM(NSUInteger, ResponseFormat) { - UTF8, - BASE64, - AUTO -}; - - -@interface RNFetchBlobNetwork () -{ - BOOL * respFile; - BOOL isNewPart; - BOOL * isIncrement; - NSMutableData * partBuffer; - NSString * destPath; - NSOutputStream * writeStream; - long bodyLength; - NSMutableDictionary * respInfo; - NSInteger respStatus; - NSMutableArray * redirects; - ResponseFormat responseFormat; - BOOL * followRedirect; - BOOL backgroundTask; -} - -@end - @implementation RNFetchBlobNetwork -NSOperationQueue *taskQueue; -@synthesize taskId; -@synthesize expectedBytes; -@synthesize receivedBytes; -@synthesize respData; -@synthesize callback; -@synthesize bridge; -@synthesize options; -@synthesize fileTaskCompletionHandler; -@synthesize dataTaskCompletionHandler; -@synthesize error; - -// constructor - (id)init { self = [super init]; - if(taskQueue == nil) { - taskQueue = [[NSOperationQueue alloc] init]; - taskQueue.maxConcurrentOperationCount = 10; + if (self) { + self.requestsTable = [NSMapTable mapTableWithKeyOptions:NSMapTableStrongMemory valueOptions:NSMapTableWeakMemory]; + + self.taskQueue = [[NSOperationQueue alloc] init]; + self.taskQueue.qualityOfService = NSQualityOfServiceUtility; + self.taskQueue.maxConcurrentOperationCount = 10; } + return self; } -+ (void) enableProgressReport:(NSString *) taskId config:(RNFetchBlobProgress *)config -{ - if(progressTable == nil) - { - progressTable = [[NSMutableDictionary alloc] init]; - } - [progressTable setValue:config forKey:taskId]; -} - -+ (void) enableUploadProgress:(NSString *) taskId config:(RNFetchBlobProgress *)config -{ - if(uploadProgressTable == nil) - { - uploadProgressTable = [[NSMutableDictionary alloc] init]; - } - [uploadProgressTable setValue:config forKey:taskId]; -} - -// removing case from headers -+ (NSMutableDictionary *) normalizeHeaders:(NSDictionary *)headers -{ ++ (RNFetchBlobNetwork* _Nullable)sharedInstance { + static id _sharedInstance = nil; + static dispatch_once_t onceToken; - NSMutableDictionary * mheaders = [[NSMutableDictionary alloc]init]; - for(NSString * key in headers) { - [mheaders setValue:[headers valueForKey:key] forKey:[key lowercaseString]]; - } - - return mheaders; -} - -- (NSString *)md5:(NSString *)input { - const char* str = [input UTF8String]; - unsigned char result[CC_MD5_DIGEST_LENGTH]; - CC_MD5(str, (CC_LONG)strlen(str), result); - - NSMutableString *ret = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH*2]; - for(int i = 0; i 0) - { - defaultConfigObject.timeoutIntervalForRequest = timeout/1000; - } - defaultConfigObject.HTTPMaximumConnectionsPerHost = 10; - session = [NSURLSession sessionWithConfiguration:defaultConfigObject delegate:self delegateQueue:taskQueue]; - if(path != nil || [self.options valueForKey:CONFIG_USE_TEMP]!= nil) - { - respFile = YES; - - NSString* cacheKey = taskId; - if (key != nil) { - cacheKey = [self md5:key]; - if (cacheKey == nil) { - cacheKey = taskId; - } - - destPath = [RNFetchBlobFS getTempPath:cacheKey withExtension:[self.options valueForKey:CONFIG_FILE_EXT]]; - if ([[NSFileManager defaultManager] fileExistsAtPath:destPath]) { - callback(@[[NSNull null], RESP_TYPE_PATH, destPath]); - return; - } - } - - if(path != nil) - destPath = path; - else - destPath = [RNFetchBlobFS getTempPath:cacheKey withExtension:[self.options valueForKey:CONFIG_FILE_EXT]]; - } - else - { - respData = [[NSMutableData alloc] init]; - respFile = NO; - } - - __block NSURLSessionDataTask * task = [session dataTaskWithRequest:req]; - [taskTable setObject:task forKey:taskId]; - [task resume]; - - // network status indicator - if([[options objectForKey:CONFIG_INDICATOR] boolValue] == YES) - [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES]; - __block UIApplication * app = [UIApplication sharedApplication]; - -} - -// #115 Invoke fetch.expire event on those expired requests so that the expired event can be handled -+ (void) emitExpiredTasks -{ - NSEnumerator * emu = [expirationTable keyEnumerator]; - NSString * key; - - while((key = [emu nextObject])) - { - RCTBridge * bridge = [RNFetchBlob getRCTBridge]; - NSData * args = @{ @"taskId": key }; - [bridge.eventDispatcher sendDeviceEventWithName:EVENT_EXPIRE body:args]; - + @synchronized([RNFetchBlobNetwork class]) { + [self.requestsTable setObject:request forKey:taskId]; } - - // clear expired task entries - [expirationTable removeAllObjects]; - expirationTable = [[NSMapTable alloc] init]; - } -//////////////////////////////////////// -// -// NSURLSession delegates -// -//////////////////////////////////////// - - -#pragma mark NSURLSession delegate methods - - -#pragma mark - Received Response -// set expected content length on response received -- (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler +- (void) enableProgressReport:(NSString *) taskId config:(RNFetchBlobProgress *)config { - expectedBytes = [response expectedContentLength]; - - NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response; - NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode]; - NSString * respType = @""; - respStatus = statusCode; - if ([response respondsToSelector:@selector(allHeaderFields)]) - { - NSDictionary *headers = [httpResponse allHeaderFields]; - NSString * respCType = [[RNFetchBlobReqBuilder getHeaderIgnoreCases:@"Content-Type" fromHeaders:headers] lowercaseString]; - if(self.isServerPush == NO) - { - self.isServerPush = [[respCType lowercaseString] RNFBContainsString:@"multipart/x-mixed-replace;"]; - } - if(self.isServerPush) - { - if(partBuffer != nil) - { - [self.bridge.eventDispatcher - sendDeviceEventWithName:EVENT_SERVER_PUSH - body:@{ - @"taskId": taskId, - @"chunk": [partBuffer base64EncodedStringWithOptions:0], - } - ]; - } - partBuffer = [[NSMutableData alloc] init]; - completionHandler(NSURLSessionResponseAllow); - return; - } - if(respCType != nil) - { - NSArray * extraBlobCTypes = [options objectForKey:CONFIG_EXTRA_BLOB_CTYPE]; - if([respCType RNFBContainsString:@"text/"]) - { - respType = @"text"; - } - else if([respCType RNFBContainsString:@"application/json"]) - { - respType = @"json"; - } - // If extra blob content type is not empty, check if response type matches - else if( extraBlobCTypes != nil) { - for(NSString * substr in extraBlobCTypes) - { - if([respCType RNFBContainsString:[substr lowercaseString]]) - { - respType = @"blob"; - respFile = YES; - destPath = [RNFetchBlobFS getTempPath:taskId withExtension:nil]; - break; - } - } - } - else - { - respType = @"blob"; - // for XMLHttpRequest, switch response data handling strategy automatically - if([options valueForKey:@"auto"] == YES) { - respFile = YES; - destPath = [RNFetchBlobFS getTempPath:taskId withExtension:@""]; - } - } + if (config) { + @synchronized ([RNFetchBlobNetwork class]) { + [self.requestsTable objectForKey:taskId].progressConfig = config; } - else - respType = @"text"; - respInfo = @{ - @"taskId": taskId, - @"state": @"2", - @"headers": headers, - @"redirects": redirects, - @"respType" : respType, - @"timeout" : @NO, - @"status": [NSNumber numberWithInteger:statusCode] - }; - -#pragma mark - handling cookies - // # 153 get cookies - if(response.URL != nil) - { - NSHTTPCookieStorage * cookieStore = [NSHTTPCookieStorage sharedHTTPCookieStorage]; - NSArray * cookies = [NSHTTPCookie cookiesWithResponseHeaderFields: headers forURL:response.URL]; - if(cookies != nil && [cookies count] > 0) { - [cookieStore setCookies:cookies forURL:response.URL mainDocumentURL:nil]; - } - } - - [self.bridge.eventDispatcher - sendDeviceEventWithName: EVENT_STATE_CHANGE - body:respInfo - ]; - headers = nil; - respInfo = nil; - - } - else - NSLog(@"oops"); - - if(respFile == YES) - { - @try{ - NSFileManager * fm = [NSFileManager defaultManager]; - NSString * folder = [destPath stringByDeletingLastPathComponent]; - if(![fm fileExistsAtPath:folder]) - { - [fm createDirectoryAtPath:folder withIntermediateDirectories:YES attributes:NULL error:nil]; - } - BOOL overwrite = [options valueForKey:@"overwrite"] == nil ? YES : [[options valueForKey:@"overwrite"] boolValue]; - BOOL appendToExistingFile = [destPath RNFBContainsString:@"?append=true"]; - - appendToExistingFile = !overwrite; - - // For solving #141 append response data if the file already exists - // base on PR#139 @kejinliang - if(appendToExistingFile) - { - destPath = [destPath stringByReplacingOccurrencesOfString:@"?append=true" withString:@""]; - } - if (![fm fileExistsAtPath:destPath]) - { - [fm createFileAtPath:destPath contents:[[NSData alloc] init] attributes:nil]; - } - writeStream = [[NSOutputStream alloc] initToFileAtPath:destPath append:appendToExistingFile]; - [writeStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - [writeStream open]; - } - @catch(NSException * ex) - { - NSLog(@"write file error"); - } - } - - completionHandler(NSURLSessionResponseAllow); -} - - -// download progress handler -- (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data -{ - // For #143 handling multipart/x-mixed-replace response - if(self.isServerPush) - { - [partBuffer appendData:data]; - return ; - } - - NSNumber * received = [NSNumber numberWithLong:[data length]]; - receivedBytes += [received longValue]; - NSString * chunkString = @""; - - if(isIncrement == YES) - { - chunkString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - } - - if(respFile == NO) - { - [respData appendData:data]; - } - else - { - [writeStream write:[data bytes] maxLength:[data length]]; } - RNFetchBlobProgress * pconfig = [progressTable valueForKey:taskId]; - if(expectedBytes == 0) - return; - NSNumber * now =[NSNumber numberWithFloat:((float)receivedBytes/(float)expectedBytes)]; - if(pconfig != nil && [pconfig shouldReport:now]) - { - [self.bridge.eventDispatcher - sendDeviceEventWithName:EVENT_PROGRESS - body:@{ - @"taskId": taskId, - @"written": [NSString stringWithFormat:@"%d", receivedBytes], - @"total": [NSString stringWithFormat:@"%d", expectedBytes], - @"chunk": chunkString - } - ]; - } - received = nil; - } -- (void) URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error +- (void) enableUploadProgress:(NSString *) taskId config:(RNFetchBlobProgress *)config { - if([session isEqual:session]) - session = nil; -} - - -- (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error -{ - - self.error = error; - NSString * errMsg = [NSNull null]; - NSString * respStr = [NSNull null]; - NSString * rnfbRespType = @""; - - [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; - - if(respInfo == nil) - { - respInfo = [NSNull null]; - } - - if(error != nil) - { - errMsg = [error localizedDescription]; - } - - if(respFile == YES) - { - [writeStream close]; - rnfbRespType = RESP_TYPE_PATH; - respStr = destPath; - } - // base64 response - else { - // #73 fix unicode data encoding issue : - // when response type is BASE64, we should first try to encode the response data to UTF8 format - // if it turns out not to be `nil` that means the response data contains valid UTF8 string, - // in order to properly encode the UTF8 string, use URL encoding before BASE64 encoding. - NSString * utf8 = [[NSString alloc] initWithData:respData encoding:NSUTF8StringEncoding]; - - if(responseFormat == BASE64) - { - rnfbRespType = RESP_TYPE_BASE64; - respStr = [respData base64EncodedStringWithOptions:0]; - } - else if (responseFormat == UTF8) - { - rnfbRespType = RESP_TYPE_UTF8; - respStr = utf8; - } - else - { - if(utf8 != nil) - { - rnfbRespType = RESP_TYPE_UTF8; - respStr = utf8; - } - else - { - rnfbRespType = RESP_TYPE_BASE64; - respStr = [respData base64EncodedStringWithOptions:0]; - } + if (config) { + @synchronized ([RNFetchBlobNetwork class]) { + [self.requestsTable objectForKey:taskId].uploadProgressConfig = config; } } - - - callback(@[ errMsg, rnfbRespType, respStr]); - - @synchronized(taskTable, uploadProgressTable, progressTable) - { - if([taskTable objectForKey:taskId] == nil) - NSLog(@"object released by ARC."); - else - [taskTable removeObjectForKey:taskId]; - [uploadProgressTable removeObjectForKey:taskId]; - [progressTable removeObjectForKey:taskId]; - } - - respData = nil; - receivedBytes = 0; - [session finishTasksAndInvalidate]; - } -// upload progress handler -- (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesWritten totalBytesExpectedToSend:(int64_t)totalBytesExpectedToWrite +- (void) cancelRequest:(NSString *)taskId { - RNFetchBlobProgress * pconfig = [uploadProgressTable valueForKey:taskId]; - if(totalBytesExpectedToWrite == 0) - return; - NSNumber * now = [NSNumber numberWithFloat:((float)totalBytesWritten/(float)totalBytesExpectedToWrite)]; - if(pconfig != nil && [pconfig shouldReport:now]) { - [self.bridge.eventDispatcher - sendDeviceEventWithName:EVENT_PROGRESS_UPLOAD - body:@{ - @"taskId": taskId, - @"written": [NSString stringWithFormat:@"%d", totalBytesWritten], - @"total": [NSString stringWithFormat:@"%d", totalBytesExpectedToWrite] - } - ]; + NSURLSessionDataTask * task; + + @synchronized ([RNFetchBlobNetwork class]) { + task = [self.requestsTable objectForKey:taskId].task; } -} - -+ (void) cancelRequest:(NSString *)taskId -{ - NSURLSessionDataTask * task = [taskTable objectForKey:taskId]; - if(task != nil && task.state == NSURLSessionTaskStateRunning) + + if (task && task.state == NSURLSessionTaskStateRunning) { [task cancel]; -} - - -- (void) URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable credantial))completionHandler -{ - BOOL trusty = [options valueForKey:CONFIG_TRUSTY]; - if(!trusty) - { - completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); - } - else - { - completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); } } - -- (void) URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session +// removing case from headers ++ (NSMutableDictionary *) normalizeHeaders:(NSDictionary *)headers { - NSLog(@"sess done in background"); + NSMutableDictionary * mheaders = [[NSMutableDictionary alloc]init]; + for (NSString * key in headers) { + [mheaders setValue:[headers valueForKey:key] forKey:[key lowercaseString]]; + } + + return mheaders; } -- (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler +// #115 Invoke fetch.expire event on those expired requests so that the expired event can be handled ++ (void) emitExpiredTasks { - - if(followRedirect) - { - if(request.URL != nil) - [redirects addObject:[request.URL absoluteString]]; - completionHandler(request); - } - else - { - completionHandler(nil); + @synchronized ([RNFetchBlobNetwork class]){ + NSEnumerator * emu = [expirationTable keyEnumerator]; + NSString * key; + + while ((key = [emu nextObject])) + { + RCTBridge * bridge = [RNFetchBlob getRCTBridge]; + id args = @{ @"taskId": key }; + [bridge.eventDispatcher sendDeviceEventWithName:EVENT_EXPIRE body:args]; + + } + + // clear expired task entries + [expirationTable removeAllObjects]; + expirationTable = [[NSMapTable alloc] init]; } } diff --git a/ios/RNFetchBlobReqBuilder.h b/ios/RNFetchBlobReqBuilder.h index e7abeb9c7..1edc3ff50 100644 --- a/ios/RNFetchBlobReqBuilder.h +++ b/ios/RNFetchBlobReqBuilder.h @@ -29,7 +29,7 @@ body:(NSString *)body onComplete:(void(^)(NSURLRequest * req, long bodyLength))onComplete; -+(NSString *) getHeaderIgnoreCases:(NSString *)field fromHeaders:(NSMutableArray *) headers; ++(NSString *) getHeaderIgnoreCases:(NSString *)field fromHeaders:(NSDictionary *) headers; @end diff --git a/ios/RNFetchBlobReqBuilder.m b/ios/RNFetchBlobReqBuilder.m index 15465a1ae..e9eced6c4 100644 --- a/ios/RNFetchBlobReqBuilder.m +++ b/ios/RNFetchBlobReqBuilder.m @@ -277,7 +277,7 @@ void __block (^getFieldData)(id field) = ^(id field) } } -+(NSString *) getHeaderIgnoreCases:(NSString *)field fromHeaders:(NSMutableDictionary *) headers { ++(NSString *) getHeaderIgnoreCases:(NSString *)field fromHeaders:(NSDictionary *) headers { NSString * normalCase = [headers valueForKey:field]; NSString * ignoredCase = [headers valueForKey:[field lowercaseString]]; diff --git a/ios/RNFetchBlobRequest.h b/ios/RNFetchBlobRequest.h new file mode 100644 index 000000000..b550ac22e --- /dev/null +++ b/ios/RNFetchBlobRequest.h @@ -0,0 +1,47 @@ +// +// RNFetchBlobRequest.h +// RNFetchBlob +// +// Created by Artur Chrusciel on 15.01.18. +// Copyright © 2018 wkh237.github.io. All rights reserved. +// + +#ifndef RNFetchBlobRequest_h +#define RNFetchBlobRequest_h + +#import + +#import "RNFetchBlobProgress.h" + +#if __has_include() +#import +#else +#import "RCTBridgeModule.h" +#endif + +@interface RNFetchBlobRequest : NSObject + +@property (nullable, nonatomic) NSString * taskId; +@property (nonatomic) long long expectedBytes; +@property (nonatomic) long long receivedBytes; +@property (nonatomic) BOOL isServerPush; +@property (nullable, nonatomic) NSMutableData * respData; +@property (nullable, strong, nonatomic) RCTResponseSenderBlock callback; +@property (nullable, nonatomic) RCTBridge * bridge; +@property (nullable, nonatomic) NSDictionary * options; +@property (nullable, nonatomic) NSError * error; +@property (nullable, nonatomic) RNFetchBlobProgress *progressConfig; +@property (nullable, nonatomic) RNFetchBlobProgress *uploadProgressConfig; +@property (nullable, nonatomic, weak) NSURLSessionDataTask *task; + +- (void) sendRequest:(NSDictionary * _Nullable )options + contentLength:(long)contentLength + bridge:(RCTBridge * _Nullable)bridgeRef + taskId:(NSString * _Nullable)taskId + withRequest:(NSURLRequest * _Nullable)req + taskOperationQueue:(NSOperationQueue * _Nonnull)operationQueue + callback:(_Nullable RCTResponseSenderBlock) callback; + +@end + +#endif /* RNFetchBlobRequest_h */ diff --git a/ios/RNFetchBlobRequest.m b/ios/RNFetchBlobRequest.m new file mode 100644 index 000000000..10b5e660f --- /dev/null +++ b/ios/RNFetchBlobRequest.m @@ -0,0 +1,469 @@ +// +// RNFetchBlobRequest.m +// RNFetchBlob +// +// Created by Artur Chrusciel on 15.01.18. +// Copyright © 2018 wkh237.github.io. All rights reserved. +// + +#import "RNFetchBlobRequest.h" + +#import "RNFetchBlobFS.h" +#import "RNFetchBlobConst.h" +#import "RNFetchBlobReqBuilder.h" + +#import "IOS7Polyfill.h" +#import + + +typedef NS_ENUM(NSUInteger, ResponseFormat) { + UTF8, + BASE64, + AUTO +}; + +@interface RNFetchBlobRequest () +{ + BOOL respFile; + BOOL isNewPart; + BOOL isIncrement; + NSMutableData * partBuffer; + NSString * destPath; + NSOutputStream * writeStream; + long bodyLength; + NSInteger respStatus; + NSMutableArray * redirects; + ResponseFormat responseFormat; + BOOL followRedirect; + BOOL backgroundTask; +} + +@end + +@implementation RNFetchBlobRequest + +@synthesize taskId; +@synthesize expectedBytes; +@synthesize receivedBytes; +@synthesize respData; +@synthesize callback; +@synthesize bridge; +@synthesize options; +@synthesize error; + + +- (NSString *)md5:(NSString *)input { + const char* str = [input UTF8String]; + unsigned char result[CC_MD5_DIGEST_LENGTH]; + CC_MD5(str, (CC_LONG)strlen(str), result); + + NSMutableString *ret = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH*2]; + for (int i = 0; i 0) { + defaultConfigObject.timeoutIntervalForRequest = timeout/1000; + } + + defaultConfigObject.HTTPMaximumConnectionsPerHost = 10; + session = [NSURLSession sessionWithConfiguration:defaultConfigObject delegate:self delegateQueue:operationQueue]; + + if (path || [self.options valueForKey:CONFIG_USE_TEMP]) { + respFile = YES; + + NSString* cacheKey = taskId; + if (key) { + cacheKey = [self md5:key]; + + if (!cacheKey) { + cacheKey = taskId; + } + + destPath = [RNFetchBlobFS getTempPath:cacheKey withExtension:[self.options valueForKey:CONFIG_FILE_EXT]]; + + if ([[NSFileManager defaultManager] fileExistsAtPath:destPath]) { + callback(@[[NSNull null], RESP_TYPE_PATH, destPath]); + + return; + } + } + + if (path) { + destPath = path; + } else { + destPath = [RNFetchBlobFS getTempPath:cacheKey withExtension:[self.options valueForKey:CONFIG_FILE_EXT]]; + } + } else { + respData = [[NSMutableData alloc] init]; + respFile = NO; + } + + self.task = [session dataTaskWithRequest:req]; + [self.task resume]; + + // network status indicator + if ([[options objectForKey:CONFIG_INDICATOR] boolValue]) { + [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES]; + } +} + +//////////////////////////////////////// +// +// NSURLSession delegates +// +//////////////////////////////////////// + + +#pragma mark NSURLSession delegate methods + + +#pragma mark - Received Response +// set expected content length on response received +- (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler +{ + expectedBytes = [response expectedContentLength]; + + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response; + NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode]; + NSString * respType = @""; + respStatus = statusCode; + + if ([response respondsToSelector:@selector(allHeaderFields)]) + { + NSDictionary *headers = [httpResponse allHeaderFields]; + NSString * respCType = [[RNFetchBlobReqBuilder getHeaderIgnoreCases:@"Content-Type" fromHeaders:headers] lowercaseString]; + + if (self.isServerPush) { + if (partBuffer) { + [self.bridge.eventDispatcher + sendDeviceEventWithName:EVENT_SERVER_PUSH + body:@{ + @"taskId": taskId, + @"chunk": [partBuffer base64EncodedStringWithOptions:0], + } + ]; + } + + partBuffer = [[NSMutableData alloc] init]; + completionHandler(NSURLSessionResponseAllow); + + return; + } else { + self.isServerPush = [[respCType lowercaseString] RNFBContainsString:@"multipart/x-mixed-replace;"]; + } + + if(respCType) + { + NSArray * extraBlobCTypes = [options objectForKey:CONFIG_EXTRA_BLOB_CTYPE]; + + if ([respCType RNFBContainsString:@"text/"]) { + respType = @"text"; + } else if ([respCType RNFBContainsString:@"application/json"]) { + respType = @"json"; + } else if(extraBlobCTypes) { // If extra blob content type is not empty, check if response type matches + for (NSString * substr in extraBlobCTypes) { + if ([respCType RNFBContainsString:[substr lowercaseString]]) { + respType = @"blob"; + respFile = YES; + destPath = [RNFetchBlobFS getTempPath:taskId withExtension:nil]; + break; + } + } + } else { + respType = @"blob"; + + // for XMLHttpRequest, switch response data handling strategy automatically + if ([options valueForKey:@"auto"]) { + respFile = YES; + destPath = [RNFetchBlobFS getTempPath:taskId withExtension:@""]; + } + } + } else { + respType = @"text"; + } + +#pragma mark - handling cookies + // # 153 get cookies + if (response.URL) { + NSHTTPCookieStorage * cookieStore = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + NSArray * cookies = [NSHTTPCookie cookiesWithResponseHeaderFields: headers forURL:response.URL]; + if (cookies.count) { + [cookieStore setCookies:cookies forURL:response.URL mainDocumentURL:nil]; + } + } + + [self.bridge.eventDispatcher + sendDeviceEventWithName: EVENT_STATE_CHANGE + body:@{ + @"taskId": taskId, + @"state": @"2", + @"headers": headers, + @"redirects": redirects, + @"respType" : respType, + @"timeout" : @NO, + @"status": [NSNumber numberWithInteger:statusCode] + } + ]; + } else { + NSLog(@"oops"); + } + + if (respFile) + { + @try{ + NSFileManager * fm = [NSFileManager defaultManager]; + NSString * folder = [destPath stringByDeletingLastPathComponent]; + + if (![fm fileExistsAtPath:folder]) { + [fm createDirectoryAtPath:folder withIntermediateDirectories:YES attributes:NULL error:nil]; + } + + // if not set overwrite in options, defaults to TRUE + BOOL overwrite = [options valueForKey:@"overwrite"] == nil ? YES : [[options valueForKey:@"overwrite"] boolValue]; + BOOL appendToExistingFile = [destPath RNFBContainsString:@"?append=true"]; + + appendToExistingFile = !overwrite; + + // For solving #141 append response data if the file already exists + // base on PR#139 @kejinliang + if (appendToExistingFile) { + destPath = [destPath stringByReplacingOccurrencesOfString:@"?append=true" withString:@""]; + } + + if (![fm fileExistsAtPath:destPath]) { + [fm createFileAtPath:destPath contents:[[NSData alloc] init] attributes:nil]; + } + + writeStream = [[NSOutputStream alloc] initToFileAtPath:destPath append:appendToExistingFile]; + [writeStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; + [writeStream open]; + } + @catch(NSException * ex) + { + NSLog(@"write file error"); + } + } + + completionHandler(NSURLSessionResponseAllow); +} + + +// download progress handler +- (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data +{ + // For #143 handling multipart/x-mixed-replace response + if (self.isServerPush) + { + [partBuffer appendData:data]; + + return ; + } + + NSNumber * received = [NSNumber numberWithLong:[data length]]; + receivedBytes += [received longValue]; + NSString * chunkString = @""; + + if (isIncrement) { + chunkString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + } + + if (respFile) { + [writeStream write:[data bytes] maxLength:[data length]]; + } else { + [respData appendData:data]; + } + + if (expectedBytes == 0) { + return; + } + + NSNumber * now =[NSNumber numberWithFloat:((float)receivedBytes/(float)expectedBytes)]; + + if ([self.progressConfig shouldReport:now]) { + [self.bridge.eventDispatcher + sendDeviceEventWithName:EVENT_PROGRESS + body:@{ + @"taskId": taskId, + @"written": [NSString stringWithFormat:@"%ld", (long) receivedBytes], + @"total": [NSString stringWithFormat:@"%ld", (long) expectedBytes], + @"chunk": chunkString + } + ]; + } +} + +- (void) URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error +{ + if ([session isEqual:session]) { + session = nil; + } +} + + +- (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error +{ + + self.error = error; + NSString * errMsg; + NSString * respStr; + NSString * rnfbRespType; + + [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; + + if (error) { + errMsg = [error localizedDescription]; + } + + if (respFile) { + [writeStream close]; + rnfbRespType = RESP_TYPE_PATH; + respStr = destPath; + } else { // base64 response + // #73 fix unicode data encoding issue : + // when response type is BASE64, we should first try to encode the response data to UTF8 format + // if it turns out not to be `nil` that means the response data contains valid UTF8 string, + // in order to properly encode the UTF8 string, use URL encoding before BASE64 encoding. + NSString * utf8 = [[NSString alloc] initWithData:respData encoding:NSUTF8StringEncoding]; + + if (responseFormat == BASE64) { + rnfbRespType = RESP_TYPE_BASE64; + respStr = [respData base64EncodedStringWithOptions:0]; + } else if (responseFormat == UTF8) { + rnfbRespType = RESP_TYPE_UTF8; + respStr = utf8; + } else { + if (utf8) { + rnfbRespType = RESP_TYPE_UTF8; + respStr = utf8; + } else { + rnfbRespType = RESP_TYPE_BASE64; + respStr = [respData base64EncodedStringWithOptions:0]; + } + } + } + + + callback(@[ + errMsg ?: [NSNull null], + rnfbRespType ?: @"", + respStr ?: [NSNull null] + ]); + + respData = nil; + receivedBytes = 0; + [session finishTasksAndInvalidate]; + +} + +// upload progress handler +- (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesWritten totalBytesExpectedToSend:(int64_t)totalBytesExpectedToWrite +{ + if (totalBytesExpectedToWrite == 0) { + return; + } + + NSNumber * now = [NSNumber numberWithFloat:((float)totalBytesWritten/(float)totalBytesExpectedToWrite)]; + + if ([self.uploadProgressConfig shouldReport:now]) { + [self.bridge.eventDispatcher + sendDeviceEventWithName:EVENT_PROGRESS_UPLOAD + body:@{ + @"taskId": taskId, + @"written": [NSString stringWithFormat:@"%ld", (long) totalBytesWritten], + @"total": [NSString stringWithFormat:@"%ld", (long) totalBytesExpectedToWrite] + } + ]; + } +} + + +- (void) URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable credantial))completionHandler +{ + if ([[options valueForKey:CONFIG_TRUSTY] boolValue]) { + completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); + } else { + completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); + } +} + + +- (void) URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session +{ + NSLog(@"sess done in background"); +} + +- (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler +{ + + if (followRedirect) { + if (request.URL) { + [redirects addObject:[request.URL absoluteString]]; + } + + completionHandler(request); + } else { + completionHandler(nil); + } +} + + +@end diff --git a/package.json b/package.json index a4524df60..a93dba81d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-fetch-blob", - "version": "0.10.6", + "version": "0.10.8", "description": "A module provides upload, download, and files access API. Supports file stream read/write for process large files.", "main": "index.js", "scripts": { @@ -8,7 +8,7 @@ }, "dependencies": { "base-64": "0.1.0", - "glob": "^7.0.6" + "glob": "7.0.6" }, "keywords": [ "react-native", @@ -35,4 +35,4 @@ "Ben ", "" ] -} \ No newline at end of file +} diff --git a/polyfill/Blob.js b/polyfill/Blob.js index 384ae8fd9..53662a798 100644 --- a/polyfill/Blob.js +++ b/polyfill/Blob.js @@ -130,6 +130,8 @@ export default class Blob extends EventTarget { // Blob data from file path else if(typeof data === 'string' && data.startsWith('RNFetchBlob-file://')) { log.verbose('create Blob cache file from file path', data) + // set this flag so that we know this blob is a wrapper of an existing file + this._isReference = true this._ref = String(data).replace('RNFetchBlob-file://', '') let orgPath = this._ref if(defer) @@ -282,6 +284,20 @@ export default class Blob extends EventTarget { }) } + safeClose() { + if(this._closed) + return Promise.reject('Blob has been released.') + this._closed = true + if(!this._isReference) { + return fs.unlink(this._ref).catch((err) => { + console.warn(err) + }) + } + else { + return Promise.resolve() + } + } + _invokeOnCreateEvent() { log.verbose('invoke create event', this._onCreated) this._blobCreated = true diff --git a/polyfill/XMLHttpRequest.js b/polyfill/XMLHttpRequest.js index 89171921f..42c987704 100644 --- a/polyfill/XMLHttpRequest.js +++ b/polyfill/XMLHttpRequest.js @@ -277,7 +277,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget{ _headerReceived = (e) => { log.debug('header received ', this._task.taskId, e) this.responseURL = this._url - if(e.state === "2") { + if(e.state === "2" && e.taskId === this._task.taskId) { this._responseHeaders = e.headers this._statusText = e.status this._status = Math.floor(e.status)