diff --git a/README.md b/README.md index f2d547667..b5ca84c45 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ rn-fetch-blob version 0.10.16 is only compatible with react native 0.60 and up. ## About -This project was started in the cause of solving issue [facebook/react-native#854](https://github.com/facebook/react-native/issues/854), React Native's lacks of `Blob` implementation which results into problems when transferring binary data. +This project was started in the cause of solving issue [facebook/react-native#854](https://github.com/facebook/react-native/issues/854), React Native's lacks of `Blob` implementation which results into problems when transferring binary data. It is committed to making file access and transfer easier and more efficient for React Native developers. We've implemented highly customizable filesystem and network module which plays well together. For example, developers can upload and download data directly from/to storage, which is more efficient, especially for large files. The file system supports file stream, so you don't have to worry about OOM problem when accessing large files. @@ -116,8 +116,8 @@ If you're going to access external storage (say, SD card storage) for `Android 5 -+ -+ ++ ++ + ... @@ -129,10 +129,18 @@ Also, if you're going to use `Android Download Manager` you have to add this to -+ ++ ``` +If you are going to use the `wifiOnly` flag, you need to add this to `AndroidManifest.xml` + +```diff ++ + ... + +``` + **Grant Access Permission for Android 6.0** Beginning in Android 6.0 (API level 23), users grant permissions to apps while the app is running, not when they install the app. So adding permissions in `AndroidManifest.xml` won't work for Android 6.0+ devices. To grant permissions in runtime, you might use [PermissionAndroid API](https://facebook.github.io/react-native/docs/permissionsandroid.html). @@ -168,7 +176,7 @@ To sum up: - To send a form data, the `Content-Type` header does not matter. When the body is an `Array` we will set proper content type for you. - To send binary data, you have two choices, use BASE64 encoded string or path points to a file contains the body. - - If the `Content-Type` containing substring`;BASE64` or `application/octet` the given body will be considered as a BASE64 encoded data which will be decoded to binary data as the request body. + - If the `Content-Type` containing substring`;BASE64` or `application/octet` the given body will be considered as a BASE64 encoded data which will be decoded to binary data as the request body. - Otherwise, if a string starts with `RNFetchBlob-file://` (which can simply be done by `RNFetchBlob.wrap(PATH_TO_THE_FILE)`), it will try to find the data from the URI string after `RNFetchBlob-file://` and use it as the request body. - To send the body as-is, simply use a `Content-Type` header not containing `;BASE64` or `application/octet`. @@ -189,7 +197,7 @@ RNFetchBlob.fetch('GET', 'http://www.example.com/images/img1.png', { }) .then((res) => { let status = res.info().status; - + if(status == 200) { // the conversion is done in native code let base64Str = res.base64() @@ -290,7 +298,7 @@ RNFetchBlob.fetch('POST', 'https://content.dropboxapi.com/2/files/upload', { 'Content-Type' : 'application/octet-stream', // here's the body you're going to send, should be a BASE64 encoded string // (you can use "base64"(refer to the library 'mathiasbynens/base64') APIs to make one). - // The data will be converted to "byte array"(say, blob) before request sent. + // The data will be converted to "byte array"(say, blob) before request sent. }, base64ImageString) .then((res) => { console.log(res.text()) @@ -648,7 +656,7 @@ RNFetchBlob.fs.readStream( ifstream.onError((err) => { console.log('oops', err) }) - ifstream.onEnd(() => { + ifstream.onEnd(() => { { // set session of a response res.session('foo') - }) + }) RNFetchblob.config({ // you can also set session beforehand @@ -759,7 +767,7 @@ You can also group requests by using `session` API and use `dispose` to remove t .fetch('GET', 'http://example.com/download/file') .then((res) => { // ... - }) + }) // or put an existing file path to the session RNFetchBlob.session('foo').add('some-file-path') @@ -794,6 +802,22 @@ RNFetchBlob.config({ }) ``` +### WiFi only requests + +If you wish to only route requests through the Wifi interface, set the below configuration. +Note: On Android, the `ACCESS_NETWORK_STATE` permission must be set, and this flag will only work +on API version 21 (Lollipop, Android 5.0) or above. APIs below 21 will ignore this flag. + +```js +RNFetchBlob.config({ + wifiOnly : true +}) +.fetch('GET', 'https://mysite.com') +.then((resp) => { + // ... +}) +``` + ## Web API Polyfills After `0.8.0` we've made some [Web API polyfills](https://github.com/joltup/rn-fetch-blob/wiki/Web-API-Polyfills-(experimental)) that makes some browser-based library available in RN. diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java index a5c68b689..8ac9e7a85 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java +++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java @@ -10,6 +10,7 @@ class RNFetchBlobConfig { public String appendExt; public ReadableMap addAndroidDownloads; public Boolean trusty; + public Boolean wifiOnly = false; public String key; public String mime; public Boolean auto; @@ -26,6 +27,7 @@ class RNFetchBlobConfig { this.path = options.hasKey("path") ? options.getString("path") : null; this.appendExt = options.hasKey("appendExt") ? options.getString("appendExt") : ""; this.trusty = options.hasKey("trusty") ? options.getBoolean("trusty") : false; + this.wifiOnly = options.hasKey("wifiOnly") ? options.getBoolean("wifiOnly") : false; if(options.hasKey("addAndroidDownloads")) { this.addAndroidDownloads = options.getMap("addAndroidDownloads"); } diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java index 309679907..a8abd7183 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java +++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java @@ -9,6 +9,10 @@ import android.net.Uri; import android.os.Build; import androidx.annotation.NonNull; +import android.net.Network; +import android.net.NetworkInfo; +import android.net.NetworkCapabilities; +import android.net.ConnectivityManager; import android.util.Base64; import com.RNFetchBlob.Response.RNFetchBlobDefaultResp; @@ -36,6 +40,7 @@ import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.URL; +import java.net.Proxy; import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; @@ -231,6 +236,49 @@ else if(this.options.fileCache) clientBuilder = client.newBuilder(); } + // wifi only, need ACCESS_NETWORK_STATE permission + // and API level >= 21 + if(this.options.wifiOnly){ + + boolean found = false; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ConnectivityManager connectivityManager = (ConnectivityManager) RNFetchBlob.RCTContext.getSystemService(RNFetchBlob.RCTContext.CONNECTIVITY_SERVICE); + Network[] networks = connectivityManager.getAllNetworks(); + + for (Network network : networks) { + + NetworkInfo netInfo = connectivityManager.getNetworkInfo(network); + NetworkCapabilities caps = connectivityManager.getNetworkCapabilities(network); + + if(caps == null || netInfo == null){ + continue; + } + + if(!netInfo.isConnected()){ + continue; + } + + if(caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)){ + clientBuilder.proxy(Proxy.NO_PROXY); + clientBuilder.socketFactory(network.getSocketFactory()); + found = true; + break; + + } + } + + if(!found){ + callback.invoke("No available WiFi connections.", null, null); + releaseTaskResource(); + return; + } + } + else{ + RNFetchBlobUtils.emitWarningEvent("RNFetchBlob: wifiOnly was set, but SDK < 21. wifiOnly was ignored."); + } + } + final Request.Builder builder = new Request.Builder(); try { builder.url(new URL(url)); @@ -378,7 +426,7 @@ public Response intercept(@NonNull Chain chain) throws IOException { } catch (SocketTimeoutException e ){ timeout = true; - RNFetchBlobUtils.emitWarningEvent("RNFetchBlob error when sending request : " + e.getLocalizedMessage()); + //RNFetchBlobUtils.emitWarningEvent("RNFetchBlob error when sending request : " + e.getLocalizedMessage()); } catch(Exception ex) { } @@ -414,7 +462,7 @@ public void onFailure(@NonNull Call call, IOException e) { // check if this error caused by socket timeout if(e.getClass().equals(SocketTimeoutException.class)) { respInfo.putBoolean("timeout", true); - callback.invoke("request timed out.", null, null); + callback.invoke("The request timed out.", null, null); } else callback.invoke(e.getLocalizedMessage(), null, null); @@ -545,13 +593,14 @@ private void done(Response resp) { RNFetchBlobFileResp rnFetchBlobFileResp = (RNFetchBlobFileResp) responseBody; - if(rnFetchBlobFileResp != null && rnFetchBlobFileResp.isDownloadComplete() == false){ - callback.invoke("RNFetchBlob failed. Download interrupted.", null); + if(rnFetchBlobFileResp != null && !rnFetchBlobFileResp.isDownloadComplete()){ + callback.invoke("Download interrupted.", null); } else { this.destPath = this.destPath.replace("?append=true", ""); callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_PATH, this.destPath); } + break; default: try { @@ -685,7 +734,7 @@ public void onReceive(Context context, Intent intent) { } String filePath = null; - try { + try { // the file exists in media content database if (c.moveToFirst()) { // #297 handle failed request diff --git a/index.d.ts b/index.d.ts index 540a99c81..972f8e609 100644 --- a/index.d.ts +++ b/index.d.ts @@ -546,6 +546,16 @@ export interface RNFetchBlobConfig { */ trusty?: boolean; + /** + * Set this property to true will only do requests through the WiFi interface, and fail otherwise. + */ + wifiOnly?: boolean; + + /** + * Set this property so redirects are not automatically followed. + */ + followRedirect?: boolean; + /** * Set this property to true will makes response data of the fetch stored in a temp file, by default the temp * file will stored in App's own root folder with file name template RNFetchBlob_tmp${timestamp}. diff --git a/index.js b/index.js index 9e46bc24e..8ab5e9685 100644 --- a/index.js +++ b/index.js @@ -105,6 +105,12 @@ function wrap(path:string):string { * If it doesn't exist, the file is downloaded as usual * @property {number} timeout * Request timeout in millionseconds, by default it's 60000ms. + * @property {boolean} followRedirect + * Follow redirects automatically, default true + * @property {boolean} trusty + * Trust all certificates + * @property {boolean} wifiOnly + * Only do requests through WiFi. Android SDK 21 or above only. * * @return {function} This method returns a `fetch` method instance. */ diff --git a/index.js.flow b/index.js.flow index ecfd50a2d..03666e569 100644 --- a/index.js.flow +++ b/index.js.flow @@ -163,7 +163,9 @@ export type RNFetchBlobConfig = { path?: string, session?: string, timeout?: number, - trusty?: boolean + trusty?: boolean, + wifiOnly?: boolean, + followRedirect?: boolean }; export type RNFetchBlobResponseInfo = { headers: {[fieldName: string]: string}, diff --git a/ios/RNFetchBlobConst.h b/ios/RNFetchBlobConst.h index 6347b7a45..7d09c3a74 100644 --- a/ios/RNFetchBlobConst.h +++ b/ios/RNFetchBlobConst.h @@ -32,6 +32,7 @@ extern NSString *const CONFIG_USE_TEMP; extern NSString *const CONFIG_FILE_PATH; extern NSString *const CONFIG_FILE_EXT; extern NSString *const CONFIG_TRUSTY; +extern NSString *const CONFIG_WIFI_ONLY; extern NSString *const CONFIG_INDICATOR; extern NSString *const CONFIG_KEY; extern NSString *const CONFIG_EXTRA_BLOB_CTYPE; diff --git a/ios/RNFetchBlobConst.m b/ios/RNFetchBlobConst.m index bc9b793a5..1376d692e 100644 --- a/ios/RNFetchBlobConst.m +++ b/ios/RNFetchBlobConst.m @@ -16,6 +16,7 @@ NSString *const CONFIG_FILE_PATH = @"path"; NSString *const CONFIG_FILE_EXT = @"appendExt"; NSString *const CONFIG_TRUSTY = @"trusty"; +NSString *const CONFIG_WIFI_ONLY = @"wifiOnly"; NSString *const CONFIG_INDICATOR = @"indicator"; NSString *const CONFIG_KEY = @"key"; NSString *const CONFIG_EXTRA_BLOB_CTYPE = @"binaryContentTypes"; diff --git a/ios/RNFetchBlobRequest.m b/ios/RNFetchBlobRequest.m index a56cc92d0..cdbe6b1e5 100644 --- a/ios/RNFetchBlobRequest.m +++ b/ios/RNFetchBlobRequest.m @@ -56,7 +56,7 @@ - (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; } - + + if([options valueForKey:CONFIG_WIFI_ONLY] != nil && ![options[CONFIG_WIFI_ONLY] boolValue]){ + [defaultConfigObject setAllowsCellularAccess:NO]; + } + 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 { @@ -156,10 +160,10 @@ - (void) sendRequest:(__weak NSDictionary * _Nullable )options respData = [[NSMutableData alloc] init]; respFile = NO; } - + self.task = [session dataTaskWithRequest:req]; [self.task resume]; - + // network status indicator if ([[options objectForKey:CONFIG_INDICATOR] boolValue]) { dispatch_async(dispatch_get_main_queue(), ^{ @@ -183,17 +187,17 @@ - (void) sendRequest:(__weak NSDictionary * _Nullable )options - (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 @@ -204,7 +208,7 @@ - (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dat } ]; } - + partBuffer = [[NSMutableData alloc] init]; completionHandler(NSURLSessionResponseAllow); @@ -212,11 +216,11 @@ - (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dat } 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"]) { @@ -232,7 +236,7 @@ - (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dat } } else { respType = @"blob"; - + // for XMLHttpRequest, switch response data handling strategy automatically if ([options valueForKey:@"auto"]) { respFile = YES; @@ -242,7 +246,7 @@ - (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dat } else { respType = @"text"; } - + #pragma mark - handling cookies // # 153 get cookies if (response.URL) { @@ -252,7 +256,7 @@ - (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dat [cookieStore setCookies:cookies forURL:response.URL mainDocumentURL:nil]; } } - + [self.bridge.eventDispatcher sendDeviceEventWithName: EVENT_STATE_CHANGE body:@{ @@ -268,33 +272,33 @@ - (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dat } 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]; @@ -304,7 +308,7 @@ - (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dat NSLog(@"write file error"); } } - + completionHandler(NSURLSessionResponseAllow); } @@ -316,30 +320,30 @@ - (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dat 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 @@ -363,16 +367,19 @@ - (void) URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable - (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { - + self.error = error; NSString * errMsg; NSString * respStr; NSString * rnfbRespType; - - dispatch_async(dispatch_get_main_queue(), ^{ - [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; - }); - + + // only run this if we were requested to change it + if ([[options objectForKey:CONFIG_INDICATOR] boolValue]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; + }); + } + if (error) { if (error.domain == NSURLErrorDomain && error.code == NSURLErrorCancelled) { errMsg = @"task cancelled"; @@ -380,7 +387,7 @@ - (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCom errMsg = [error localizedDescription]; } } - + if (respFile) { [writeStream close]; rnfbRespType = RESP_TYPE_PATH; @@ -391,7 +398,7 @@ - (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCom // 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]; @@ -408,18 +415,18 @@ - (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCom } } } - - + + callback(@[ errMsg ?: [NSNull null], rnfbRespType ?: @"", respStr ?: [NSNull null] ]); - + respData = nil; receivedBytes = 0; [session finishTasksAndInvalidate]; - + } // upload progress handler @@ -428,7 +435,7 @@ - (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSen if (totalBytesExpectedToWrite == 0) { return; } - + NSNumber * now = [NSNumber numberWithFloat:((float)totalBytesWritten/(float)totalBytesExpectedToWrite)]; if ([self.uploadProgressConfig shouldReport:now]) { @@ -461,12 +468,12 @@ - (void) URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)sessio - (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); diff --git a/types.js b/types.js index f2f03ccd1..da256af61 100644 --- a/types.js +++ b/types.js @@ -5,7 +5,10 @@ type RNFetchBlobConfig = { appendExt : string, session : string, addAndroidDownloads : any, - indicator : bool + indicator : bool, + followRedirect : bool, + trusty : bool, + wifiOnly : bool }; type RNFetchBlobNative = {