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 0e38899ce..b2ff27251 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) @@ -494,11 +494,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) @@ -655,6 +655,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 fa7b3633b..1f5efedc3 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java +++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java @@ -150,11 +150,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]; @@ -162,6 +164,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(); @@ -238,7 +248,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; @@ -246,9 +258,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 5be77b933..15835cd85 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java +++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java @@ -7,11 +7,13 @@ import android.content.IntentFilter; import android.database.Cursor; import android.net.Uri; +import android.os.Build; import android.os.Bundle; 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; @@ -23,6 +25,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; @@ -37,11 +40,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; @@ -50,26 +56,20 @@ import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; +import okhttp3.TlsVersion; public class RNFetchBlobReq extends BroadcastReceiver implements Runnable { - enum RequestType { - Form, - SingleFile, - AsIs, - WithoutBody, - Others + enum RequestType { + Form, SingleFile, AsIs, WithoutBody, Others } enum ResponseType { - KeepInMemory, - FileStorage + KeepInMemory, FileStorage } enum ResponseFormat { - Auto, - UTF8, - BASE64 + Auto, UTF8, BASE64 } public static HashMap taskTable = new HashMap<>(); @@ -98,7 +98,8 @@ enum ResponseFormat { ArrayList redirects = new ArrayList<>(); OkHttpClient client; - public RNFetchBlobReq(ReadableMap options, String taskId, String method, String url, ReadableMap headers, String body, ReadableArray arrayBody, OkHttpClient client, final Callback callback) { + public RNFetchBlobReq(ReadableMap options, String taskId, String method, String url, ReadableMap headers, + String body, ReadableArray arrayBody, OkHttpClient client, final Callback callback) { this.method = method.toUpperCase(); this.options = new RNFetchBlobConfig(options); this.taskId = taskId; @@ -109,12 +110,11 @@ public RNFetchBlobReq(ReadableMap options, String taskId, String method, String this.rawRequestBodyArray = arrayBody; this.client = client; - if(this.options.fileCache || this.options.path != null) + if (this.options.fileCache || this.options.path != null) responseType = ResponseType.FileStorage; else responseType = ResponseType.KeepInMemory; - if (body != null) requestType = RequestType.SingleFile; else if (arrayBody != null) @@ -124,7 +124,7 @@ else if (arrayBody != null) } public static void cancelTask(String taskId) { - if(taskTable.containsKey(taskId)) { + if (taskTable.containsKey(taskId)) { Call call = taskTable.get(taskId); call.cancel(); taskTable.remove(taskId); @@ -140,23 +140,30 @@ public void run() { if (options.addAndroidDownloads.getBoolean("useDownloadManager")) { Uri uri = Uri.parse(url); DownloadManager.Request req = new DownloadManager.Request(uri); - if(options.addAndroidDownloads.hasKey("notificationsEnum")) { + if (options.addAndroidDownloads.hasKey("notificationsEnum")) { req.setNotificationVisibility(options.addAndroidDownloads.getInt("notificationsEnum")); - } - else{ + } else { req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); } - if(options.addAndroidDownloads.hasKey("title")) { + if (options.addAndroidDownloads.hasKey("title")) { req.setTitle(options.addAndroidDownloads.getString("title")); } - if(options.addAndroidDownloads.hasKey("description")) { + if (options.addAndroidDownloads.hasKey("description")) { req.setDescription(options.addAndroidDownloads.getString("description")); } - if(options.addAndroidDownloads.hasKey("path")) { + 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)); @@ -170,7 +177,7 @@ public void run() { } - if(this.options.multipartFileUpload) { + if (this.options.multipartFileUpload) { HashMap mheaders = new HashMap<>(); // set headers if (headers != null) { @@ -186,7 +193,7 @@ public void run() { bundle.putString("taskId", taskId); bundle.putString("url", url); bundle.putSerializable("mheaders", mheaders); - bundle.putSerializable("requestBodyArray", ((ReadableNativeArray)rawRequestBodyArray).toArrayList()); + bundle.putSerializable("requestBodyArray", ((ReadableNativeArray) rawRequestBodyArray).toArrayList()); Context appCtx = RNFetchBlob.RCTContext.getApplicationContext(); @@ -221,12 +228,11 @@ public void run() { } } - if(this.options.path != null) + if (this.options.path != null) this.destPath = this.options.path; - else if(this.options.fileCache) + else if (this.options.fileCache) this.destPath = RNFetchBlobFS.getTmpPath(RNFetchBlob.RCTContext, cacheKey) + ext; - OkHttpClient.Builder clientBuilder; try { @@ -251,46 +257,43 @@ else if(this.options.fileCache) while (it.hasNextKey()) { String key = it.nextKey(); String value = headers.getString(key); - if(key.equalsIgnoreCase("RNFB-Response")) { - if(value.equalsIgnoreCase("base64")) + if (key.equalsIgnoreCase("RNFB-Response")) { + if (value.equalsIgnoreCase("base64")) responseFormat = ResponseFormat.BASE64; else if (value.equalsIgnoreCase("utf8")) responseFormat = ResponseFormat.UTF8; - } - else { + } else { builder.header(key.toLowerCase(), value); mheaders.put(key.toLowerCase(), value); } } } - if(method.equalsIgnoreCase("post") || method.equalsIgnoreCase("put") || method.equalsIgnoreCase("patch")) { + if (method.equalsIgnoreCase("post") || method.equalsIgnoreCase("put") || method.equalsIgnoreCase("patch")) { String cType = getHeaderIgnoreCases(mheaders, "Content-Type").toLowerCase(); - if(rawRequestBodyArray != null) { + if (rawRequestBodyArray != null) { requestType = RequestType.Form; - } - else if(cType.isEmpty()) { + } else if (cType.isEmpty()) { builder.header("Content-Type", "application/octet-stream"); requestType = RequestType.SingleFile; } - if(rawRequestBody != null) { - if(rawRequestBody.startsWith(RNFetchBlobConst.FILE_PREFIX)) { + if (rawRequestBody != null) { + if (rawRequestBody.startsWith(RNFetchBlobConst.FILE_PREFIX)) { requestType = RequestType.SingleFile; - } - else if (cType.toLowerCase().contains(";base64") || cType.toLowerCase().startsWith("application/octet")) { - cType = cType.replace(";base64","").replace(";BASE64",""); - if(mheaders.containsKey("content-type")) + } else if (cType.toLowerCase().contains(";base64") + || cType.toLowerCase().startsWith("application/octet")) { + cType = cType.replace(";base64", "").replace(";BASE64", ""); + if (mheaders.containsKey("content-type")) mheaders.put("content-type", cType); - if(mheaders.containsKey("Content-Type")) + if (mheaders.containsKey("Content-Type")) mheaders.put("Content-Type", cType); requestType = RequestType.SingleFile; } else { requestType = RequestType.AsIs; } } - } - else { + } else { requestType = RequestType.WithoutBody; } @@ -298,40 +301,33 @@ else if(cType.isEmpty()) { // set request body switch (requestType) { - case SingleFile: - requestBody = new RNFetchBlobBody(taskId) - .chunkedEncoding(isChunkedRequest) - .setRequestType(requestType) - .setBody(rawRequestBody) - .setMIME(MediaType.parse(getHeaderIgnoreCases(mheaders, "content-type"))); - builder.method(method, requestBody); - break; - case AsIs: - requestBody = new RNFetchBlobBody(taskId) - .chunkedEncoding(isChunkedRequest) - .setRequestType(requestType) - .setBody(rawRequestBody) - .setMIME(MediaType.parse(getHeaderIgnoreCases(mheaders, "content-type"))); - builder.method(method, requestBody); - break; - case Form: - String boundary = "RNFetchBlob-" + taskId; - requestBody = new RNFetchBlobBody(taskId) - .chunkedEncoding(isChunkedRequest) - .setRequestType(requestType) - .setBody(rawRequestBodyArray) - .setMIME(MediaType.parse("multipart/form-data; boundary="+ boundary)); - builder.method(method, requestBody); - break; + case SingleFile: + requestBody = new RNFetchBlobBody(taskId).chunkedEncoding(isChunkedRequest).setRequestType(requestType) + .setBody(rawRequestBody) + .setMIME(MediaType.parse(getHeaderIgnoreCases(mheaders, "content-type"))); + builder.method(method, requestBody); + break; + case AsIs: + requestBody = new RNFetchBlobBody(taskId).chunkedEncoding(isChunkedRequest).setRequestType(requestType) + .setBody(rawRequestBody) + .setMIME(MediaType.parse(getHeaderIgnoreCases(mheaders, "content-type"))); + builder.method(method, requestBody); + break; + case Form: + String boundary = "RNFetchBlob-" + taskId; + requestBody = new RNFetchBlobBody(taskId).chunkedEncoding(isChunkedRequest).setRequestType(requestType) + .setBody(rawRequestBodyArray) + .setMIME(MediaType.parse("multipart/form-data; boundary=" + boundary)); + builder.method(method, requestBody); + break; - case WithoutBody: - if(method.equalsIgnoreCase("post") || method.equalsIgnoreCase("put") || method.equalsIgnoreCase("patch")) - { - builder.method(method, RequestBody.create(null, new byte[0])); - } - else - builder.method(method, null); - break; + case WithoutBody: + if (method.equalsIgnoreCase("post") || method.equalsIgnoreCase("put") + || method.equalsIgnoreCase("patch")) { + builder.method(method, RequestBody.create(null, new byte[0])); + } else + builder.method(method, null); + break; } // #156 fix cookie issue @@ -351,46 +347,34 @@ public Response intercept(Chain chain) throws IOException { Response originalResponse = chain.proceed(req); ResponseBody extended; switch (responseType) { - case KeepInMemory: - extended = new RNFetchBlobDefaultResp( - RNFetchBlob.RCTContext, - taskId, - originalResponse.body(), - options.increment); - break; - case FileStorage: - extended = new RNFetchBlobFileResp( - RNFetchBlob.RCTContext, - taskId, - originalResponse.body(), - destPath, - options.overwrite); - break; - default: - extended = new RNFetchBlobDefaultResp( - RNFetchBlob.RCTContext, - taskId, - originalResponse.body(), - options.increment); - break; + case KeepInMemory: + extended = new RNFetchBlobDefaultResp(RNFetchBlob.RCTContext, taskId, + originalResponse.body(), options.increment); + break; + case FileStorage: + extended = new RNFetchBlobFileResp(RNFetchBlob.RCTContext, taskId, originalResponse.body(), + destPath, options.overwrite); + break; + default: + extended = new RNFetchBlobDefaultResp(RNFetchBlob.RCTContext, taskId, + originalResponse.body(), options.increment); + break; } return originalResponse.newBuilder().body(extended).build(); - } - catch(SocketException e) { + } catch (SocketException e) { timeout = true; - } - catch (SocketTimeoutException e ){ + } catch (SocketTimeoutException e) { timeout = true; - RNFetchBlobUtils.emitWarningEvent("RNFetchBlob error when sending request : " + e.getLocalizedMessage()); - } catch(Exception ex) { + RNFetchBlobUtils.emitWarningEvent( + "RNFetchBlob error when sending request : " + e.getLocalizedMessage()); + } catch (Exception ex) { } return chain.proceed(chain.request()); } }); - - if(options.timeout >= 0) { + if (options.timeout >= 0) { clientBuilder.connectTimeout(options.timeout, TimeUnit.MILLISECONDS); clientBuilder.readTimeout(options.timeout, TimeUnit.MILLISECONDS); } @@ -399,26 +383,26 @@ 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); + Call call = client.newCall(req); taskTable.put(taskId, call); call.enqueue(new okhttp3.Callback() { @Override public void onFailure(Call call, IOException e) { cancelTask(taskId); - if(respInfo == null) { + if (respInfo == null) { respInfo = Arguments.createMap(); } // check if this error caused by socket timeout - if(e.getClass().equals(SocketTimeoutException.class)) { + if (e.getClass().equals(SocketTimeoutException.class)) { respInfo.putBoolean("timeout", true); callback.invoke("request timed out.", null, null); - } - else + } else callback.invoke(e.getLocalizedMessage(), null, null); releaseTaskResource(); } @@ -427,20 +411,21 @@ public void onFailure(Call call, IOException e) { public void onResponse(Call call, Response response) throws IOException { ReadableMap notifyConfig = options.addAndroidDownloads; // Download manager settings - if(notifyConfig != null ) { + if (notifyConfig != null) { String title = "", desc = "", mime = "text/plain"; boolean scannable = false, notification = false; - if(notifyConfig.hasKey("title")) + if (notifyConfig.hasKey("title")) title = options.addAndroidDownloads.getString("title"); - if(notifyConfig.hasKey("description")) + if (notifyConfig.hasKey("description")) desc = notifyConfig.getString("description"); - if(notifyConfig.hasKey("mime")) + if (notifyConfig.hasKey("mime")) mime = notifyConfig.getString("mime"); - if(notifyConfig.hasKey("mediaScannable")) + if (notifyConfig.hasKey("mediaScannable")) scannable = notifyConfig.getBoolean("mediaScannable"); - if(notifyConfig.hasKey("notification")) + if (notifyConfig.hasKey("notification")) notification = notifyConfig.getBoolean("notification"); - DownloadManager dm = (DownloadManager)RNFetchBlob.RCTContext.getSystemService(RNFetchBlob.RCTContext.DOWNLOAD_SERVICE); + DownloadManager dm = (DownloadManager) RNFetchBlob.RCTContext + .getSystemService(RNFetchBlob.RCTContext.DOWNLOAD_SERVICE); dm.addCompletedDownload(title, desc, scannable, mime, destPath, contentLength, notification); } @@ -448,7 +433,6 @@ public void onResponse(Call call, Response response) throws IOException { } }); - } catch (Exception error) { error.printStackTrace(); releaseTaskResource(); @@ -460,13 +444,13 @@ public void onResponse(Call call, Response response) throws IOException { * Remove cached information of the HTTP task */ private void releaseTaskResource() { - if(taskTable.containsKey(taskId)) + if (taskTable.containsKey(taskId)) taskTable.remove(taskId); - if(uploadProgressReport.containsKey(taskId)) + if (uploadProgressReport.containsKey(taskId)) uploadProgressReport.remove(taskId); - if(progressReport.containsKey(taskId)) + if (progressReport.containsKey(taskId)) progressReport.remove(taskId); - if(requestBody != null) + if (requestBody != null) requestBody.clearRequestBody(); } @@ -478,78 +462,79 @@ private void done(Response resp) { boolean isBlobResp = isBlobResponse(resp); emitStateEvent(getResponseInfo(resp, isBlobResp)); switch (responseType) { - case KeepInMemory: - try { - // For XMLHttpRequest, automatic response data storing strategy, when response - // data is considered as binary data, write it to file system - if(isBlobResp && options.auto) { - String dest = RNFetchBlobFS.getTmpPath(ctx, taskId); - InputStream ins = resp.body().byteStream(); - FileOutputStream os = new FileOutputStream(new File(dest)); - int read; - byte [] buffer = new byte[10240]; - while ((read = ins.read(buffer)) != -1) { - os.write(buffer, 0, read); - } - ins.close(); - os.flush(); - os.close(); - callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_PATH, dest); + case KeepInMemory: + try { + // For XMLHttpRequest, automatic response data storing strategy, when response + // data is considered as binary data, write it to file system + if (isBlobResp && options.auto) { + String dest = RNFetchBlobFS.getTmpPath(ctx, taskId); + InputStream ins = resp.body().byteStream(); + FileOutputStream os = new FileOutputStream(new File(dest)); + int read; + byte[] buffer = new byte[10240]; + while ((read = ins.read(buffer)) != -1) { + os.write(buffer, 0, read); } - // response data directly pass to JS context as string. - else { - // #73 Check if the response data contains valid UTF8 string, since BASE64 - // encoding will somehow break the UTF8 string format, to encode UTF8 - // string correctly, we should do URL encoding before BASE64. - byte[] b = resp.body().bytes(); - CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder(); - if(responseFormat == ResponseFormat.BASE64) { - callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_BASE64, android.util.Base64.encodeToString(b, Base64.NO_WRAP)); - return; - } - try { - encoder.encode(ByteBuffer.wrap(b).asCharBuffer()); - // if the data contains invalid characters the following lines will be - // skipped. - String utf8 = new String(b); - callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_UTF8, utf8); - } - // This usually mean the data is contains invalid unicode characters, it's - // binary data - catch(CharacterCodingException ignored) { - if(responseFormat == ResponseFormat.UTF8) { - callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_UTF8, ""); - } - else { - callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_BASE64, android.util.Base64.encodeToString(b, Base64.NO_WRAP)); - } + ins.close(); + os.flush(); + os.close(); + callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_PATH, dest); + } + // response data directly pass to JS context as string. + else { + // #73 Check if the response data contains valid UTF8 string, since BASE64 + // encoding will somehow break the UTF8 string format, to encode UTF8 + // string correctly, we should do URL encoding before BASE64. + byte[] b = resp.body().bytes(); + CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder(); + if (responseFormat == ResponseFormat.BASE64) { + callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_BASE64, + android.util.Base64.encodeToString(b, Base64.NO_WRAP)); + return; + } + try { + encoder.encode(ByteBuffer.wrap(b).asCharBuffer()); + // if the data contains invalid characters the following lines will be + // skipped. + String utf8 = new String(b); + callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_UTF8, utf8); + } + // This usually mean the data is contains invalid unicode characters, it's + // binary data + catch (CharacterCodingException ignored) { + if (responseFormat == ResponseFormat.UTF8) { + callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_UTF8, ""); + } else { + callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_BASE64, + android.util.Base64.encodeToString(b, Base64.NO_WRAP)); } } - } catch (IOException e) { - callback.invoke("RNFetchBlob failed to encode response data to BASE64 string.", null); - } - break; - case FileStorage: - try { - // In order to write response data to `destPath` we have to invoke this method. - // It uses customized response body which is able to report download progress - // and write response data to destination path. - resp.body().bytes(); - } catch (Exception ignored) { -// ignored.printStackTrace(); - } - this.destPath = this.destPath.replace("?append=true", ""); - callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_PATH, this.destPath); - break; - default: - try { - callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_UTF8, new String(resp.body().bytes(), "UTF-8")); - } catch (IOException e) { - callback.invoke("RNFetchBlob failed to encode response data to UTF8 string.", null); } - break; + } catch (IOException e) { + callback.invoke("RNFetchBlob failed to encode response data to BASE64 string.", null); + } + break; + case FileStorage: + try { + // In order to write response data to `destPath` we have to invoke this method. + // It uses customized response body which is able to report download progress + // and write response data to destination path. + resp.body().bytes(); + } catch (Exception ignored) { + // ignored.printStackTrace(); + } + this.destPath = this.destPath.replace("?append=true", ""); + callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_PATH, this.destPath); + break; + default: + try { + callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_UTF8, new String(resp.body().bytes(), "UTF-8")); + } catch (IOException e) { + callback.invoke("RNFetchBlob failed to encode response data to UTF8 string.", null); + } + break; } -// if(!resp.isSuccessful()) + // if(!resp.isSuccessful()) resp.body().close(); releaseTaskResource(); } @@ -560,7 +545,8 @@ private void done(Response resp) { * @return Task ID of the target task */ public static RNFetchBlobProgressConfig getReportProgress(String taskId) { - if(!progressReport.containsKey(taskId)) return null; + if (!progressReport.containsKey(taskId)) + return null; return progressReport.get(taskId); } @@ -570,7 +556,8 @@ public static RNFetchBlobProgressConfig getReportProgress(String taskId) { * @return Task ID of the target task */ public static RNFetchBlobProgressConfig getReportUploadProgress(String taskId) { - if(!uploadProgressReport.containsKey(taskId)) return null; + if (!uploadProgressReport.containsKey(taskId)) + return null; return uploadProgressReport.get(taskId); } @@ -587,26 +574,23 @@ private WritableMap getResponseInfo(Response resp, boolean isBlobResp) { info.putString("taskId", this.taskId); info.putBoolean("timeout", timeout); WritableMap headers = Arguments.createMap(); - for(int i =0;i< resp.headers().size();i++) { + for (int i = 0; i < resp.headers().size(); i++) { headers.putString(resp.headers().name(i), resp.headers().value(i)); } WritableArray redirectList = Arguments.createArray(); - for(String r : redirects) { + for (String r : redirects) { redirectList.pushString(r); } info.putArray("redirects", redirectList); info.putMap("headers", headers); Headers h = resp.headers(); - if(isBlobResp) { + if (isBlobResp) { info.putString("respType", "blob"); - } - else if(getHeaderIgnoreCases(h, "content-type").equalsIgnoreCase("text/")) { + } else if (getHeaderIgnoreCases(h, "content-type").equalsIgnoreCase("text/")) { info.putString("respType", "text"); - } - else if(getHeaderIgnoreCases(h, "content-type").contains("application/json")) { + } else if (getHeaderIgnoreCases(h, "content-type").contains("application/json")) { info.putString("respType", "json"); - } - else { + } else { info.putString("respType", ""); } return info; @@ -623,26 +607,28 @@ private boolean isBlobResponse(Response resp) { boolean isText = !ctype.equalsIgnoreCase("text/"); boolean isJSON = !ctype.equalsIgnoreCase("application/json"); boolean isCustomBinary = false; - if(options.binaryContentTypes != null) { - for(int i = 0; i< options.binaryContentTypes.size();i++) { - if(ctype.toLowerCase().contains(options.binaryContentTypes.getString(i).toLowerCase())) { + if (options.binaryContentTypes != null) { + for (int i = 0; i < options.binaryContentTypes.size(); i++) { + if (ctype.toLowerCase().contains(options.binaryContentTypes.getString(i).toLowerCase())) { isCustomBinary = true; break; } } } - return (!(isJSON || isText)) || isCustomBinary; + return (!(isJSON || isText)) || isCustomBinary; } private String getHeaderIgnoreCases(Headers headers, String field) { String val = headers.get(field); - if(val != null) return val; + if (val != null) + return val; return headers.get(field.toLowerCase()) == null ? "" : headers.get(field.toLowerCase()); } - private String getHeaderIgnoreCases(HashMap headers, String field) { + private String getHeaderIgnoreCases(HashMap headers, String field) { String val = headers.get(field); - if(val != null) return val; + if (val != null) + return val; String lowerCasedValue = headers.get(field.toLowerCase()); return lowerCasedValue == null ? "" : lowerCasedValue; } @@ -665,20 +651,24 @@ public void onReceive(Context context, Intent intent) { dm.query(query); Cursor c = dm.query(query); - String filePath = null; // the file exists in media content database if (c.moveToFirst()) { // #297 handle failed request int statusCode = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS)); - if(statusCode == DownloadManager.STATUS_FAILED) { - this.callback.invoke("Download manager failed to download from " + this.url + ". Statu Code = " + statusCode, null, null); + if (statusCode == DownloadManager.STATUS_FAILED) { + this.callback.invoke("Download manager failed to download from " + this.url + ". Statu Code = " + + statusCode, null, null); 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); + Cursor cursor = appCtx.getContentResolver().query(uri, + new String[] { android.provider.MediaStore.Images.ImageColumns.DATA }, null, null, + null); + // use default destination of DownloadManager if (cursor != null) { cursor.moveToFirst(); @@ -686,24 +676,26 @@ public void onReceive(Context context, Intent intent) { } } } + // When the file is not found in media content database, check if custom path exists if (options.addAndroidDownloads.hasKey("path")) { try { String customDest = options.addAndroidDownloads.getString("path"); boolean exists = new File(customDest).exists(); - if(!exists) - throw new Exception("Download manager download failed, the file does not downloaded to destination."); + if (!exists) + throw new Exception( + "Download manager download failed, the file does not downloaded to destination."); else this.callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_PATH, customDest); - } catch(Exception ex) { + } catch (Exception ex) { ex.printStackTrace(); this.callback.invoke(ex.getLocalizedMessage(), null); } - } - else { - if(filePath == null) - this.callback.invoke("Download manager could not resolve downloaded file path.", RNFetchBlobConst.RNFB_RESPONSE_PATH, null); + } else { + if (filePath == null) + this.callback.invoke("Download manager could not resolve downloaded file path.", + RNFetchBlobConst.RNFB_RESPONSE_PATH, null); else this.callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_PATH, filePath); } @@ -743,4 +735,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 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.js b/ios.js index 95df49f0c..db8354c33 100644 --- a/ios.js +++ b/ios.js @@ -6,10 +6,10 @@ import { NativeModules, DeviceEventEmitter, Platform, - NativeAppEventEmitter, + NativeAppEventEmitter } from 'react-native' -const RNFetchBlob:RNFetchBlobNative = NativeModules.RNFetchBlob +const RNFetchBlob: RNFetchBlobNative = NativeModules.RNFetchBlob /** * Open a file using UIDocumentInteractionController @@ -17,25 +17,54 @@ const RNFetchBlob:RNFetchBlobNative = NativeModules.RNFetchBlob * @param {string} scheme URI scheme that needs to support, optional * @return {Promise} */ -function previewDocument(path:string, scheme:string) { - if(Platform.OS === 'ios') +function previewDocument(path: string, scheme: string) { + if (Platform.OS === 'ios') { return RNFetchBlob.previewDocument('file://' + path, scheme) - else + } else { return Promise.reject('RNFetchBlob.openDocument only supports IOS.') + } } /** * Preview a file using UIDocumentInteractionController * @param {string]} path Path of the file to be open. * @param {string} scheme URI scheme that needs to support, optional - * @param {string} name The name of the target file, optional * @return {Promise} */ -function openDocument(path:string, scheme:string, name: string) { - if(Platform.OS === 'ios') - return RNFetchBlob.openDocument('file://' + path, scheme, name) - else - return Promise.reject('RNFetchBlob.previewDocument only supports IOS.') +function openDocument(path: string, scheme: string) { + if (Platform.OS === 'ios') { + return RNFetchBlob.openDocument('file://' + path, scheme) + } +} + +/** + * Preview a file using UIDocumentInteractionController + * @param {string} path Path of the file to be open. + * @param {string} fontFamily The name of the font family + * @param {string} hexString The color in hex format + * @param {number} fontSize Size of the font + * @param {string} backgroundColor The color of the navigation bar in hex format + * @param {string} scheme URI scheme that needs to support, optional + * @return {Promise} + */ +function openDocumentWithFont( + path: string, + fontFamily: string, + fontSize: number, + hexString: string, + backgroundColor: string, + scheme: string +) { + if (Platform.OS === 'ios') { + return RNFetchBlob.openDocumentWithFont( + 'file://' + path, + fontFamily, + fontSize, + hexString, + backgroundColor, + scheme + ) + } } /** @@ -44,12 +73,13 @@ function openDocument(path:string, scheme:string, name: string) { * @param {string} url URL of the resource, only file URL is supported * @return {Promise} */ -function excludeFromBackupKey(url:string) { - return RNFetchBlob.excludeFromBackupKey('file://' + path); +function excludeFromBackupKey(url: string) { + return RNFetchBlob.excludeFromBackupKey('file://' + path) } export default { openDocument, + openDocumentWithFont, previewDocument, excludeFromBackupKey } diff --git a/ios/RNFetchBlob/RNFetchBlob.m b/ios/RNFetchBlob/RNFetchBlob.m index d6f957195..0eb997960 100644 --- a/ios/RNFetchBlob/RNFetchBlob.m +++ b/ios/RNFetchBlob/RNFetchBlob.m @@ -11,7 +11,6 @@ #import "RNFetchBlobReqBuilder.h" #import "RNFetchBlobProgress.h" - __strong RCTBridge * bridgeRef; dispatch_queue_t commonTaskQueue; dispatch_queue_t fsQueue; @@ -31,8 +30,9 @@ @implementation RNFetchBlob @synthesize bridge = _bridge; - (dispatch_queue_t) methodQueue { - if(commonTaskQueue == nil) + if(commonTaskQueue == nil) { commonTaskQueue = dispatch_queue_create("RNFetchBlob.queue", DISPATCH_QUEUE_SERIAL); + } return commonTaskQueue; } @@ -47,11 +47,14 @@ + (RCTBridge *)getRCTBridge - (id) init { self = [super init]; self.filePathPrefix = FILE_PREFIX; - if(commonTaskQueue == nil) + if(commonTaskQueue == nil) { commonTaskQueue = dispatch_queue_create("RNFetchBlob.queue", DISPATCH_QUEUE_SERIAL); - if(fsQueue == nil) + } + if(fsQueue == nil) { fsQueue = dispatch_queue_create("RNFetchBlob.fs.queue", DISPATCH_QUEUE_SERIAL); + } BOOL isDir; + // if temp folder not exists, create one if(![[NSFileManager defaultManager] fileExistsAtPath: [RNFetchBlobFS getTempPath] isDirectory:&isDir]) { [[NSFileManager defaultManager] createDirectoryAtPath:[RNFetchBlobFS getTempPath] withIntermediateDirectories:YES attributes:nil error:NULL]; @@ -66,8 +69,7 @@ - (NSDictionary *)constantsToExport return @{ @"MainBundleDir" : [RNFetchBlobFS getMainBundleDir], @"DocumentDir": [RNFetchBlobFS getDocumentDir], - @"CacheDir" : [RNFetchBlobFS getCacheDir], - @"LibraryDir" : [RNFetchBlobFS getLibraryDir] + @"CacheDir" : [RNFetchBlobFS getCacheDir] }; } @@ -80,7 +82,7 @@ - (NSDictionary *)constantsToExport form:(NSArray *)form callback:(RCTResponseSenderBlock)callback) { - + [RNFetchBlobReqBuilder buildMultipartRequest:options taskId:taskId method:method @@ -88,20 +90,20 @@ - (NSDictionary *)constantsToExport headers:headers form:form onComplete:^(__weak NSURLRequest *req, long bodyLength) - { - // something went wrong when building the request body - if(req == nil) - { - callback(@[@"RNFetchBlob.fetchBlobForm failed to create request body"]); - } - // send HTTP request - else - { - RNFetchBlobNetwork * utils = [[RNFetchBlobNetwork alloc] init]; - [utils sendRequest:options contentLength:bodyLength bridge:self.bridge taskId:taskId withRequest:req callback:callback]; - } - }]; - + { + // something went wrong when building the request body + if(req == nil) + { + callback(@[@"RNFetchBlob.fetchBlobForm failed to create request body"]); + } + // send HTTP request + else + { + RNFetchBlobNetwork * utils = [[RNFetchBlobNetwork alloc] init]; + [utils sendRequest:options contentLength:bodyLength bridge:self.bridge taskId:taskId withRequest:req callback:callback]; + } + }]; + } @@ -120,27 +122,27 @@ - (NSDictionary *)constantsToExport headers:headers body:body onComplete:^(NSURLRequest *req, long bodyLength) - { - // something went wrong when building the request body - if(req == nil) - { - callback(@[@"RNFetchBlob.fetchBlob failed to create request body"]); - } - // send HTTP request - else - { - __block RNFetchBlobNetwork * utils = [[RNFetchBlobNetwork alloc] init]; - [utils sendRequest:options contentLength:bodyLength bridge:self.bridge taskId:taskId withRequest:req callback:callback]; - } - }]; + { + // something went wrong when building the request body + if(req == nil) + { + callback(@[@"RNFetchBlob.fetchBlob failed to create request body"]); + } + // send HTTP request + else + { + __block RNFetchBlobNetwork * utils = [[RNFetchBlobNetwork alloc] init]; + [utils sendRequest:options contentLength:bodyLength bridge:self.bridge taskId:taskId withRequest:req callback:callback]; + } + }]; } #pragma mark - fs.createFile RCT_EXPORT_METHOD(createFile:(NSString *)path data:(NSString *)data encoding:(NSString *)encoding callback:(RCTResponseSenderBlock)callback) { - + NSFileManager * fm = [NSFileManager defaultManager]; NSData * fileContent = nil; - + if([[encoding lowercaseString] isEqualToString:@"utf8"]) { fileContent = [[NSData alloc] initWithData:[data dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES]]; } @@ -154,35 +156,36 @@ - (NSDictionary *)constantsToExport else { fileContent = [[NSData alloc] initWithData:[data dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES]]; } - + BOOL success = [fm createFileAtPath:path contents:fileContent attributes:NULL]; - if(success == YES) + if(success == YES) { callback(@[[NSNull null]]); - else + } else { callback(@[[NSString stringWithFormat:@"failed to create new file at path %@ please ensure the folder exists"]]); - + } } #pragma mark - fs.createFileASCII // method for create file with ASCII content RCT_EXPORT_METHOD(createFileASCII:(NSString *)path data:(NSArray *)dataArray callback:(RCTResponseSenderBlock)callback) { - + NSFileManager * fm = [NSFileManager defaultManager]; NSMutableData * fileContent = [NSMutableData alloc]; // prevent stack overflow, alloc on heap char * bytes = (char*) malloc([dataArray count]); - + for(int i = 0; i < dataArray.count; i++) { bytes[i] = [[dataArray objectAtIndex:i] charValue]; } [fileContent appendBytes:bytes length:dataArray.count]; BOOL success = [fm createFileAtPath:path contents:fileContent attributes:NULL]; free(bytes); - if(success == YES) + + if(success == YES) { callback(@[[NSNull null]]); - else + } else { callback(@[[NSString stringWithFormat:@"failed to create new file at path %@ please ensure the folder exists"]]); - + } } #pragma mark - fs.pathForAppGroup @@ -191,7 +194,7 @@ - (NSDictionary *)constantsToExport rejecter:(RCTPromiseRejectBlock)reject) { NSString * path = [RNFetchBlobFS getPathForAppGroup:groupName]; - + if(path) { resolve(path); } else { @@ -268,10 +271,11 @@ - (NSDictionary *)constantsToExport NSError * error = nil; NSString * tmpPath = nil; [[NSFileManager defaultManager] removeItemAtPath:path error:&error]; - if(error == nil || [[NSFileManager defaultManager] fileExistsAtPath:path] == NO) + if(error == nil || [[NSFileManager defaultManager] fileExistsAtPath:path] == NO) { callback(@[[NSNull null]]); - else + } else { callback(@[[NSString stringWithFormat:@"failed to unlink file or path at %@", path]]); + } } #pragma mark - fs.removeSession @@ -279,7 +283,7 @@ - (NSDictionary *)constantsToExport { NSError * error = nil; NSString * tmpPath = nil; - + for(NSString * path in paths) { [[NSFileManager defaultManager] removeItemAtPath:path error:&error]; if(error != nil) { @@ -288,7 +292,7 @@ - (NSDictionary *)constantsToExport } } callback(@[[NSNull null]]); - + } #pragma mark - fs.ls @@ -300,22 +304,21 @@ - (NSDictionary *)constantsToExport exist = [fm fileExistsAtPath:path isDirectory:&isDir]; if(exist == NO || isDir == NO) { callback(@[[NSString stringWithFormat:@"failed to list path `%@` for it is not exist or it is not a folder", path]]); - return ; + return; } NSError * error = nil; NSArray * result = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:&error]; - - if(error == nil) + + if(error == nil) { callback(@[[NSNull null], result == nil ? [NSNull null] :result ]); - else + } else { callback(@[[error localizedDescription], [NSNull null]]); - + } } #pragma mark - fs.stat RCT_EXPORT_METHOD(stat:(NSString *)target callback:(RCTResponseSenderBlock) callback) { - [RNFetchBlobFS getPathFromUri:target completionHandler:^(NSString *path, ALAssetRepresentation *asset) { __block NSMutableArray * result; if(path != nil) @@ -324,19 +327,19 @@ - (NSDictionary *)constantsToExport BOOL exist = nil; BOOL isDir = nil; NSError * error = nil; - + exist = [fm fileExistsAtPath:path isDirectory:&isDir]; if(exist == NO) { callback(@[[NSString stringWithFormat:@"failed to stat path `%@` for it is not exist or it is not exist", path]]); return ; } result = [RNFetchBlobFS stat:path error:&error]; - - if(error == nil) + + if(error == nil) { callback(@[[NSNull null], result]); - else + } else { callback(@[[error localizedDescription], [NSNull null]]); - + } } else if(asset != nil) { @@ -358,17 +361,17 @@ - (NSDictionary *)constantsToExport NSFileManager* fm = [NSFileManager defaultManager]; BOOL exist = nil; BOOL isDir = nil; - + path = [RNFetchBlobFS getPathOfAsset:path]; - + exist = [fm fileExistsAtPath:path isDirectory:&isDir]; if(exist == NO) { callback(@[[NSString stringWithFormat:@"failed to list path `%@` for it is not exist or it is not exist", path]]); - return ; + return; } NSError * error = nil; NSArray * files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:&error]; - + NSMutableArray * res = [[NSMutableArray alloc] init]; if(isDir == YES) { for(NSString * p in files) { @@ -379,19 +382,19 @@ - (NSDictionary *)constantsToExport else { [res addObject:[RNFetchBlobFS stat:path error:&error]]; } - - if(error == nil) + + if(error == nil) { callback(@[[NSNull null], res == nil ? [NSNull null] :res ]); - else + } else { callback(@[[error localizedDescription], [NSNull null]]); - + } } #pragma mark - fs.cp RCT_EXPORT_METHOD(cp:(NSString*)src toPath:(NSString *)dest callback:(RCTResponseSenderBlock) callback) { - -// path = [RNFetchBlobFS getPathOfAsset:path]; + + // path = [RNFetchBlobFS getPathOfAsset:path]; [RNFetchBlobFS getPathFromUri:src completionHandler:^(NSString *path, ALAssetRepresentation *asset) { NSError * error = nil; if(path == nil) @@ -402,14 +405,15 @@ - (NSDictionary *)constantsToExport else { BOOL result = [[NSFileManager defaultManager] copyItemAtURL:[NSURL fileURLWithPath:path] toURL:[NSURL fileURLWithPath:dest] error:&error]; - - if(error == nil) + + if(error == nil) { callback(@[[NSNull null], @YES]); - else + } else { callback(@[[error localizedDescription], @NO]); + } } }]; - + } @@ -418,12 +422,12 @@ - (NSDictionary *)constantsToExport { NSError * error = nil; BOOL result = [[NSFileManager defaultManager] moveItemAtURL:[NSURL fileURLWithPath:path] toURL:[NSURL fileURLWithPath:dest] error:&error]; - - if(error == nil) + + if(error == nil) { callback(@[[NSNull null], @YES]); - else + } else { callback(@[[error localizedDescription], @NO]); - + } } #pragma mark - fs.mkdir @@ -432,10 +436,10 @@ - (NSDictionary *)constantsToExport if([[NSFileManager defaultManager] fileExistsAtPath:path]) { callback(@[@"mkdir failed, folder already exists"]); return; - } - else + } else { [RNFetchBlobFS mkdir:path]; - callback(@[[NSNull null]]); + callback(@[[NSNull null]]); + } } #pragma mark - fs.readFile @@ -444,7 +448,7 @@ - (NSDictionary *)constantsToExport resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - + [RNFetchBlobFS readFile:path encoding:encoding onComplete:^(id content, NSString * err) { if(err != nil) { @@ -466,12 +470,13 @@ - (NSDictionary *)constantsToExport RCT_EXPORT_METHOD(readStream:(NSString *)path withEncoding:(NSString *)encoding bufferSize:(int)bufferSize tick:(int)tick streamId:(NSString *)streamId) { if(bufferSize == nil) { - if([[encoding lowercaseString] isEqualToString:@"base64"]) + if([[encoding lowercaseString] isEqualToString:@"base64"]) { bufferSize = 4095; - else + } else { bufferSize = 4096; + } } - + dispatch_async(fsQueue, ^{ [RNFetchBlobFS readStream:path encoding:encoding bufferSize:bufferSize tick:tick streamId:streamId bridgeRef:_bridge]; }); @@ -480,7 +485,7 @@ - (NSDictionary *)constantsToExport #pragma mark - fs.getEnvionmentDirs RCT_EXPORT_METHOD(getEnvironmentDirs:(RCTResponseSenderBlock) callback) { - + callback(@[ [RNFetchBlobFS getDocumentDir], [RNFetchBlobFS getCacheDir], @@ -491,13 +496,13 @@ - (NSDictionary *)constantsToExport RCT_EXPORT_METHOD(cancelRequest:(NSString *)taskId callback:(RCTResponseSenderBlock)callback) { [RNFetchBlobNetwork cancelRequest:taskId]; callback(@[[NSNull null], taskId]); - + } #pragma mark - net.enableProgressReport RCT_EXPORT_METHOD(enableProgressReport:(NSString *)taskId interval:(nonnull NSNumber*)interval count:(nonnull NSNumber*)count) { - + RNFetchBlobProgress * cfg = [[RNFetchBlobProgress alloc] initWithType:Download interval:interval count:count]; [RNFetchBlobNetwork enableProgressReport:taskId config:cfg]; } @@ -524,10 +529,10 @@ - (NSDictionary *)constantsToExport UIViewController *rootCtrl = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; documentController.delegate = self; if(scheme == nil || [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:scheme]]) { - CGRect rect = CGRectMake(0.0, 0.0, 0.0, 0.0); - dispatch_sync(dispatch_get_main_queue(), ^{ - [documentController presentOptionsMenuFromRect:rect inView:rootCtrl.view animated:YES]; - }); + CGRect rect = CGRectMake(0.0, 0.0, 0.0, 0.0); + dispatch_sync(dispatch_get_main_queue(), ^{ + [documentController presentOptionsMenuFromRect:rect inView:rootCtrl.view animated:YES]; + }); resolve(@[[NSNull null]]); } else { reject(@"RNFetchBlob could not open document", @"scheme is not supported", nil); @@ -536,15 +541,65 @@ - (NSDictionary *)constantsToExport # pragma mark - open file with UIDocumentInteractionController and delegate -RCT_EXPORT_METHOD(openDocument:(NSString*)uri scheme:(NSString *)scheme name:(NSString*)name resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXPORT_METHOD(openDocument:(NSString*)uri scheme:(NSString*)scheme resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { NSString * utf8uri = [uri stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSURL * url = [[NSURL alloc] initWithString:utf8uri]; - // NSURL * url = [[NSURL alloc] initWithString:uri]; documentController = [UIDocumentInteractionController interactionControllerWithURL:url]; documentController.delegate = self; - documentController.name = name; + + if(scheme == nil || [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:scheme]]) { + dispatch_sync(dispatch_get_main_queue(), ^{ + [documentController presentPreviewAnimated:YES]; + }); + resolve(@[[NSNull null]]); + } else { + reject(@"RNFetchBlob could not open document", @"scheme is not supported", nil); + } +} +RCT_EXPORT_METHOD(openDocumentWithFont:(NSString*)uri fontFamily:(NSString*)fontFamily fontSize:(CGFloat)fontSize hexString:(NSString*)hexString backgroundColor:(NSString*)backgroundColor scheme:(NSString*)scheme resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString * utf8uri = [uri stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + NSURL * url = [[NSURL alloc] initWithString:utf8uri]; + // NSURL * url = [[NSURL alloc] initWithString:uri]; + unsigned rgbValue = 0; + NSScanner *scanner = [NSScanner scannerWithString:hexString]; + if (fontFamily) { + NSMutableDictionary *titleBarAttributes = [NSMutableDictionary dictionaryWithDictionary: [[UINavigationBar appearance] titleTextAttributes]]; + [titleBarAttributes setValue:[UIFont fontWithName:fontFamily size:fontSize] forKey:NSFontAttributeName]; + [scanner setScanLocation:1]; // bypass '#' character + [scanner scanHexInt:&rgbValue]; + + [titleBarAttributes setValue:[UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 \ + green:((float)((rgbValue & 0x00FF00) >> 8))/255.0 \ + blue:((float)((rgbValue & 0x0000FF) >> 0))/255.0 \ + alpha:1.0] forKey:NSForegroundColorAttributeName]; + [[UINavigationBar appearance] setTitleTextAttributes:titleBarAttributes]; + + NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithDictionary: [[UIBarButtonItem appearance] titleTextAttributesForState:UIControlStateNormal]]; + [attributes setValue:[UIFont fontWithName:fontFamily size:fontSize] forKey:NSFontAttributeName]; + [[UIBarButtonItem appearance] setTitleTextAttributes:attributes forState:UIControlStateNormal]; + [[UIBarButtonItem appearance] setTintColor: [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 \ + green:((float)((rgbValue & 0x00FF00) >> 8))/255.0 \ + blue:((float)((rgbValue & 0x0000FF) >> 0))/255.0 \ + alpha:1.0]]; + } + + if (backgroundColor) { + scanner = [NSScanner scannerWithString:backgroundColor]; + [scanner setScanLocation:1]; + [scanner scanHexInt:&rgbValue]; + + [[UINavigationBar appearance] setBackgroundColor:[UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 \ + green:((float)((rgbValue & 0x00FF00) >> 8))/255.0 \ + blue:((float)((rgbValue & 0x0000FF) >> 0))/255.0 \ + alpha:1.0]]; + } + + documentController = [UIDocumentInteractionController interactionControllerWithURL:url]; + documentController.delegate = self; + if(scheme == nil || [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:scheme]]) { dispatch_sync(dispatch_get_main_queue(), ^{ [documentController presentPreviewAnimated:YES]; @@ -567,7 +622,7 @@ - (NSDictionary *)constantsToExport } else { reject(@"RNFetchBlob could not open document", [error description], nil); } - + } @@ -576,7 +631,7 @@ - (NSDictionary *)constantsToExport [RNFetchBlobFS df:callback]; } -- (UIViewController *) documentInteractionControllerViewControllerForPreview: (UIDocumentInteractionController *) controller +- (UIViewController *)documentInteractionControllerViewControllerForPreview:(UIDocumentInteractionController *)controller { UIWindow *window = [UIApplication sharedApplication].keyWindow; return window.rootViewController; @@ -602,3 +657,5 @@ - (UIViewController *) documentInteractionControllerViewControllerForPreview: (U @end + + diff --git a/ios/RNFetchBlobFS.m b/ios/RNFetchBlobFS.m index 8142806f8..74548666a 100644 --- a/ios/RNFetchBlobFS.m +++ b/ios/RNFetchBlobFS.m @@ -34,7 +34,7 @@ //////////////////////////////////////// @interface RNFetchBlobFS() { UIDocumentInteractionController * docCtrl; - + } @end @implementation RNFetchBlobFS @@ -261,11 +261,11 @@ + (void) emitDataChunks:(NSData *)data encoding:(NSString *) encoding streamId:( [asciiArray addObject:[NSNumber numberWithChar:bytePtr[i]]]; } } - + NSDictionary * payload = @{ @"event": FS_EVENT_DATA, @"detail" : asciiArray }; [event sendDeviceEventWithName:streamId body:payload]; } - + } @catch (NSException * ex) { @@ -463,9 +463,9 @@ + (void) readFile:(NSString *)path return; } fileContent = [NSData dataWithContentsOfFile:path]; - + } - + if(encoding != nil) { if([[encoding lowercaseString] isEqualToString:@"utf8"]) @@ -492,7 +492,7 @@ + (void) readFile:(NSString *)path { onComplete(fileContent, nil); } - + }]; } @@ -575,11 +575,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]; + decodedData = [[NSData alloc] initWithBase64EncodedString:chunk options: NSDataBase64DecodingIgnoreUnknownCharacters]; } - if([[self.encoding lowercaseString] isEqualToString:@"utf8"]) { + else if([[self.encoding lowercaseString] isEqualToString:@"utf8"]) { decodedData = [chunk dataUsingEncoding:NSUTF8StringEncoding]; } else if([[self.encoding lowercaseString] isEqualToString:@"ascii"]) { @@ -775,7 +775,7 @@ +(void) df:(RCTResponseSenderBlock)callback if (dictionary) { NSNumber *fileSystemSizeInBytes = [dictionary objectForKey: NSFileSystemSize]; NSNumber *freeFileSystemSizeInBytes = [dictionary objectForKey:NSFileSystemFreeSize]; - + callback(@[[NSNull null], @{ @"free" : freeFileSystemSizeInBytes, @"total" : fileSystemSizeInBytes, @@ -803,60 +803,4 @@ + (void) writeAssetToPath:(ALAssetRepresentation * )rep dest:(NSString *)dest return; } -+ (void) openFileHandle:(NSString * )uri - mode:(NSString *)mode - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject -{ - openedHandleCount ++; - RNFetchBlobFileHandle * handle; - handle = [[RNFetchBlobFileHandle alloc] initWithPath:uri mode:mode]; - - if(fileHandles == nil) - { - fileHandles = [[NSMutableDictionary alloc] init]; - [fileHandles setObject:handle forKey:[NSNumber numberWithLong:openedHandleCount]]; - } - resolve([NSNumber numberWithLong:openedHandleCount]); -} - -+ (void) writeFileHandle:(NSNumber *)hid - encoding:(NSString *)encoding - data:(NSString *)data - offset:(NSNumber *)offset - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject -{ - if(fileHandles == nil) - return; - NSNumber * handleId = [NSNumber numberWithLong:[hid longValue]]; - RNFetchBlobFileHandle * handle = [fileHandles objectForKey:handleId]; - [handle write:encoding data:data offset:offset onComplete:^(NSNumber *written) { - resolve([NSNull null]); - }]; - -} - -+ (void) readFileHandle:(NSNumber *)hid - encoding:(NSString *)encoding - offset:(NSNumber *)offset - length:(NSNumber *)length - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject -{ - if(fileHandles == nil) - return; - NSNumber * handleId = [NSNumber numberWithLong:[hid longValue]]; - [fileHandles objectForKey:handleId]; - RNFetchBlobFileHandle * handle = [fileHandles objectForKey:handleId]; - id result = [handle read:encoding offset:offset length:length]; - if(![encoding isEqualToString:@"ascii"]) - { - resolve((NSString *) result); - } - else - { - resolve((NSArray *) result); - } -} @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)