diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE deleted file mode 100644 index b03e3e8e3..000000000 --- a/.github/PULL_REQUEST_TEMPLATE +++ /dev/null @@ -1,5 +0,0 @@ -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.9 -3. Correct README.md can directly to master diff --git a/.gitignore b/.gitignore index 418c56e5c..1e25f4d3d 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ local.properties # node_modules/ npm-debug.log +yarn.lock # BUCK buck-out/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 000000000..1b63e0419 --- /dev/null +++ b/.npmignore @@ -0,0 +1,8 @@ +CONTRIBUTORS.md +CONTRIBUTING.md +CODE_OF_CONDUCT.md + +.github/ +components/ +img/ +scripts/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fb71468eb..ad245bfce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1 @@ -For developers who interested in making contribution to this project, please see [https://github.com/wkh237/react-native-fetch-blob-dev](https://github.com/wkh237/react-native-fetch-blob-dev) for more information. - -Please read the following rules before opening a PR : - -1. If the PR is offering a feature please make the PR to our "Feature Branch" 0.11.0 -2. Bug fix request to "Bug Fix Branch" 0.10.6 -3. Correct README.md can directly to master +For developers who interested in making contribution to this project, please see [https://github.com/joltup/rn-fetch-blob-dev](https://github.com/joltup/rn-fetch-blob-dev) for more information. diff --git a/README.md b/README.md index 8831db5cf..584845ccd 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,15 @@ -# react-native-fetch-blob -[![release](https://img.shields.io/github/release/wkh237/react-native-fetch-blob.svg?style=flat-square)](https://github.com/wkh237/react-native-fetch-blob/releases) [![npm](https://img.shields.io/npm/v/react-native-fetch-blob.svg?style=flat-square)](https://www.npmjs.com/package/react-native-fetch-blob) ![](https://img.shields.io/badge/PR-Welcome-brightgreen.svg?style=flat-square) [![](https://img.shields.io/badge/Wiki-Public-brightgreen.svg?style=flat-square)](https://github.com/wkh237/react-native-fetch-blob/wiki) [![npm](https://img.shields.io/npm/l/react-native-fetch-blob.svg?maxAge=2592000&style=flat-square)]() +# rn-fetch-blob +[![release](https://img.shields.io/github/release/joltup/rn-fetch-blob.svg?style=flat-square)](https://github.com/joltup/rn-fetch-blob/releases) [![npm](https://img.shields.io/npm/v/rn-fetch-blob.svg?style=flat-square)](https://www.npmjs.com/package/rn-fetch-blob) ![](https://img.shields.io/badge/PR-Welcome-brightgreen.svg?style=flat-square) [![](https://img.shields.io/badge/Wiki-Public-brightgreen.svg?style=flat-square)](https://github.com/joltup/rn-fetch-blob/wiki) [![npm](https://img.shields.io/npm/l/rn-fetch-blob.svg?maxAge=2592000&style=flat-square)]() A project committed to making file access and data transfer easier and more efficient for React Native developers. -> For Firebase Storage solution, please upgrade to the latest version for the best compatibility. + +# Version Compatibility Warning + +rn-fetch-blob version 0.10.16 is only compatible with react native 0.60 and up. It should have been a major version bump, we apologize for the mistake. If you are not yet upgraded to react native 0.60 or above, you should remain on rn-fetch-blob version 0.10.15 ## Features + - Transfer data directly from/to storage without BASE64 bridging - File API supports regular files, Asset files, and CameraRoll files - Native-to-native file manipulation API, reduce JS bridging performance loss @@ -13,87 +17,111 @@ A project committed to making file access and data transfer easier and more effi - Blob, File, XMLHttpRequest polyfills that make browser-based library available in RN (experimental) - JSON stream supported base on [Oboe.js](https://github.com/jimhigson/oboe.js/) @jimhigson -## TOC (visit [Wiki](https://github.com/wkh237/react-native-fetch-blob/wiki) to get the complete documentation) -* [About](#user-content-about) -* [Installation](#user-content-installation) -* [HTTP Data Transfer](#user-content-http-data-transfer) - * [Regular Request](#user-content-regular-request) - * [Download file](#user-content-download-example--fetch-files-that-needs-authorization-token) - * [Upload file](#user-content-upload-example--dropbox-files-upload-api) - * [Multipart/form upload](#user-content-multipartform-data-example--post-form-data-with-file-and-data) - * [Upload/Download progress](#user-content-uploaddownload-progress) - * [Cancel HTTP request](#user-content-cancel-request) - * [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) - * [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-tips) -* [API References](https://github.com/wkh237/react-native-fetch-blob/wiki/Fetch-API) -* [Caveats](#user-content-caveats) -* [Development](#user-content-development) +## TOC (visit [Wiki](https://github.com/joltup/rn-fetch-blob/wiki) to get the complete documentation) + +- [About](#user-content-about) +- [Installation](#user-content-installation) +- [HTTP Data Transfer](#user-content-http-data-transfer) +- [Regular Request](#user-content-regular-request) +- [Download file](#download-example-fetch-files-that-need-authorization-token) +- [Upload file](#user-content-upload-example--dropbox-files-upload-api) +- [Multipart/form upload](#user-content-multipartform-data-example--post-form-data-with-file-and-data) +- [Upload/Download progress](#user-content-uploaddownload-progress) +- [Cancel HTTP request](#user-content-cancel-request) +- [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) +- [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-tips) +- [API References](https://github.com/joltup/rn-fetch-blob/wiki/Fetch-API) +- [Caveats](#user-content-caveats) +- [Development](#user-content-development) ## 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. -In `0.8.0` we introduced experimental Web API polyfills that make it possible to use browser-based libraries in React Native, such as, [FireBase JS SDK](https://github.com/wkh237/rn-firebase-storage-upload-sample) - +In `0.8.0` we introduced experimental Web API polyfills that make it possible to use browser-based libraries in React Native, such as, [FireBase JS SDK](https://github.com/joltup/rn-firebase-storage-upload-sample) ## Installation Install package from npm ```sh -npm install --save react-native-fetch-blob +npm install --save rn-fetch-blob ``` Or if using CocoaPods, add the pod to your `Podfile` ``` -pod 'react-native-fetch-blob', - :path => '../node_modules/react-native-fetch-blob' +pod 'rn-fetch-blob', + :path => '../node_modules/rn-fetch-blob' ``` -After `0.10.3` you can install this package directly from Github +After `0.10.3` you can install this package directly from Github ```sh # replace with any one of the branches -npm install --save github:wkh237/react-native-fetch-blob-package# +npm install --save github:joltup/rn-fetch-blob# ``` + +**Manually Link Native Modules** + +If automatically linking doesn't work for you, see instructions on [manually linking](https://github.com/joltup/rn-fetch-blob/wiki/Manually-Link-Package#index). + **Automatically Link Native Modules** -For 0.29.2+ projects, simply link native packages via the following command (note: rnpm has been merged into react-native) +For projects 0.29.2 < 0.69.0 , simply link native packages via the following command (note: rnpm has been merged into react-native) ``` -react-native link +react-native link rn-fetch-blob ``` +This link command no longer works for react-native 0.69.0+ + As for projects < 0.29 you need `rnpm` to link native packages ```sh rnpm link ``` -Optionally, use the following command to add Android permissions to `AndroidManifest.xml` automatically +You also might need add the following lines to `AndroidManifest.xml` -```sh -RNFB_ANDROID_PERMISSIONS=true react-native link +```diff + + + ++ + ... + + ++ + ... ``` -pre 0.29 projects +For projects < 0.28 -```sh -RNFB_ANDROID_PERMISSIONS=true rnpm link +React Native has changed OkHttp version in 0.27, if your project is older than 0.28 you have to explicitly specify OkHttp version in `node_modules/rn-fetch-blob/android/build.gradle` + +```diff +dependencies { + implementation 'com.facebook.react:react-native:+' ++ implementation 'com.squareup.okhttp3:okhttp:3.4.1' +- //{RNFetchBlob_PRE_0.28_DEPDENDENCY} +} ``` -The link script might not take effect if you have non-default project structure, please visit [the wiki](https://github.com/wkh237/react-native-fetch-blob/wiki/Manually-Link-Package) to link the package manually. +The link script might not take effect if you have non-default project structure, please visit [the wiki](https://github.com/joltup/rn-fetch-blob/wiki/Manually-Link-Package) to link the package manually. **Grant Permission to External storage for Android 5.0 or lower** @@ -109,9 +137,9 @@ If you're going to access external storage (say, SD card storage) for `Android 5 -+ -+ - ++ ++ ++ ... ``` @@ -122,10 +150,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). @@ -137,23 +173,22 @@ ES6 The module uses ES6 style export statement, simply use `import` to load the module. ```js -import RNFetchBlob from 'react-native-fetch-blob' +import RNFetchBlob from "rn-fetch-blob"; ``` ES5 -If you're using ES5 require statement to load the module, please add `default`. See [here](https://github.com/wkh237/react-native-fetch-blob/wiki/Trouble-Shooting#rnfetchblobfetch-is-not-a-function) for more detail. +If you're using ES5 require statement to load the module, please add `default`. See [here](https://github.com/joltup/rn-fetch-blob/wiki/Trouble-Shooting#rnfetchblobfetch-is-not-a-function) for more detail. ``` -var RNFetchBlob = require('react-native-fetch-blob').default +var RNFetchBlob = require('rn-fetch-blob').default ``` ## HTTP Data Transfer - ### Regular Request -After `0.8.0` react-native-fetch-blob automatically decides how to send the body by checking its type and `Content-Type` in the header. The rule is described in the following diagram +After `0.8.0` rn-fetch-blob automatically decides how to send the body by checking its type and `Content-Type` in the header. The rule is described in the following diagram @@ -161,8 +196,8 @@ 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. - - 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. +- 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`. > It is Worth to mentioning that the HTTP request uses cache by default, if you're going to disable it simply add a Cache-Control header `'Cache-Control' : 'no-store'` @@ -171,28 +206,31 @@ To sum up: ### Download example: Fetch files that need authorization token -Most simple way is download to memory and stored as BASE64 encoded string, this is handy when the response data is small. +Most simple way is download to memory and stored as BASE64 encoded string, this is handy when the response data is small. Note that when it comes to authorization, not only can you use an authorization token, but this package will automatically pass the cookies created by normal js requests such as axios and fetch. Therefore, if you are using traditional cookie-based ways to authorize your user, you don't need to do anything before this package works. ```js - // send http request in a new thread (using native code) -RNFetchBlob.fetch('GET', 'http://www.example.com/images/img1.png', { - Authorization : 'Bearer access-token...', - // more headers .. - }) - // when response status code is 200 +RNFetchBlob.fetch("GET", "http://www.example.com/images/img1.png", { + Authorization: "Bearer access-token...", + // more headers .. +}) .then((res) => { - // the conversion is done in native code - let base64Str = res.base64() - // the following conversions are done in js, it's SYNC - let text = res.text() - let json = res.json() - + let status = res.info().status; + + if (status == 200) { + // the conversion is done in native code + let base64Str = res.base64(); + // the following conversions are done in js, it's SYNC + let text = res.text(); + let json = res.json(); + } else { + // handle other status codes + } }) - // Status code is not 200 + // Something went wrong: .catch((errorMessage, statusCode) => { // error handling - }) + }); ``` ### Download to storage directly @@ -202,19 +240,18 @@ If the response data is large, that would be a bad idea to convert it into BASE6 **These files won't be removed automatically, please refer to [Cache File Management](#user-content-cache-file-management)** ```js -RNFetchBlob - .config({ - // add this option that makes response data to be stored as a file, - // this is much more performant. - fileCache : true, - }) - .fetch('GET', 'http://www.example.com/file/example.zip', { +RNFetchBlob.config({ + // add this option that makes response data to be stored as a file, + // this is much more performant. + fileCache: true, +}) + .fetch("GET", "http://www.example.com/file/example.zip", { //some headers .. }) .then((res) => { // the temp file path - console.log('The file saved to ', res.path()) - }) + console.log("The file saved to ", res.path()); + }); ``` **Set Temp File Extension** @@ -222,71 +259,82 @@ RNFetchBlob Sometimes you might need a file extension for some reason. For example, when using file path as the source of `Image` component, the path should end with something like .png or .jpg, you can do this by add `appendExt` option to `config`. ```js -RNFetchBlob - .config({ - fileCache : true, - // by adding this option, the temp files will have a file extension - appendExt : 'png' - }) - .fetch('GET', 'http://www.example.com/file/example.zip', { +RNFetchBlob.config({ + fileCache: true, + // by adding this option, the temp files will have a file extension + appendExt: "png", +}) + .fetch("GET", "http://www.example.com/file/example.zip", { //some headers .. }) .then((res) => { // the temp file path with file extension `png` - console.log('The file saved to ', res.path()) + console.log("The file saved to ", res.path()); // Beware that when using a file path as Image source on Android, // you must prepend "file://"" before the file path - imageView = - }) + imageView = ( + + ); + }); ``` **Use Specific File Path** -If you prefer a particular file path rather than randomly generated one, you can use `path` option. We've added [several constants](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#dirs) in v0.5.0 which represents commonly used directories. +If you prefer a particular file path rather than randomly generated one, you can use `path` option. We've added [several constants](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#dirs) in v0.5.0 which represents commonly used directories. ```js -let dirs = RNFetchBlob.fs.dirs -RNFetchBlob -.config({ +let dirs = RNFetchBlob.fs.dirs; +RNFetchBlob.config({ // response data will be saved to this path if it has access right. - path : dirs.DocumentDir + '/path-to-file.anything' -}) -.fetch('GET', 'http://www.example.com/file/example.zip', { - //some headers .. -}) -.then((res) => { - // the path should be dirs.DocumentDir + 'path-to-file.anything' - console.log('The file saved to ', res.path()) + path: dirs.DocumentDir + "/path-to-file.anything", }) + .fetch("GET", "http://www.example.com/file/example.zip", { + //some headers .. + }) + .then((res) => { + // the path should be dirs.DocumentDir + 'path-to-file.anything' + console.log("The file saved to ", res.path()); + }); ``` **These files won't be removed automatically, please refer to [Cache File Management](#user-content-cache-file-management)** -#### Upload example : Dropbox [files-upload](https://www.dropbox.com/developers/documentation/http/documentation#files-upload) API +#### Upload example : Dropbox [files-upload](https://www.dropbox.com/developers/documentation/http/documentation#files-upload) API -`react-native-fetch-blob` will convert the base64 string in `body` to binary format using native API, this process is done in a separated thread so that it won't block your GUI. +`rn-fetch-blob` will convert the base64 string in `body` to binary format using native API, this process is done in a separated thread so that it won't block your GUI. ```js - -RNFetchBlob.fetch('POST', 'https://content.dropboxapi.com/2/files/upload', { - Authorization : "Bearer access-token...", - 'Dropbox-API-Arg': JSON.stringify({ - path : '/img-from-react-native.png', - mode : 'add', - autorename : true, - mute : false +RNFetchBlob.fetch( + "POST", + "https://content.dropboxapi.com/2/files/upload", + { + Authorization: "Bearer access-token...", + "Dropbox-API-Arg": JSON.stringify({ + path: "/img-from-react-native.png", + mode: "add", + autorename: true, + mute: false, }), - 'Content-Type' : 'application/octet-stream', + "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. - }, base64ImageString) + // The data will be converted to "byte array"(say, blob) before request sent. + }, + base64ImageString +) .then((res) => { - console.log(res.text()) + console.log(res.text()); }) .catch((err) => { // error handling .. - }) + }); ``` ### Upload a file from storage @@ -294,25 +342,30 @@ RNFetchBlob.fetch('POST', 'https://content.dropboxapi.com/2/files/upload', { If you're going to use a `file` as request body, just wrap the path with `wrap` API. ```js -RNFetchBlob.fetch('POST', 'https://content.dropboxapi.com/2/files/upload', { +RNFetchBlob.fetch( + "POST", + "https://content.dropboxapi.com/2/files/upload", + { // dropbox upload headers - Authorization : "Bearer access-token...", - 'Dropbox-API-Arg': JSON.stringify({ - path : '/img-from-react-native.png', - mode : 'add', - autorename : true, - mute : false + Authorization: "Bearer access-token...", + "Dropbox-API-Arg": JSON.stringify({ + path: "/img-from-react-native.png", + mode: "add", + autorename: true, + mute: false, }), - 'Content-Type' : 'application/octet-stream', + "Content-Type": "application/octet-stream", // Change BASE64 encoded data to a file path with prefix `RNFetchBlob-file://`. // Or simply wrap the file path with RNFetchBlob.wrap(). - }, RNFetchBlob.wrap(PATH_TO_THE_FILE)) + }, + RNFetchBlob.wrap(PATH_TO_THE_FILE) +) .then((res) => { - console.log(res.text()) + console.log(res.text()); }) .catch((err) => { // error handling .. - }) + }); ``` ### Multipart/form-data example: Post form data with file and data @@ -322,32 +375,51 @@ In `version >= 0.3.0` you can also post files with form data, just put an array Elements have property `filename` will be transformed into binary format, otherwise, it turns into utf8 string. ```js - - RNFetchBlob.fetch('POST', 'http://www.example.com/upload-form', { - Authorization : "Bearer access-token", - otherHeader : "foo", - 'Content-Type' : 'multipart/form-data', - }, [ +RNFetchBlob.fetch( + "POST", + "http://www.example.com/upload-form", + { + Authorization: "Bearer access-token", + otherHeader: "foo", + "Content-Type": "multipart/form-data", + }, + [ // element with property `filename` will be transformed into `file` in form data - { name : 'avatar', filename : 'avatar.png', data: binaryDataInBase64}, + { name: "avatar", filename: "avatar.png", data: binaryDataInBase64 }, // custom content type - { name : 'avatar-png', filename : 'avatar-png.png', type:'image/png', data: binaryDataInBase64}, + { + name: "avatar-png", + filename: "avatar-png.png", + type: "image/png", + data: binaryDataInBase64, + }, // part file from storage - { name : 'avatar-foo', filename : 'avatar-foo.png', type:'image/foo', data: RNFetchBlob.wrap(path_to_a_file)}, + { + name: "avatar-foo", + filename: "avatar-foo.png", + type: "image/foo", + data: RNFetchBlob.wrap(path_to_a_file), + }, // elements without property `filename` will be sent as plain text - { name : 'name', data : 'user'}, - { name : 'info', data : JSON.stringify({ - mail : 'example@example.com', - tel : '12345678' - })}, - ]).then((resp) => { - // ... - }).catch((err) => { + { name: "name", data: "user" }, + { + name: "info", + data: JSON.stringify({ + mail: "example@example.com", + tel: "12345678", + }), + }, + ] +) + .then((resp) => { // ... }) + .catch((err) => { + // ... + }); ``` -What if you want to append a file to form data? Just like [upload a file from storage](#user-content-upload-a-file-from-storage) example, wrap `data` by `wrap` API (this feature is only available for `version >= v0.5.0`). On version >= `0.6.2`, it is possible to set custom MIME type when appending a file to form data. But keep in mind when the file is large it's likely to crash your app. Please consider use other strategy (see [#94](https://github.com/wkh237/react-native-fetch-blob/issues/94)). +What if you want to append a file to form data? Just like [upload a file from storage](#user-content-upload-a-file-from-storage) example, wrap `data` by `wrap` API (this feature is only available for `version >= v0.5.0`). On version >= `0.6.2`, it is possible to set custom MIME type when appending a file to form data. But keep in mind when the file is large it's likely to crash your app. Please consider use other strategy (see [#94](https://github.com/joltup/rn-fetch-blob/issues/94)). ```js @@ -391,48 +463,57 @@ What if you want to append a file to form data? Just like [upload a file from st In `version >= 0.4.2` it is possible to know the upload/download progress. After `0.7.0` IOS and Android upload progress are also supported. ```js - RNFetchBlob.fetch('POST', 'http://www.example.com/upload', { - //... some headers, - 'Content-Type' : 'octet-stream' - }, base64DataString) - // listen to upload progress event - .uploadProgress((written, total) => { - console.log('uploaded', written / total) - }) - // listen to download progress event - .progress((received, total) => { - console.log('progress', received / total) - }) - .then((resp) => { - // ... - }) - .catch((err) => { - // ... - }) +RNFetchBlob.fetch( + "POST", + "http://www.example.com/upload", + { + //... some headers, + "Content-Type": "octet-stream", + }, + base64DataString +) + // listen to upload progress event + .uploadProgress((written, total) => { + console.log("uploaded", written / total); + }) + // listen to download progress event + .progress((received, total) => { + console.log("progress", received / total); + }) + .then((resp) => { + // ... + }) + .catch((err) => { + // ... + }); ``` -In `0.9.6`, you can specify an object as the first argument which contains `count` and `interval`, to the frequency of progress event (this will be done in the native context a reduce RCT bridge overhead). Notice that `count` argument will not work if the server does not provide response content length. - +In `0.9.6`, you can specify an object as the first argument which contains `count` and `interval`, to the frequency of progress event (this will be done in the native context a reduce RCT bridge overhead). Notice that `count` argument will not work if the server does not provide response content length. ```js - RNFetchBlob.fetch('POST', 'http://www.example.com/upload', { - //... some headers, - 'Content-Type' : 'octet-stream' - }, base64DataString) - // listen to upload progress event, emit every 250ms - .uploadProgress({ interval : 250 },(written, total) => { - console.log('uploaded', written / total) - }) - // listen to download progress event, every 10% - .progress({ count : 10 }, (received, total) => { - console.log('progress', received / total) - }) - .then((resp) => { - // ... - }) - .catch((err) => { - // ... - }) +RNFetchBlob.fetch( + "POST", + "http://www.example.com/upload", + { + //... some headers, + "Content-Type": "octet-stream", + }, + base64DataString +) + // listen to upload progress event, emit every 250ms + .uploadProgress({ interval: 250 }, (written, total) => { + console.log("uploaded", written / total); + }) + // listen to download progress event, every 10% + .progress({ count: 10 }, (received, total) => { + console.log("progress", received / total); + }) + .then((resp) => { + // ... + }) + .catch((err) => { + // ... + }); ``` ### Cancel Request @@ -458,7 +539,7 @@ task.cancel((err) => { ... }) 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) +[See document and examples](https://github.com/joltup/rn-fetch-blob/wiki/Fetch-API#fetch-replacement) ### Android Media Scanner, and Download Manager Support @@ -469,20 +550,20 @@ If you want to make a file in `External Storage` becomes visible in Picture, Dow Media scanner scans the file and categorizes by given MIME type, if MIME type not specified, it will try to resolve the file using its file extension. ```js - -RNFetchBlob - .config({ - // DCIMDir is in external storage - path : dirs.DCIMDir + '/music.mp3' - }) - .fetch('GET', 'http://example.com/music.mp3') - .then((res) => RNFetchBlob.fs.scanFile([ { path : res.path(), mime : 'audio/mpeg' } ])) - .then(() => { - // scan file success - }) - .catch((err) => { - // scan file error - }) +RNFetchBlob.config({ + // DCIMDir is in external storage + path: dirs.DCIMDir + "/music.mp3", +}) + .fetch("GET", "http://example.com/music.mp3") + .then((res) => + RNFetchBlob.fs.scanFile([{ path: res.path(), mime: "audio/mpeg" }]) + ) + .then(() => { + // scan file success + }) + .catch((err) => { + // scan file error + }); ``` **Download Manager** @@ -496,23 +577,22 @@ When using DownloadManager, `fileCache` and `path` properties in `config` will n When download complete, DownloadManager will generate a file path so that you can deal with it. ```js -RNFetchBlob - .config({ - addAndroidDownloads : { - useDownloadManager : true, // <-- this is the only thing required - // Optional, override notification setting (default to true) - notification : false, - // Optional, but recommended since android DownloadManager will fail when - // the url does not contains a file extension, by default the mime type will be text/plain - mime : 'text/plain', - description : 'File downloaded by download manager.' - } - }) - .fetch('GET', 'http://example.com/file/somefile') - .then((resp) => { - // the path of downloaded file - resp.path() - }) +RNFetchBlob.config({ + addAndroidDownloads: { + useDownloadManager: true, // <-- this is the only thing required + // Optional, override notification setting (default to true) + notification: false, + // Optional, but recommended since android DownloadManager will fail when + // the url does not contains a file extension, by default the mime type will be text/plain + mime: "text/plain", + description: "File downloaded by download manager.", + }, +}) + .fetch("GET", "http://example.com/file/somefile") + .then((resp) => { + // the path of downloaded file + resp.path(); + }); ``` Your app might not have right to remove/change the file created by Download Manager, therefore you might need to [set custom location to the download task](https://github.com/wkh237/react-native-fetch-blob/issues/236). @@ -522,7 +602,6 @@ Your app might not have right to remove/change the file created by Download Mana - If you need to display a notification upon the file is downloaded to storage (as the above) or make the downloaded file visible in "Downloads" app. You have to add some options to `config`. ```js @@ -552,29 +631,31 @@ This is a new feature added in `0.9.0` if you're going to open a file path using Download and install an APK programmatically ```js - -const android = RNFetchBlob.android +const android = RNFetchBlob.android; RNFetchBlob.config({ - addAndroidDownloads : { - useDownloadManager : true, - title : 'awesome.apk', - description : 'An APK that will be installed', - mime : 'application/vnd.android.package-archive', - mediaScannable : true, - notification : true, - } - }) - .fetch('GET', `http://www.example.com/awesome.apk`) + addAndroidDownloads: { + useDownloadManager: true, + title: "awesome.apk", + description: "An APK that will be installed", + mime: "application/vnd.android.package-archive", + mediaScannable: true, + notification: true, + }, +}) + .fetch("GET", `http://www.example.com/awesome.apk`) .then((res) => { - android.actionViewIntent(res.path(), 'application/vnd.android.package-archive') - }) + android.actionViewIntent( + res.path(), + "application/vnd.android.package-archive" + ); + }); ``` Or show an image in image viewer ```js - android.actionViewIntent(PATH_OF_IMG, 'image/png') +android.actionViewIntent(PATH_OF_IMG, "image/png"); ``` ## File System @@ -583,35 +664,36 @@ Or show an image in image viewer File access APIs were made when developing `v0.5.0`, which helping us write tests, and was not planned to be a part of this module. However, we realized that it's hard to find a great solution to manage cached files, everyone who uses this module may need these APIs for their cases. -Before start using file APIs, we recommend read [Differences between File Source](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#differences-between-file-source) first. +Before start using file APIs, we recommend read [Differences between File Source](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#differences-between-file-source) first. File Access APIs -- [asset (0.6.2)](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#assetfilenamestringstring) -- [dirs](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#dirs) -- [createFile](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#createfilepath-data-encodingpromise) -- [writeFile (0.6.0)](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#writefilepathstring-contentstring--array-encodingstring-appendbooleanpromise) -- [appendFile (0.6.0) ](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#appendfilepathstring-contentstring--arraynumber-encodingstring-promisenumber) -- [readFile (0.6.0)](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#readfilepath-encodingpromise) -- [readStream](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#readstreampath-encoding-buffersize-interval-promisernfbreadstream) -- [hash (0.10.9)](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#hashpath-algorithm-promise) -- [writeStream](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#writestreampathstring-encodingstringpromise) -- [hash](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#hashpath-algorithmpromise) -- [unlink](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#unlinkpathstringpromise) -- [mkdir](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#mkdirpathstringpromise) -- [ls](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#lspathstringpromise) -- [mv](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#mvfromstring-tostringpromise) -- [cp](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#cpsrcstring-deststringpromise) -- [exists](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#existspathstringpromise) -- [isDir](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#isdirpathstringpromise) -- [stat](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#statpathstringpromise) -- [lstat](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#lstatpathstringpromise) -- [scanFile (Android only)](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API#scanfilepathstringpromise-androi-only) - -See [File API](https://github.com/wkh237/react-native-fetch-blob/wiki/File-System-Access-API) for more information + +- [asset (0.6.2)](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#assetfilenamestringstring) +- [dirs](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#dirs) +- [createFile](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#createfilepath-data-encodingpromise) +- [writeFile (0.6.0)](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#writefilepathstring-contentstring--array-encodingstring-appendbooleanpromise) +- [appendFile (0.6.0) ](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#appendfilepathstring-contentstring--arraynumber-encodingstring-promisenumber) +- [readFile (0.6.0)](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#readfilepath-encodingpromise) +- [readStream](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#readstreampath-encoding-buffersize-interval-promisernfbreadstream) +- [hash (0.10.9)](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#hashpath-algorithm-promise) +- [writeStream](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#writestreampathstring-encodingstringpromise) +- [hash](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#hashpath-algorithmpromise) +- [unlink](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#unlinkpathstringpromise) +- [mkdir](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#mkdirpathstringpromise) +- [ls](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#lspathstringpromise) +- [mv](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#mvfromstring-tostringpromise) +- [cp](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#cpsrcstring-deststringpromise) +- [exists](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#existspathstringpromise) +- [isDir](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#isdirpathstringpromise) +- [stat](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#statpathstringpromise) +- [lstat](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#lstatpathstringpromise) +- [scanFile (Android only)](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API#scanfilepathstringpromise-androi-only) + +See [File API](https://github.com/joltup/rn-fetch-blob/wiki/File-System-Access-API) for more information ### File Stream -In `v0.5.0` we've added `writeStream` and `readStream`, which allows your app read/write data from the file path. This API creates a file stream, rather than convert entire data into BASE64 encoded string. It's handy when processing **large files**. +In `v0.5.0` we've added `writeStream` and `readStream`, which allows your app read/write data from the file path. This API creates a file stream, rather than convert entire data into BASE64 encoded string. It's handy when processing **large files**. 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)) @@ -637,7 +719,7 @@ RNFetchBlob.fs.readStream( ifstream.onError((err) => { console.log('oops', err) }) - ifstream.onEnd(() => { + ifstream.onEnd(() => { ofstream.write('foo')) -.then(ofstream => ofstream.write('bar')) -.then(ofstream => ofstream.write('foobar')) -.then(ofstream => ofstream.close()) -.catch(console.error) + ) + .then((ofstream) => ofstream.write("foo")) + .then((ofstream) => ofstream.write("bar")) + .then((ofstream) => ofstream.write("foobar")) + .then((ofstream) => ofstream.close()) + .catch(console.error); ``` -or +or ```js -RNFetchBlob.fs.writeStream( +RNFetchBlob.fs + .writeStream( PATH_TO_FILE, // encoding, should be one of `base64`, `utf8`, `ascii` - 'utf8', + "utf8", // should data append to existing content ? true -) -.then(stream => Promise.all([ - stream.write('foo'), - stream.write('bar'), - stream.write('foobar') -])) -// Use array destructuring to get the stream object from the first item of the array we get from Promise.all() -.then(([stream]) => stream.close()) -.catch(console.error) + ) + .then((stream) => + Promise.all([ + stream.write("foo"), + stream.write("bar"), + stream.write("foobar"), + ]) + ) + // Use array destructuring to get the stream object from the first item of the array we get from Promise.all() + .then(([stream]) => stream.close()) + .catch(console.error); ``` You should **NOT** do something like this: ```js -RNFetchBlob.fs.writeStream( +RNFetchBlob.fs + .writeStream( PATH_TO_FILE, // encoding, should be one of `base64`, `utf8`, `ascii` - 'utf8', + "utf8", // should data append to existing content ? - true) -.then((ofstream) => { + true + ) + .then((ofstream) => { // BAD IDEA - Don't do this, those writes are unchecked: - ofstream.write('foo') - ofstream.write('bar') - ofstream.close() -}) -.catch(console.error) // Cannot catch any write() errors! + ofstream.write("foo"); + ofstream.write("bar"); + ofstream.close(); + }) + .catch(console.error); // Cannot catch any write() errors! ``` The problem with the above code is that the promises from the `ofstream.write()` calls are detached and "Lost". @@ -709,22 +797,20 @@ That code may _seem_ to work if there are no errors, but those writes are of the When using `fileCache` or `path` options along with `fetch` API, response data will automatically store into the file system. The files will **NOT** removed unless you `unlink` it. There're several ways to remove the files ```js +// remove file using RNFetchblobResponse.flush() object method +RNFetchblob.config({ + fileCache: true, +}) + .fetch("GET", "http://example.com/download/file") + .then((res) => { + // remove cached file from storage + res.flush(); + }); - // remove file using RNFetchblobResponse.flush() object method - RNFetchblob.config({ - fileCache : true - }) - .fetch('GET', 'http://example.com/download/file') - .then((res) => { - // remove cached file from storage - res.flush() - }) - - // remove file by specifying a path - RNFetchBlob.fs.unlink('some-file-path').then(() => { - // ... - }) - +// remove file by specifying a path +RNFetchBlob.fs.unlink("some-file-path").then(() => { + // ... +}); ``` You can also group requests by using `session` API and use `dispose` to remove them all when needed. @@ -738,7 +824,7 @@ You can also group requests by using `session` API and use `dispose` to remove t .then((res) => { // set session of a response res.session('foo') - }) + }) RNFetchblob.config({ // you can also set session beforehand @@ -748,7 +834,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') @@ -766,37 +852,58 @@ You can also group requests by using `session` API and use `dispose` to remove t After `0.9.4`, the `Chunked` transfer encoding is disabled by default due to some service provider may not support chunked transfer. To enable it, set `Transfer-Encoding` header to `Chunked`. ```js -RNFetchBlob.fetch('POST', 'http://example.com/upload', { 'Transfer-Encoding' : 'Chunked' }, bodyData) +RNFetchBlob.fetch( + "POST", + "http://example.com/upload", + { "Transfer-Encoding": "Chunked" }, + bodyData +); ``` ### Self-Signed SSL Server -By default, react-native-fetch-blob does NOT allow connection to unknown certification provider since it's dangerous. To connect a server with self-signed certification, you need to add `trusty` to `config` explicitly. This function is available for version >= `0.5.3` +By default, rn-fetch-blob does NOT allow connection to unknown certification provider since it's dangerous. To connect a server with self-signed certification, you need to add `trusty` to `config` explicitly. This function is available for version >= `0.5.3` ```js RNFetchBlob.config({ - trusty : true + trusty: true, }) -.then('GET', 'https://mysite.com') -.then((resp) => { - // ... + .fetch("GET", "https://mysite.com") + .then((resp) => { + // ... + }); +``` + +### 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/wkh237/react-native-fetch-blob/wiki/Web-API-Polyfills-(experimental)) that makes some browser-based library available in RN. +After `0.8.0` we've made some [Web API polyfills]() that makes some browser-based library available in RN. - Blob - XMLHttpRequest (Use our implementation if you're going to use it with Blob) -Here's a [sample app](https://github.com/wkh237/rn-firebase-storage-upload-sample) that uses polyfills to upload files to FireBase. +Here's a [sample app](https://github.com/joltup/rn-firebase-storage-upload-sample) that uses polyfills to upload files to FireBase. ## Performance Tips **Read Stream and Progress Event Overhead** -If the process seems to block JS thread when file is large when reading data via `fs.readStream`. It might because the default buffer size is quite small (4kb) which result in a lot of events triggered from JS thread. Try to increase the buffer size (for example 100kb = 102400) and set a larger interval (available for 0.9.4+, the default value is 10ms) to limit the frequency. +If the process seems to block JS thread when file is large when reading data via `fs.readStream`. It might because the default buffer size is quite small (4kb) which result in a lot of events triggered from JS thread. Try to increase the buffer size (for example 100kb = 102400) and set a larger interval (available for 0.9.4+, the default value is 10ms) to limit the frequency. **Reduce RCT Bridge and BASE64 Overhead** @@ -818,19 +925,19 @@ If you're going to concatenate files, you don't have to read the data to JS cont ## Caveats -* This library does not urlencode unicode characters in URL automatically, see [#146](https://github.com/wkh237/react-native-fetch-blob/issues/146). -* When you create a `Blob` , from an existing file, the file **WILL BE REMOVED** if you `close` the blob. -* If you replaced `window.XMLHttpRequest` for some reason (e.g. make Firebase SDK work), it will also affect how official `fetch` works (basically it should work just fine). -* When file stream and upload/download progress event slow down your app, consider an upgrade to `0.9.6+`, use [additional arguments](https://github.com/wkh237/react-native-fetch-blob/wiki/Fetch-API#fetchprogressconfig-eventlistenerpromisernfetchblobresponse) to limit its frequency. -* When passing a file path to the library, remove `file://` prefix. +- This library does not urlencode unicode characters in URL automatically, see [#146](https://github.com/wkh237/react-native-fetch-blob/issues/146). +- When you create a `Blob` , from an existing file, the file **WILL BE REMOVED** if you `close` the blob. +- If you replaced `window.XMLHttpRequest` for some reason (e.g. make Firebase SDK work), it will also affect how official `fetch` works (basically it should work just fine). +- When file stream and upload/download progress event slow down your app, consider an upgrade to `0.9.6+`, use [additional arguments](https://github.com/joltup/rn-fetch-blob/wiki/Fetch-API#fetchprogressconfig-eventlistenerpromisernfetchblobresponse) to limit its frequency. +- When passing a file path to the library, remove `file://` prefix. -when you got a problem, have a look at [Trouble Shooting](https://github.com/wkh237/react-native-fetch-blob/wiki/Trouble-Shooting) or [issues labeled Trouble Shooting](https://github.com/wkh237/react-native-fetch-blob/issues?utf8=✓&q=label:%22trouble%20shooting%22%20), there'd be some helpful information. +when you got a problem, have a look at [Trouble Shooting](https://github.com/joltup/rn-fetch-blob/wiki/Trouble-Shooting) or [issues labeled Trouble Shooting](https://github.com/joltup/rn-fetch-blob/issues?utf8=✓&q=label:%22trouble%20shooting%22%20), there'd be some helpful information. ## Changes -See [release notes](https://github.com/wkh237/react-native-fetch-blob/releases) +See [release notes](https://github.com/joltup/rn-fetch-blob/releases) ### Development -If you're interested in hacking this module, check our [development guide](https://github.com/wkh237/react-native-fetch-blob/wiki/Home), there might be some helpful information. +If you're interested in hacking this module, check our [development guide](https://github.com/joltup/rn-fetch-blob/wiki/Home), there might be some helpful information. Please feel free to make a PR or file an issue. diff --git a/android.js b/android.js index 80c4e4a96..3e4faa472 100644 --- a/android.js +++ b/android.js @@ -38,9 +38,25 @@ function addCompleteDownload(config) { return Promise.reject('RNFetchBlob.android.addCompleteDownload only supports Android.') } +function getSDCardDir() { + if(Platform.OS === 'android') + return RNFetchBlob.getSDCardDir() + else + return Promise.reject('RNFetchBlob.android.getSDCardDir only supports Android.') +} + +function getSDCardApplicationDir() { + if(Platform.OS === 'android') + return RNFetchBlob.getSDCardApplicationDir() + else + return Promise.reject('RNFetchBlob.android.getSDCardApplicationDir only supports Android.') +} + export default { actionViewIntent, getContentIntent, - addCompleteDownload + addCompleteDownload, + getSDCardDir, + getSDCardApplicationDir, } diff --git a/android/.project b/android/.project new file mode 100644 index 000000000..5c0128a89 --- /dev/null +++ b/android/.project @@ -0,0 +1,28 @@ + + + android + Project android created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + + + 1634215444278 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/android/.settings/org.eclipse.buildship.core.prefs b/android/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 000000000..98515123a --- /dev/null +++ b/android/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,13 @@ +arguments= +auto.sync=false +build.scans.enabled=false +connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) +connection.project.dir= +eclipse.preferences.version=1 +gradle.user.home= +java.home=/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home +jvm.arguments= +offline.mode=false +override.workspace.settings=true +show.console.view=true +show.executions.view=true diff --git a/android/build.gradle b/android/build.gradle index d68693bf1..893f9cd08 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,24 +1,30 @@ apply plugin: 'com.android.library' +def safeExtGet(prop, fallback) { + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback +} + repositories { mavenCentral() + google() } buildscript { repositories { - jcenter() + mavenCentral() + google() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.3' + classpath 'com.android.tools.build:gradle:4.0.1' } } android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" + compileSdkVersion safeExtGet('compileSdkVersion', 30) + buildToolsVersion safeExtGet('buildToolsVersion', '28.0.3') defaultConfig { - minSdkVersion 16 - targetSdkVersion 23 + minSdkVersion safeExtGet('minSdkVersion', 16) + targetSdkVersion safeExtGet('targetSdkVersion', 30) versionCode 1 versionName "1.0" } @@ -33,7 +39,9 @@ android { } dependencies { - compile 'com.facebook.react:react-native:+' - //compile 'com.squareup.okhttp3:okhttp:+' - //{RNFetchBlob_PRE_0.28_DEPDENDENCY} + implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}" + implementation "com.squareup.okhttp3:okhttp:${safeExtGet('okhttp', '+')}" + implementation "com.squareup.okhttp3:logging-interceptor:${safeExtGet('okhttp', '+')}" + implementation "com.squareup.okhttp3:okhttp-urlconnection:${safeExtGet('okhttp', '+')}" +// {RNFetchBlob_PRE_0.28_DEPDENDENCY} } diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 000000000..5465fec0e --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,2 @@ +android.enableJetifier=true +android.useAndroidX=true \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index c60fdf0b6..ecceb2dea 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Aug 12 07:48:35 CEST 2017 +#Fri Aug 07 22:58:34 IST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 807904417..1c92420b8 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,9 +1,37 @@ - + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlob.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlob.java index c4980d445..fb73d1a89 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlob.java +++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlob.java @@ -3,7 +3,10 @@ import android.app.Activity; import android.app.DownloadManager; import android.content.Intent; +import android.content.pm.PackageManager; import android.net.Uri; +import android.os.Build; +import androidx.core.content.FileProvider; import android.util.SparseArray; import com.facebook.react.bridge.ActivityEventListener; @@ -21,10 +24,11 @@ import com.facebook.react.modules.network.ForwardingCookieHandler; import com.facebook.react.modules.network.CookieJarContainer; import com.facebook.react.modules.network.OkHttpClientProvider; -import okhttp3.OkHttpClient; + import okhttp3.JavaNetCookieJar; +import okhttp3.OkHttpClient; -import java.util.HashMap; +import java.io.File; import java.util.Map; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -100,16 +104,31 @@ public void run() { RNFetchBlobFS.createFileASCII(path, dataArray, promise); } }); - } @ReactMethod public void actionViewIntent(String path, String mime, final Promise promise) { try { - Intent intent= new Intent(Intent.ACTION_VIEW) - .setDataAndType(Uri.parse("file://" + path), mime); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - this.getReactApplicationContext().startActivity(intent); + Uri uriForFile = FileProvider.getUriForFile(this.getReactApplicationContext(), + this.getReactApplicationContext().getPackageName() + ".provider", new File(path)); + + // Create the intent with data and type + Intent intent = new Intent(Intent.ACTION_VIEW) + .setDataAndType(uriForFile, mime); + + // Set flag to give temporary permission to external app to use FileProvider + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + // All the activity to be opened outside of an activity + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // Validate that the device can open the file + PackageManager pm = getCurrentActivity().getPackageManager(); + if (intent.resolveActivity(pm) != null) { + this.getReactApplicationContext().startActivity(intent); + } else { + promise.reject("EUNSPECIFIED", "Cannot open the URL."); + } + ActionViewVisible = true; final LifecycleEventListener listener = new LifecycleEventListener() { @@ -164,7 +183,6 @@ public void run() { RNFetchBlobFS.cp(path, dest, callback); } }); - } @ReactMethod @@ -225,7 +243,6 @@ public void run() { RNFetchBlobFS.writeFile(path, encoding, data, append, promise); } }); - } @ReactMethod @@ -260,7 +277,6 @@ public void run() { new RNFetchBlobFS(ctx).scanFile(p, m, callback); } }); - } @ReactMethod @@ -316,7 +332,7 @@ public void df(final Callback callback) { fsThreadPool.execute(new Runnable() { @Override public void run() { - RNFetchBlobFS.df(callback); + RNFetchBlobFS.df(callback, getReactApplicationContext()); } }); } @@ -331,7 +347,7 @@ public void enableUploadProgressReport(String taskId, int interval, int count) { @ReactMethod public void fetchBlob(ReadableMap options, String taskId, String method, String url, ReadableMap headers, String body, final Callback callback) { new RNFetchBlobReq(options, taskId, method, url, headers, body, null, mClient, callback).run(); -} + } @ReactMethod public void fetchBlobForm(ReadableMap options, String taskId, String method, String url, ReadableMap headers, ReadableArray body, final Callback callback) { @@ -353,6 +369,11 @@ public void getContentIntent(String mime, Promise promise) { @ReactMethod public void addCompleteDownload (ReadableMap config, Promise promise) { DownloadManager dm = (DownloadManager) RCTContext.getSystemService(RCTContext.DOWNLOAD_SERVICE); + if (config == null || !config.hasKey("path")) + { + promise.reject("EINVAL", "RNFetchblob.addCompleteDownload config or path missing."); + return; + } String path = RNFetchBlobFS.normalizePath(config.getString("path")); if(path == null) { promise.reject("EINVAL", "RNFetchblob.addCompleteDownload can not resolve URI:" + config.getString("path")); @@ -377,4 +398,13 @@ public void addCompleteDownload (ReadableMap config, Promise promise) { } + @ReactMethod + public void getSDCardDir(Promise promise) { + RNFetchBlobFS.getSDCardDir(this.getReactApplicationContext(), promise); + } + + @ReactMethod + public void getSDCardApplicationDir(Promise promise) { + RNFetchBlobFS.getSDCardApplicationDir(this.getReactApplicationContext(), promise); + } } diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobBody.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobBody.java index 15045cffa..adbe48b72 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobBody.java +++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobBody.java @@ -1,6 +1,6 @@ package com.RNFetchBlob; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import android.util.Base64; import com.facebook.react.bridge.Arguments; @@ -18,6 +18,7 @@ import java.io.InputStream; import java.util.ArrayList; +import android.net.Uri; import okhttp3.MediaType; import okhttp3.RequestBody; import okio.BufferedSink; @@ -68,7 +69,7 @@ RNFetchBlobBody setBody(String body) { try { switch (requestType) { case SingleFile: - requestStream = getReuqestStream(); + requestStream = getRequestStream(); contentLength = requestStream.available(); break; case AsIs: @@ -135,7 +136,7 @@ boolean clearRequestBody() { return true; } - private InputStream getReuqestStream() throws Exception { + private InputStream getRequestStream() throws Exception { // upload from storage if (rawBody.startsWith(RNFetchBlobConst.FILE_PREFIX)) { @@ -159,6 +160,13 @@ private InputStream getReuqestStream() throws Exception { throw new Exception("error when getting request stream: " +e.getLocalizedMessage()); } } + } else if (rawBody.startsWith(RNFetchBlobConst.CONTENT_PREFIX)) { + String contentURI = rawBody.substring(RNFetchBlobConst.CONTENT_PREFIX.length()); + try { + return RNFetchBlob.RCTContext.getContentResolver().openInputStream(Uri.parse(contentURI)); + } catch (Exception e) { + throw new Exception("error when getting request stream for content URI: " + contentURI, e); + } } // base 64 encoded else { @@ -224,6 +232,20 @@ private File createMultipartBodyCache() throws IOException { RNFetchBlobUtils.emitWarningEvent("Failed to create form data from path :" + orgPath + ", file not exists."); } } + } else if (data.startsWith(RNFetchBlobConst.CONTENT_PREFIX)) { + String contentURI = data.substring(RNFetchBlobConst.CONTENT_PREFIX.length()); + InputStream is = null; + try { + is = ctx.getContentResolver().openInputStream(Uri.parse(contentURI)); + pipeStreamToFileStream(is, os); + } catch(Exception e) { + RNFetchBlobUtils.emitWarningEvent( + "Failed to create form data from content URI:" + contentURI + ", " + e.getLocalizedMessage()); + } finally { + if (is != null) { + is.close(); + } + } } // base64 embedded file content else { @@ -259,7 +281,7 @@ private File createMultipartBodyCache() throws IOException { */ private void pipeStreamToSink(InputStream stream, BufferedSink sink) throws IOException { byte[] chunk = new byte[10240]; - int totalWritten = 0; + long totalWritten = 0; int read; while((read = stream.read(chunk, 0, 10240)) > 0) { sink.write(chunk, 0, read); @@ -289,7 +311,7 @@ private void pipeStreamToFileStream(InputStream is, FileOutputStream os) throws * Compute approximate content length for form data * @return ArrayList */ - private ArrayList countFormDataLength() { + private ArrayList countFormDataLength() throws IOException { long total = 0; ArrayList list = new ArrayList<>(); ReactApplicationContext ctx = RNFetchBlob.RCTContext; @@ -320,6 +342,21 @@ else if (field.filename != null) { File file = new File(RNFetchBlobFS.normalizePath(orgPath)); total += file.length(); } + } else if (data.startsWith(RNFetchBlobConst.CONTENT_PREFIX)) { + String contentURI = data.substring(RNFetchBlobConst.CONTENT_PREFIX.length()); + InputStream is = null; + try { + is = ctx.getContentResolver().openInputStream(Uri.parse(contentURI)); + long length = is.available(); + total += length; + } catch(Exception e) { + RNFetchBlobUtils.emitWarningEvent( + "Failed to estimate form data length from content URI:" + contentURI + ", " + e.getLocalizedMessage()); + } finally { + if (is != null) { + is.close(); + } + } } // base64 embedded file content else { @@ -366,7 +403,7 @@ private class FormField { * Emit progress event * @param written Integer */ - private void emitUploadProgress(int written) { + private void emitUploadProgress(long written) { RNFetchBlobProgressConfig config = RNFetchBlobReq.getReportUploadProgress(mTaskId); if(config != null && contentLength != 0 && config.shouldReport((float)written/contentLength)) { WritableMap args = Arguments.createMap(); 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/RNFetchBlobConst.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobConst.java index 015cc8954..b86902a31 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobConst.java +++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobConst.java @@ -7,6 +7,7 @@ public class RNFetchBlobConst { public static final String EVENT_HTTP_STATE = "RNFetchBlobState"; public static final String EVENT_MESSAGE = "RNFetchBlobMessage"; public static final String FILE_PREFIX = "RNFetchBlob-file://"; + public static final String CONTENT_PREFIX = "RNFetchBlob-content://"; public static final String FILE_PREFIX_BUNDLE_ASSET = "bundle-assets://"; public static final String FILE_PREFIX_CONTENT = "content://"; public static final String DATA_ENCODE_URI = "uri"; diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java index dde39bc4c..124bb81d7 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java +++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java @@ -69,32 +69,45 @@ static void writeFile(String path, String encoding, String data, final boolean a } } - FileOutputStream fout = new FileOutputStream(f, append); // write data from a file if(encoding.equalsIgnoreCase(RNFetchBlobConst.DATA_ENCODE_URI)) { String normalizedData = normalizePath(data); File src = new File(normalizedData); if (!src.exists()) { promise.reject("ENOENT", "No such file '" + path + "' " + "('" + normalizedData + "')"); - fout.close(); return; } - FileInputStream fin = new FileInputStream(src); byte[] buffer = new byte [10240]; int read; written = 0; - while((read = fin.read(buffer)) > 0) { - fout.write(buffer, 0, read); - written += read; + FileInputStream fin = null; + FileOutputStream fout = null; + try { + fin = new FileInputStream(src); + fout = new FileOutputStream(f, append); + while ((read = fin.read(buffer)) > 0) { + fout.write(buffer, 0, read); + written += read; + } + } finally { + if (fin != null) { + fin.close(); + } + if (fout != null) { + fout.close(); + } } - fin.close(); } else { byte[] bytes = stringToBytes(data, encoding); - fout.write(bytes); - written = bytes.length; + FileOutputStream fout = new FileOutputStream(f, append); + try { + fout.write(bytes); + written = bytes.length; + } finally { + fout.close(); + } } - fout.close(); promise.resolve(written); } catch (FileNotFoundException e) { // According to https://docs.oracle.com/javase/7/docs/api/java/io/FileOutputStream.html @@ -129,12 +142,15 @@ static void writeFile(String path, ReadableArray data, final boolean append, fin } FileOutputStream os = new FileOutputStream(f, append); - byte[] bytes = new byte[data.size()]; - for(int i=0;i getSystemfolders(ReactApplicationContext ctx) { res.put("DocumentDir", ctx.getFilesDir().getAbsolutePath()); res.put("CacheDir", ctx.getCacheDir().getAbsolutePath()); - res.put("DCIMDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath()); - res.put("PictureDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath()); - res.put("MusicDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getAbsolutePath()); - res.put("DownloadDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath()); - res.put("MovieDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES).getAbsolutePath()); - res.put("RingtoneDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_RINGTONES).getAbsolutePath()); + res.put("DCIMDir", ctx.getExternalFilesDir(Environment.DIRECTORY_DCIM).getAbsolutePath()); + res.put("PictureDir", ctx.getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath()); + res.put("MusicDir", ctx.getExternalFilesDir(Environment.DIRECTORY_MUSIC).getAbsolutePath()); + res.put("DownloadDir", ctx.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath()); + res.put("MovieDir", ctx.getExternalFilesDir(Environment.DIRECTORY_MOVIES).getAbsolutePath()); + res.put("RingtoneDir", ctx.getExternalFilesDir(Environment.DIRECTORY_RINGTONES).getAbsolutePath()); String state; state = Environment.getExternalStorageState(); if (state.equals(Environment.MEDIA_MOUNTED)) { - res.put("SDCardDir", Environment.getExternalStorageDirectory().getAbsolutePath()); - res.put("SDCardApplicationDir", ctx.getExternalFilesDir(null).getParentFile().getAbsolutePath()); + res.put("SDCardDir", ctx.getExternalFilesDir(null).getAbsolutePath()); + + File externalDirectory = ctx.getExternalFilesDir(null); + + if (externalDirectory != null) { + res.put("SDCardApplicationDir", externalDirectory.getParentFile().getAbsolutePath()); + } else { + res.put("SDCardApplicationDir", ""); + } } res.put("MainBundleDir", ctx.getApplicationInfo().dataDir); + return res; } + static public void getSDCardDir(ReactApplicationContext ctx, Promise promise) { + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + promise.resolve(ctx.getExternalFilesDir(null).getAbsolutePath()); + } else { + promise.reject("RNFetchBlob.getSDCardDir", "External storage not mounted"); + } + + } + + static public void getSDCardApplicationDir(ReactApplicationContext ctx, Promise promise) { + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + try { + final String path = ctx.getExternalFilesDir(null).getParentFile().getAbsolutePath(); + promise.resolve(path); + } catch (Exception e) { + promise.reject("RNFetchBlob.getSDCardApplicationDir", e.getLocalizedMessage()); + } + } else { + promise.reject("RNFetchBlob.getSDCardApplicationDir", "External storage not mounted"); + } + } + /** * Static method that returns a temp file path * @param taskId An unique string for identify @@ -460,7 +506,8 @@ static void closeStream(String streamId, Callback callback) { */ static void unlink(String path, Callback callback) { try { - RNFetchBlobFS.deleteRecursive(new File(path)); + String normalizedPath = normalizePath(path); + RNFetchBlobFS.deleteRecursive(new File(normalizedPath)); callback.invoke(null, true); } catch(Exception err) { callback.invoke(err.getLocalizedMessage(), false); @@ -492,7 +539,7 @@ private static void deleteRecursive(File fileOrDirectory) throws IOException { static void mkdir(String path, Promise promise) { File dest = new File(path); if(dest.exists()) { - promise.reject("EEXIST", dest.isDirectory() ? "Folder" : "File" + " '" + path + "' already exists"); + promise.reject("EEXIST", (dest.isDirectory() ? "Folder" : "File") + " '" + path + "' already exists"); return; } try { @@ -518,6 +565,7 @@ static void cp(String path, String dest, Callback callback) { path = normalizePath(path); InputStream in = null; OutputStream out = null; + String message = ""; try { if(!isPathExists(path)) { @@ -541,7 +589,7 @@ static void cp(String path, String dest, Callback callback) { out.write(buf, 0, len); } } catch (Exception err) { - callback.invoke(err.getLocalizedMessage()); + message += err.getLocalizedMessage(); } finally { try { if (in != null) { @@ -550,11 +598,17 @@ static void cp(String path, String dest, Callback callback) { if (out != null) { out.close(); } - callback.invoke(); } catch (Exception e) { - callback.invoke(e.getLocalizedMessage()); + message += e.getLocalizedMessage(); } } + // Only call the callback once to prevent the app from crashing + // with an 'Illegal callback invocation from native module' exception. + if (message != "") { + callback.invoke(message); + } else { + callback.invoke(); + } } /** @@ -569,16 +623,29 @@ static void mv(String path, String dest, Callback callback) { callback.invoke("Source file at path `" + path + "` does not exist"); return; } + try { - boolean result = src.renameTo(new File(dest)); - if (!result) { - callback.invoke("mv failed for unknown reasons"); - return; + InputStream in = new FileInputStream(path); + OutputStream out = new FileOutputStream(dest); + + //read source path to byte buffer. Write from input to output stream + byte[] buffer = new byte[1024]; + int read; + while ((read = in.read(buffer)) != -1) { //read is successful + out.write(buffer, 0, read); } + in.close(); + out.flush(); + + src.delete(); //remove original file + } catch (FileNotFoundException exception) { + callback.invoke("Source file not found."); + return; } catch (Exception e) { - callback.invoke(e.getLocalizedMessage()); + callback.invoke(e.toString()); return; } + callback.invoke(); } @@ -599,9 +666,14 @@ static void exists(String path, Callback callback) { } else { path = normalizePath(path); - boolean exist = new File(path).exists(); - boolean isDir = new File(path).isDirectory(); - callback.invoke(exist, isDir); + if (path != null) { + boolean exist = new File(path).exists(); + boolean isDir = new File(path).isDirectory(); + callback.invoke(exist, isDir); + } + else { + callback.invoke(false, false); + } } } @@ -825,11 +897,14 @@ static void hash(String path, String algorithm, Promise promise) { MessageDigest md = MessageDigest.getInstance(algorithms.get(algorithm)); FileInputStream inputStream = new FileInputStream(path); - byte[] buffer = new byte[(int)file.length()]; + int chunkSize = 4096 * 256; // 1Mb + byte[] buffer = new byte[chunkSize]; - int read; - while ((read = inputStream.read(buffer)) != -1) { - md.update(buffer, 0, read); + if(file.length() != 0) { + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + md.update(buffer, 0, bytesRead); + } } StringBuilder hexString = new StringBuilder(); @@ -911,13 +986,13 @@ static void createFileASCII(String path, ReadableArray data, Promise promise) { } } - static void df(Callback callback) { - StatFs stat = new StatFs(Environment.getDataDirectory().getPath()); + static void df(Callback callback, ReactApplicationContext ctx) { + StatFs stat = new StatFs(ctx.getFilesDir().getPath()); WritableMap args = Arguments.createMap(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { args.putString("internal_free", String.valueOf(stat.getFreeBytes())); args.putString("internal_total", String.valueOf(stat.getTotalBytes())); - StatFs statEx = new StatFs(Environment.getExternalStorageDirectory().getPath()); + StatFs statEx = new StatFs(ctx.getExternalFilesDir(null).getPath()); args.putString("external_free", String.valueOf(statEx.getFreeBytes())); args.putString("external_total", String.valueOf(statEx.getTotalBytes())); diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java index aaa837cee..55fb15c4c 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java +++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java @@ -8,7 +8,11 @@ import android.database.Cursor; import android.net.Uri; import android.os.Build; -import android.support.annotation.NonNull; +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; @@ -16,7 +20,6 @@ import com.facebook.common.logging.FLog; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; @@ -37,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; @@ -161,7 +165,11 @@ public void run() { if (options.addAndroidDownloads.getBoolean("useDownloadManager")) { Uri uri = Uri.parse(url); DownloadManager.Request req = new DownloadManager.Request(uri); - req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + if(options.addAndroidDownloads.hasKey("notification") && options.addAndroidDownloads.getBoolean("notification")) { + req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + } else { + req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN); + } if(options.addAndroidDownloads.hasKey("title")) { req.setTitle(options.addAndroidDownloads.getString("title")); } @@ -169,7 +177,7 @@ public void run() { req.setDescription(options.addAndroidDownloads.getString("description")); } if(options.addAndroidDownloads.hasKey("path")) { - req.setDestinationUri(Uri.parse("file://" + options.addAndroidDownloads.getString("path"))); + req.setDestinationUri(Uri.fromFile(new File(options.addAndroidDownloads.getString("path")))); } // #391 Add MIME type to the request if(options.addAndroidDownloads.hasKey("mime")) { @@ -228,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)); @@ -262,11 +313,14 @@ else if (value.equalsIgnoreCase("utf8")) requestType = RequestType.Form; } else if(cType.isEmpty()) { - builder.header("Content-Type", "application/octet-stream"); + if(!cType.equalsIgnoreCase("")) { + builder.header("Content-Type", "application/octet-stream"); + } requestType = RequestType.SingleFile; } if(rawRequestBody != null) { - if(rawRequestBody.startsWith(RNFetchBlobConst.FILE_PREFIX)) { + if (rawRequestBody.startsWith(RNFetchBlobConst.FILE_PREFIX) + || rawRequestBody.startsWith(RNFetchBlobConst.CONTENT_PREFIX)) { requestType = RequestType.SingleFile; } else if (cType.toLowerCase().contains(";base64") || cType.toLowerCase().startsWith("application/octet")) { @@ -338,8 +392,9 @@ public Response intercept(Chain chain) throws IOException { clientBuilder.addInterceptor(new Interceptor() { @Override public Response intercept(@NonNull Chain chain) throws IOException { + Response originalResponse = null; try { - Response originalResponse = chain.proceed(req); + originalResponse = chain.proceed(req); ResponseBody extended; switch (responseType) { case KeepInMemory: @@ -369,13 +424,21 @@ public Response intercept(@NonNull Chain chain) throws IOException { } catch(SocketException e) { timeout = true; - } - catch (SocketTimeoutException e ){ + if (originalResponse != null) { + originalResponse.close(); + } + } catch (SocketTimeoutException e) { timeout = true; - RNFetchBlobUtils.emitWarningEvent("RNFetchBlob error when sending request : " + e.getLocalizedMessage()); - } catch(Exception ex) { - + if (originalResponse != null) { + originalResponse.close(); + } + //ReactNativeBlobUtilUtils.emitWarningEvent("ReactNativeBlobUtil error when sending request : " + e.getLocalizedMessage()); + } catch (Exception ex) { + if (originalResponse != null) { + originalResponse.close(); + } } + return chain.proceed(chain.request()); } }); @@ -408,7 +471,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); @@ -508,11 +571,13 @@ private void done(Response resp) { 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 + // This usually mean the data is contains invalid unicode characters but still valid data, + // it's binary data, so send it as a normal string catch(CharacterCodingException ignored) { + if(responseFormat == ResponseFormat.UTF8) { - callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_UTF8, ""); + String utf8 = new String(b); + callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_UTF8, utf8); } else { callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_BASE64, android.util.Base64.encodeToString(b, Base64.NO_WRAP)); @@ -524,16 +589,49 @@ private void done(Response resp) { } break; case FileStorage: + ResponseBody responseBody = resp.body(); + 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(); + responseBody.bytes(); } catch (Exception ignored) { // ignored.printStackTrace(); } - this.destPath = this.destPath.replace("?append=true", ""); - callback.invoke(null, RNFetchBlobConst.RNFB_RESPONSE_PATH, this.destPath); + + RNFetchBlobFileResp rnFetchBlobFileResp; + + try { + rnFetchBlobFileResp = (RNFetchBlobFileResp) responseBody; + } catch (ClassCastException ex) { + // unexpected response type + if (responseBody != null) { + String responseBodyString = null; + try { + boolean isBufferDataExists = responseBody.source().buffer().size() > 0; + boolean isContentExists = responseBody.contentLength() > 0; + if (isBufferDataExists && isContentExists) { + responseBodyString = responseBody.string(); + } + } catch(IOException exception) { + exception.printStackTrace(); + } + callback.invoke("Unexpected FileStorage response file: " + responseBodyString, null); + } else { + callback.invoke("Unexpected FileStorage response with no file.", null); + } + return; + } + + 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 { @@ -642,8 +740,12 @@ private String getHeaderIgnoreCases(HashMap headers, String field } private void emitStateEvent(WritableMap args) { - RNFetchBlob.RCTContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(RNFetchBlobConst.EVENT_HTTP_STATE, args); + try { + RNFetchBlob.RCTContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(RNFetchBlobConst.EVENT_HTTP_STATE, args); + } catch (Exception e) { + FLog.e("RNFetchBlobReq", "Error emitting state event", e); + } } @Override @@ -660,29 +762,49 @@ public void onReceive(Context context, Intent intent) { DownloadManager dm = (DownloadManager) appCtx.getSystemService(Context.DOWNLOAD_SERVICE); 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); + // #236 unhandled null check for DownloadManager.query() return value + if (c == null) { + try { + this.callback.invoke("Download manager failed to download from " + this.url + ". Query was unsuccessful ", null, null); return; } - String contentUri = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)); - 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 - if (cursor != null) { - cursor.moveToFirst(); - filePath = cursor.getString(0); - cursor.close(); + catch(Exception e) { + return; + } + } + + String filePath = null; + try { + // 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) { + try { + this.callback.invoke("Download manager failed to download from " + this.url + ". Query was unsuccessful ", null, null); + return; + } + catch(Exception e) { + return; + } } + String contentUri = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)); + 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 + if (cursor != null) { + cursor.moveToFirst(); + filePath = cursor.getString(0); + cursor.close(); + } + } + } + } finally { + if (c != null) { + c.close(); } } diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobUtils.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobUtils.java index 6dbcbe7c7..ab35fdd31 100644 --- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobUtils.java +++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobUtils.java @@ -56,22 +56,21 @@ public static void emitWarningEvent(String data) { public static OkHttpClient.Builder getUnsafeOkHttpClient(OkHttpClient client) { try { // Create a trust manager that does not validate certificate chains - final TrustManager[] trustAllCerts = new TrustManager[]{ - new X509TrustManager() { - @Override - public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { - } - - @Override - public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { - } - - @Override - public java.security.cert.X509Certificate[] getAcceptedIssuers() { - return new java.security.cert.X509Certificate[]{}; - } - } + final X509TrustManager x509TrustManager = new X509TrustManager() { + @Override + public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return new java.security.cert.X509Certificate[]{}; + } }; + final TrustManager[] trustAllCerts = new TrustManager[]{ x509TrustManager }; // Install the all-trusting trust manager final SSLContext sslContext = SSLContext.getInstance("SSL"); @@ -80,7 +79,7 @@ public java.security.cert.X509Certificate[] getAcceptedIssuers() { final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); OkHttpClient.Builder builder = client.newBuilder(); - builder.sslSocketFactory(sslSocketFactory); + builder.sslSocketFactory(sslSocketFactory, x509TrustManager); builder.hostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { diff --git a/android/src/main/java/com/RNFetchBlob/Response/RNFetchBlobFileResp.java b/android/src/main/java/com/RNFetchBlob/Response/RNFetchBlobFileResp.java index e4f725777..2470eef61 100644 --- a/android/src/main/java/com/RNFetchBlob/Response/RNFetchBlobFileResp.java +++ b/android/src/main/java/com/RNFetchBlob/Response/RNFetchBlobFileResp.java @@ -1,7 +1,6 @@ package com.RNFetchBlob.Response; -import android.support.annotation.NonNull; -import android.util.Log; +import androidx.annotation.NonNull; import com.RNFetchBlob.RNFetchBlobConst; import com.RNFetchBlob.RNFetchBlobProgressConfig; @@ -34,6 +33,7 @@ public class RNFetchBlobFileResp extends ResponseBody { long bytesDownloaded = 0; ReactApplicationContext rctContext; FileOutputStream ofStream; + boolean isEndMarkerReceived; public RNFetchBlobFileResp(ReactApplicationContext ctx, String taskId, ResponseBody body, String path, boolean overwrite) throws IOException { super(); @@ -42,6 +42,7 @@ public RNFetchBlobFileResp(ReactApplicationContext ctx, String taskId, ResponseB this.originalBody = body; assert path != null; this.mPath = path; + this.isEndMarkerReceived = false; if (path != null) { boolean appendToExistingFile = !overwrite; path = path.replace("?append=true", ""); @@ -69,6 +70,11 @@ public long contentLength() { return originalBody.contentLength(); } + public boolean isDownloadComplete() { + return (bytesDownloaded == contentLength()) // Case of non-chunked downloads + || (contentLength() == -1 && isEndMarkerReceived); // Case of chunked downloads + } + @Override public BufferedSource source() { ProgressReportingSource countable = new ProgressReportingSource(); @@ -84,22 +90,49 @@ public long read(@NonNull Buffer sink, long byteCount) throws IOException { bytesDownloaded += read > 0 ? read : 0; if (read > 0) { ofStream.write(bytes, 0, (int) read); + } else if (contentLength() == -1 && read == -1) { + // End marker has been received for chunked download + isEndMarkerReceived = true; } RNFetchBlobProgressConfig reportConfig = RNFetchBlobReq.getReportProgress(mTaskId); - if (reportConfig != null && contentLength() != 0 &&reportConfig.shouldReport(bytesDownloaded / contentLength())) { - WritableMap args = Arguments.createMap(); - args.putString("taskId", mTaskId); - args.putString("written", String.valueOf(bytesDownloaded)); - args.putString("total", String.valueOf(contentLength())); - rctContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(RNFetchBlobConst.EVENT_PROGRESS, args); + + if (contentLength() != 0) { + + // For non-chunked download, progress is received / total + // For chunked download, progress can be either 0 (started) or 1 (ended) + float progress = (contentLength() != -1) ? bytesDownloaded / contentLength() : ( ( isEndMarkerReceived ) ? 1 : 0 ); + + if (reportConfig != null && reportConfig.shouldReport(progress /* progress */)) { + if (contentLength() != -1) { + // For non-chunked downloads + reportProgress(mTaskId, bytesDownloaded, contentLength()); + } else { + // For chunked downloads + if (!isEndMarkerReceived) { + reportProgress(mTaskId, 0, contentLength()); + } else{ + reportProgress(mTaskId, bytesDownloaded, bytesDownloaded); + } + } + } + } + return read; } catch(Exception ex) { return -1; } } + private void reportProgress(String taskId, long bytesDownloaded, long contentLength) { + WritableMap args = Arguments.createMap(); + args.putString("taskId", taskId); + args.putString("written", String.valueOf(bytesDownloaded)); + args.putString("total", String.valueOf(contentLength)); + rctContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(RNFetchBlobConst.EVENT_PROGRESS, args); + } + @Override public Timeout timeout() { return null; diff --git a/android/src/main/java/com/RNFetchBlob/Utils/FileProvider.java b/android/src/main/java/com/RNFetchBlob/Utils/FileProvider.java new file mode 100644 index 000000000..d48f75fb1 --- /dev/null +++ b/android/src/main/java/com/RNFetchBlob/Utils/FileProvider.java @@ -0,0 +1,4 @@ +package com.RNFetchBlob.Utils; + +public class FileProvider extends androidx.core.content.FileProvider { +} diff --git a/android/src/main/java/com/RNFetchBlob/Utils/PathResolver.java b/android/src/main/java/com/RNFetchBlob/Utils/PathResolver.java index fef3ada97..c20c1943b 100644 --- a/android/src/main/java/com/RNFetchBlob/Utils/PathResolver.java +++ b/android/src/main/java/com/RNFetchBlob/Utils/PathResolver.java @@ -1,5 +1,6 @@ package com.RNFetchBlob.Utils; +import android.annotation.TargetApi; import android.content.Context; import android.database.Cursor; import android.net.Uri; @@ -7,7 +8,6 @@ import android.provider.DocumentsContract; import android.provider.MediaStore; import android.content.ContentUris; -import android.os.Environment; import android.content.ContentResolver; import com.RNFetchBlob.RNFetchBlobUtils; import java.io.File; @@ -15,6 +15,7 @@ import java.io.FileOutputStream; public class PathResolver { + @TargetApi(19) public static String getRealPathFromURI(final Context context, final Uri uri) { final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; @@ -28,19 +29,32 @@ public static String getRealPathFromURI(final Context context, final Uri uri) { final String type = split[0]; if ("primary".equalsIgnoreCase(type)) { - return Environment.getExternalStorageDirectory() + "/" + split[1]; + return context.getExternalFilesDir(null) + "/" + split[1]; } // TODO handle non-primary volumes } // DownloadsProvider else if (isDownloadsDocument(uri)) { + try { + final String id = DocumentsContract.getDocumentId(uri); + //Starting with Android O, this "id" is not necessarily a long (row number), + //but might also be a "raw:/some/file/path" URL + if (id != null && id.startsWith("raw:/")) { + Uri rawuri = Uri.parse(id); + String path = rawuri.getPath(); + return path; + } + final Uri contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); - final String id = DocumentsContract.getDocumentId(uri); - final Uri contentUri = ContentUris.withAppendedId( - Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); - - return getDataColumn(context, contentUri, null, null); + return getDataColumn(context, contentUri, null, null); + } + catch (Exception ex) { + //something went wrong, but android should still be able to handle the original uri by returning null here (see readFile(...)) + return null; + } + } // MediaProvider else if (isMediaDocument(uri)) { diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index dc2d93a52..de5cc9fd5 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - react-native-fetch-blob + rn-fetch-blob diff --git a/android/src/main/res/xml/provider_paths.xml b/android/src/main/res/xml/provider_paths.xml new file mode 100644 index 000000000..c38617d34 --- /dev/null +++ b/android/src/main/res/xml/provider_paths.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/fs.js b/fs.js index a286e937a..77bbe7a9d 100644 --- a/fs.js +++ b/fs.js @@ -13,17 +13,17 @@ import RNFetchBlobFile from './class/RNFetchBlobFile' const RNFetchBlob: RNFetchBlobNative = NativeModules.RNFetchBlob const dirs = { - DocumentDir: RNFetchBlob.DocumentDir, - CacheDir: RNFetchBlob.CacheDir, - PictureDir: RNFetchBlob.PictureDir, - MusicDir: RNFetchBlob.MusicDir, - MovieDir: RNFetchBlob.MovieDir, - DownloadDir: RNFetchBlob.DownloadDir, - DCIMDir: RNFetchBlob.DCIMDir, - SDCardDir: RNFetchBlob.SDCardDir, - SDCardApplicationDir: RNFetchBlob.SDCardApplicationDir, - MainBundleDir: RNFetchBlob.MainBundleDir, - LibraryDir: RNFetchBlob.LibraryDir + DocumentDir : RNFetchBlob.DocumentDir, + CacheDir : RNFetchBlob.CacheDir, + PictureDir : RNFetchBlob.PictureDir, + MusicDir : RNFetchBlob.MusicDir, + MovieDir : RNFetchBlob.MovieDir, + DownloadDir : RNFetchBlob.DownloadDir, + DCIMDir : RNFetchBlob.DCIMDir, + SDCardDir: RNFetchBlob.SDCardDir, // Depracated + SDCardApplicationDir: RNFetchBlob.SDCardApplicationDir, // Deprecated + MainBundleDir : RNFetchBlob.MainBundleDir, + LibraryDir : RNFetchBlob.LibraryDir } function addCode(code: string, error: Error): Error { @@ -135,6 +135,19 @@ function pathForAppGroup(groupName: string): Promise { return RNFetchBlob.pathForAppGroup(groupName) } +/** + * Returns the path for the app group synchronous. + * @param {string} groupName Name of app group + * @return {string} Path of App Group dir + */ +function syncPathAppGroup(groupName: string): string { + if (Platform.OS === 'ios') { + return RNFetchBlob.syncPathAppGroup(groupName); + } else { + return ''; + } +} + /** * Wrapper method of readStream. * @param {string} path Path of the file. @@ -402,6 +415,7 @@ export default { writeFile, appendFile, pathForAppGroup, + syncPathAppGroup, readFile, hash, exists, diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 000000000..58dc9e1de --- /dev/null +++ b/index.d.ts @@ -0,0 +1,690 @@ +// Type definitions for react-native-fetch-blob 0.10 +// Project: https://github.com/wkh237/react-native-fetch-blob#readme +// Definitions by: MNB +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +export const RNFetchBlob: RNFetchBlobStatic; +export type RNFetchBlob = RNFetchBlobStatic; +export default RNFetchBlob; + +interface RNFetchBlobStatic { + fetch(method: Methods, url: string, headers?: { [key: string]: string }, body?: any + | null): StatefulPromise; + base64: { encode(input: string): string; decode(input: string): string }; + android: AndroidApi; + ios: IOSApi; + config(options: RNFetchBlobConfig): RNFetchBlobStatic; + session(name: string): RNFetchBlobSession; + fs: FS; + wrap(path: string): string; + net: Net; + polyfill: Polyfill; + // this require external module https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/oboe + JSONStream: any; +} + +export interface Polyfill { + Blob: PolyfillBlob; + File: PolyfillFile; + XMLHttpRequest: PolyfillXMLHttpRequest; + ProgressEvent: PolyfillProgressEvent; + Event: PolyfillEvent; + FileReader: PolyfillFileReader; + Fetch: PolyfillFetch; +} + +export declare class PolyfillFetch extends RNFetchBlobFetchPolyfill { + constructor(config: RNFetchBlobConfig); +} + +export declare class RNFetchBlobFetchPolyfill { + constructor(config: RNFetchBlobConfig); + + build(): (url: string, options: RNFetchBlobConfig) => StatefulPromise; +} + +export interface RNFetchBlobFetchRepsonse { + arrayBuffer(): Promise; + blob(): Promise; + json(): Promise; + rawResp(): Promise; + text(): Promise; + bodyUsed: boolean; + headers: any; + ok: boolean; + resp: FetchBlobResponse; + rnfbResp: FetchBlobResponse; + rnfbRespInfo: RNFetchBlobResponseInfo; + status: number; + type: string; +} + +/** + * RNFetchBlob response object class. + */ +export interface FetchBlobResponse { + taskId: string; + /** + * get path of response temp file + * @return File path of temp file. + */ + path(): string; + type: "base64" | "path" | "utf8"; + data: any; + /** + * Convert result to javascript RNFetchBlob object. + * @return Return a promise resolves Blob object. + */ + blob(contentType: string, sliceSize: number): Promise; + /** + * Convert result to text. + * @return Decoded base64 string. + */ + text(): string | Promise; + /** + * Convert result to JSON object. + * @return Parsed javascript object. + */ + json(): any; + /** + * Return BASE64 string directly. + * @return BASE64 string of response body. + */ + base64(): any; + /** + * Remove cahced file + */ + flush(): void; + respInfo: RNFetchBlobResponseInfo; + info(): RNFetchBlobResponseInfo; + session(name: string): RNFetchBlobSession | null; + /** + * Read file content with given encoding, if the response does not contains + * a file path, show warning message + * @param encode Encode type, should be one of `base64`, `ascrii`, `utf8`. + */ + readFile(encode: Encoding): Promise | null; + /** + * Start read stream from cached file + * @param encode Encode type, should be one of `base64`, `ascrii`, `utf8`. + */ + readStream(encode: Encoding): RNFetchBlobStream | null; +} + +export interface PolyfillFileReader extends EventTarget { + isRNFBPolyFill: boolean; + onloadstart(e: Event): void; + onprogress(e: Event): void; + onload(e: Event): void; + onabort(e: Event): void; + onerror(e: Event): void; + onloadend(e: Event): void; + + abort(): void; + readAsArrayBuffer(b: PolyfillBlob): void; + readAsBinaryString(b: PolyfillBlob): void; + readAsText(b: PolyfillBlob, label?: string): void; + readAsDataURL(b: PolyfillBlob): void; + + readyState: number; + result: number; +} + +export declare namespace PolyfillFileReader { + const EMPTY: number; + const LOADING: number; + const DONE: number; +} + +export declare class PolyfillEvent { +} + +export interface PolyfillProgressEvent extends EventTarget { + lengthComputable: boolean; + loaded: number; + total: number; +} + +export declare class PolyfillBlob implements EventTarget { + /** + * RNFetchBlob Blob polyfill, create a Blob directly from file path, BASE64 + * encoded data, and string. The conversion is done implicitly according to + * given `mime`. However, the blob creation is asynchronously, to register + * event `onCreated` is need to ensure the Blob is creadted. + * + * @param data Content of Blob object + * @param cType Content type settings of Blob object, `text/plain` by default + * @param defer When this argument set to `true`, blob constructor will not invoke blob created event automatically. + */ + constructor(data: any, cType: any, defer: boolean); + + /** + * Since Blob content will asynchronously write to a file during creation, + * use this method to register an event handler for Blob initialized event. + * @param fn An event handler invoked when Blob created + * @return The Blob object instance itself + */ + onCreated(fn: () => void): PolyfillBlob; + + markAsDerived(): void; + + /** + * Get file reference of the Blob object. + * @return Blob file reference which can be consumed by RNFetchBlob fs + */ + getRNFetchBlobRef(): string; + + /** + * Create a Blob object which is sliced from current object + * @param start Start byte number + * @param end End byte number + * @param contentType Optional, content type of new Blob object + */ + slice(start?: number, end?: number, contentType?: string): PolyfillBlob; + + /** + * Read data of the Blob object, this is not standard method. + * @param encoding Read data with encoding + */ + readBlob(encoding: string): Promise; + + /** + * Release the resource of the Blob object. + * @nonstandard + */ + close(): Promise; +} + +export declare namespace PolyfillBlob { + function clearCache(): void; + + function build(data: any, cType: any): Promise; + + function setLog(level: number): void; +} + +export declare class PolyfillFile extends PolyfillBlob { +} + +export interface PolyfillXMLHttpRequest extends PolyfillXMLHttpRequestEventTarget { + upload: PolyfillXMLHttpRequestEventTarget; + readonly UNSENT: number; + readonly OPENED: number; + readonly HEADERS_RECEIVED: number; + readonly LOADING: number; + readonly DONE: number; + + /** + * XMLHttpRequest.open, always async, user and password not supported. When + * this method invoked, headers should becomes empty again. + * @param method Request method + * @param url Request URL + * @param async Always async + * @param user NOT SUPPORTED + * @param password NOT SUPPORTED + */ + open(method: string, url: string, async: true, user: any, password: any): void; + + /** + * Invoke this function to send HTTP request, and set body. + * @param body Body in RNfetchblob flavor + */ + send(body: any): void; + + overrideMimeType(mime: string): void; + + setRequestHeader(name: string, value: string): void; + + abort(): void; + + getResponseHeader(field: string): string | null; + + getAllResponseHeaders(): string | null; + + onreadystatechange(e: Event): void; + readyState: number; + status: number; + statusText: string; + response: any; + responseText: any; + responseURL: string; + responseHeaders: any; + timeout: number; + responseType: string; +} + +export declare namespace PolyfillXMLHttpRequest { + const binaryContentTypes: string[]; + const UNSENT: number; + const OPENED: number; + const HEADERS_RECEIVED: number; + const LOADING: number; + const DONE: number; + + function setLog(level: number): void; + + function addBinaryContentType(substr: string): void; + + function removeBinaryContentType(): void; +} + +export interface PolyfillXMLHttpRequestEventTarget extends EventTarget { + onabort(e: Event): void; + onerror(e: Event): void; + onload(e: Event): void; + onloadstart(e: Event): void; + onprogress(e: Event): void; + ontimeout(e: Event): void; + onloadend(e: Event): void; +} + +export interface Net { + /** + * Get cookie according to the given url. + * @param domain Domain of the cookies to be removed, remove all + * @return Cookies of a specific domain. + */ + getCookies(domain: string): Promise; + + /** + * Remove cookies for a specific domain + * @param domain Domain of the cookies to be removed, remove all + * cookies when this is null. + */ + removeCookies(domain?: string): Promise; +} + +type HashAlgorithm = "md5" | "sha1" | "sha224" | "sha256" | "sha384" | "sha512"; +export interface FS { + RNFetchBlobSession: RNFetchBlobSession; + + /** + * Remove file at path. + * @param path:string Path of target file. + */ + unlink(path: string): Promise; + + /** + * Create a directory. + * @param path Path of directory to be created + */ + mkdir(path: string): Promise; + + /** + * Get a file cache session + * @param name Stream ID + */ + session(name: string): RNFetchBlobSession; + + ls(path: string): Promise; + + /** + * Read the file from the given path and calculate a cryptographic hash sum over its contents. + * + * @param path Path to the file + * @param algorithm The hash algorithm to use + */ + hash(path: string, algorithm: HashAlgorithm): Promise; + + /** + * Create file stream from file at `path`. + * @param path The file path. + * @param encoding Data encoding, should be one of `base64`, `utf8`, `ascii` + * @param bufferSize Size of stream buffer. + * @return RNFetchBlobStream stream instance. + */ + readStream(path: string, encoding: Encoding, bufferSize?: number, tick?: number): Promise; + + mv(path: string, dest: string): Promise; + + cp(path: string, dest: string): Promise; + + /** + * Create write stream to a file. + * @param path Target path of file stream. + * @param encoding Encoding of input data. + * @param append A flag represent if data append to existing ones. + * @return A promise resolves a `WriteStream` object. + */ + writeStream(path: string, encoding: Encoding, append?: boolean): Promise; + + /** + * Write data to file. + * @param path Path of the file. + * @param data Data to write to the file. + * @param encoding Encoding of data (Optional). + */ + writeFile(path: string, data: string | number[], encoding?: Encoding): Promise; + + appendFile(path: string, data: string | number[], encoding?: Encoding | "uri"): Promise; + + /** + * Wrapper method of readStream. + * @param path Path of the file. + * @param encoding Encoding of read stream. + */ + readFile(path: string, encoding: Encoding, bufferSize?: number): Promise; + /** + * Check if file exists and if it is a folder. + * @param path Path to check + */ + exists(path: string): Promise; + + createFile(path: string, data: string, encoding: Encoding): Promise; + + isDir(path: string): Promise; + + /** + * Show statistic data of a path. + * @param path Target path + */ + stat(path: string): Promise; + + lstat(path: string): Promise; + + /** + * Android only method, request media scanner to scan the file. + * @param pairs Array contains Key value pairs with key `path` and `mime`. + */ + scanFile(pairs: Array<{ [key: string]: string }>): Promise; + + dirs: Dirs; + + slice(src: string, dest: string, start: number, end: number): Promise; + asset(path: string): string; + df(): Promise<{ free: number, total: number }>; +} + +export interface Dirs { + DocumentDir: string; + CacheDir: string; + PictureDir: string; + LibraryDir: string; + MusicDir: string; + MovieDir: string; + DownloadDir: string; + DCIMDir: string; + SDCardDir: string; + MainBundleDir: string; +} + +export interface RNFetchBlobWriteStream { + id: string; + encoding: string; + append: boolean; + + write(data: string): Promise; + close(): void; +} + +export interface RNFetchBlobReadStream { + path: string; + encoding: Encoding; + bufferSize?: number; + closed: boolean; + tick: number; + + open(): void; + + onData(fn: (chunk: string | number[]) => void): void; + + onError(fn: (err: any) => void): void; + + onEnd(fn: () => void): void; +} + +export type Encoding = "utf8" | "ascii" | "base64"; + +/* tslint:disable-next-line interface-name*/ +export interface IOSApi { + /** + * Open a file in {@link https://developer.apple.com/reference/uikit/uidocumentinteractioncontroller UIDocumentInteractionController}, + * this is the default document viewer of iOS, supports several kinds of files. On Android, there's an similar method {@link android.actionViewIntent}. + * @param path This is a required field, the path to the document. The path should NOT contains any scheme prefix. + */ + previewDocument(path: string): void; + + /** + * Show options menu for interact with the file. + * @param path This is a required field, the path to the document. The path should NOT contains any scheme prefix. + */ + openDocument(path: string): void; +} + +export interface AndroidDownloadOption { + /** + * Title string to be displayed when the file added to Downloads app. + */ + title: string + + /** + * File description to be displayed when the file added to Downloads app. + */ + description: string + + /** + * MIME string of the file. + */ + mime: string + + /** + * URI string of the file. + */ + path: string + + /** + * Boolean value that determines if notification will be displayed. + */ + showNotification: boolean +} + +export interface AndroidApi { + /** + * When sending an ACTION_VIEW intent with given file path and MIME type, system will try to open an + * App to handle the file. For example, open Gallery app to view an image, or install APK. + * @param path Path of the file to be opened. + * @param mime Basically system will open an app according to this MIME type. + */ + actionViewIntent(path: string, mime: string): Promise; + + /** + * + * This method brings up OS default file picker and resolves a file URI when the user selected a file. + * However, it does not resolve or reject when user dismiss the file picker via pressing hardware back button, + * but you can still handle this behavior via AppState. + * @param mime MIME type filter, only the files matches the MIME will be shown. + */ + getContentIntent(mime: string): Promise; + + /** + * Using this function to add an existing file to Downloads app. + * @param options An object that for setting the title, description, mime, and notification of the item. + */ + addCompleteDownload(options: AndroidDownloadOption): Promise; + + getSDCardDir(): Promise; + + getSDCardApplicationDir(): Promise; +} + +type Methods = "POST" | "GET" | "DELETE" | "PUT" | "post" | "get" | "delete" | "put"; + +/** + * A declare class inherits Promise, it has extra method like progress, uploadProgress, + * and cancel which can help managing an asynchronous task's state. + */ +export interface StatefulPromise extends Promise { + /** + * Cancel the request when invoke this method. + */ + cancel(cb?: (reason: any) => void): StatefulPromise; + + /** + * Add an event listener which triggers when data receiving from server. + */ + progress(callback: (received: number, total: number) => void): StatefulPromise; + + /** + * Add an event listener with custom configuration + */ + progress(config: { count?: number, interval?: number }, callback: (received: number, total: number) => void): StatefulPromise; + + /** + * Add an event listener with custom configuration. + */ + uploadProgress(callback: (sent: number, total: number) => void): StatefulPromise; + + /** + * Add an event listener with custom configuration + */ + uploadProgress(config: { count?: number, interval?: number }, callback: (sent: number, total: number) => void): StatefulPromise; + + /** + * An IOS only API, when IOS app turns into background network tasks will be terminated after ~180 seconds, + * in order to handle these expired tasks, you can register an event handler, which will be called after the + * app become active. + */ + expire(callback: () => void): StatefulPromise; +} + +export declare class RNFetchBlobSession { + constructor(name: string, list: string[]); + + add(path: string): RNFetchBlobSession; + + remove(path: string): RNFetchBlobSession; + + dispose(): Promise; + + list(): string[]; + + name: string; + + static getSession(name: string): any; + + static setSession(name: string): void; + + static removeSession(name: string): void; +} + +/** + * A set of configurations that will be injected into a fetch method, with the following properties. + */ +export interface RNFetchBlobConfig { + /** + * When this property is true, the downloaded data will overwrite the existing file. (true by default) + */ + overwrite?: boolean; + + /** + * Set timeout of the request (in milliseconds). + */ + timeout?: number; + + /** + * Set this property to true to display a network indicator on status bar, this feature is only supported on IOS. + */ + indicator?: boolean; + + /** + * Set this property to true will allow the request create connection with server have self-signed SSL + * certification. This is not recommended to use in production. + */ + 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}. + */ + fileCache?: boolean; + + /** + * Set this property to change temp file extension that created by fetch response data. + */ + appendExt?: string; + + /** + * When this property has value, fetch API will try to store response data in the path ignoring fileCache and + * appendExt property. + */ + path?: string; + + session?: string; + + addAndroidDownloads?: AddAndroidDownloads; + + /** + * Fix IOS request timeout issue #368 by change default request setting to defaultSessionConfiguration, and make backgroundSessionConfigurationWithIdentifier optional + */ + IOSBackgroundTask?: boolean; +} + +export interface AddAndroidDownloads { + /** + * download file using Android download manager or not. + */ + useDownloadManager?: boolean; + /** + * title of the file + */ + title?: string; + /** + * File description of the file. + */ + description?: string; + /** + * The destination which the file will be downloaded, it SHOULD be a location on external storage (DCIMDir). + */ + path?: string; + /** + * MIME type of the file. By default is text/plain + */ + mime?: string; + /** + * A boolean value, see Officail Document + * (https://developer.android.com/reference/android/app/DownloadManager.html#addCompletedDownload(java.lang.String, java.lang.String, boolean, java.lang.String, java.lang.String, long, boolean)) + */ + mediaScannable?: boolean; + /** + * A boolean value decide whether show a notification when download complete. + */ + notification?: boolean; +} + +export interface RNFetchBlobResponseInfo { + taskId: string; + state: string; + headers: any; + redirects: string[]; + status: number; + respType: "text" | "blob" | "" | "json"; + rnfbEncode: "path" | "base64" | "ascii" | "utf8"; + timeout: boolean; +} + +export interface RNFetchBlobStream { + onData(): void; + onError(): void; + onEnd(): void; +} + +export declare class RNFetchBlobFile { +} + +export declare class RNFetchBlobStat { + lastModified: number; + size: number; + type: "directory" | "file"; + path: string; + filename: string; +} diff --git a/index.js b/index.js index bd67986fa..03063fe12 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,6 @@ import { DeviceEventEmitter, NativeAppEventEmitter, Platform, - AsyncStorage, AppState, } from 'react-native' import type { @@ -22,7 +21,7 @@ import fs from './fs' import getUUID from './utils/uuid' import base64 from 'base-64' import polyfill from './polyfill' -import _ from 'lodash' +import reduce from 'lodash/reduce' import android from './android' import ios from './ios' import JSONStream from './json-stream' @@ -72,14 +71,15 @@ emitter.addListener("RNFetchBlobMessage", (e) => { // Show warning if native module not detected if(!RNFetchBlob || !RNFetchBlob.fetchBlobForm || !RNFetchBlob.fetchBlob) { console.warn( - 'react-native-fetch-blob could not find valid native module.', + 'rn-fetch-blob could not find valid native module.', 'please make sure you have linked native modules using `rnpm link`,', 'and restart RN packager or manually compile IOS/Android project.' ) } function wrap(path:string):string { - return 'RNFetchBlob-file://' + path + const prefix = path.startsWith('content://') ? 'RNFetchBlob-content://' : 'RNFetchBlob-file://' + return prefix + path } /** @@ -104,7 +104,13 @@ function wrap(path:string):string { * activity takes place ) * If it doesn't exist, the file is downloaded as usual * @property {number} timeout - * Request timeout in millionseconds, by default it's 30000ms. + * 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. */ @@ -218,7 +224,7 @@ function fetch(...args:any):Promise { // # 241 normalize null or undefined headers, in case nil or null string // pass to native context - headers = _.reduce(headers, (result, value, key) => { + headers = reduce(headers, (result, value, key) => { result[key] = value || '' return result }, {}); @@ -228,8 +234,14 @@ function fetch(...args:any):Promise { return fetchFile(options, method, url, headers, body) } + let promiseResolve; + let promiseReject; + // from remote HTTP(S) let promise = new Promise((resolve, reject) => { + promiseResolve = resolve; + promiseReject = reject; + let nativeMethodName = Array.isArray(body) ? 'fetchBlobForm' : 'fetchBlob' // on progress event listener @@ -370,6 +382,7 @@ function fetch(...args:any):Promise { subscriptionUpload.remove() stateEvent.remove() RNFetchBlob.cancelRequest(taskId, fn) + promiseReject(new Error("canceled")) } promise.taskId = taskId diff --git a/index.js.flow b/index.js.flow new file mode 100644 index 000000000..03666e569 --- /dev/null +++ b/index.js.flow @@ -0,0 +1,189 @@ +// @flow strict + +declare class AndroidApi { + actionViewIntent(path: string, mime?: string): Promise; + addCompleteDownload(options: AndroidDownloadOption): Promise; + getContentIntent(mime: string): Promise; + getSDCardApplicationDir(): Promise; + getSDCardDir(): Promise; +} + +declare class FetchBlobResponse { + data: string; + respInfo: RNFetchBlobResponseInfo; + taskId: string; + type: "utf8" | "base64" | "path"; + array(): Promise; + base64(): string | Promise; + blob(): Promise; + flush(): Promise; + info(): RNFetchBlobResponseInfo; + // $FlowExpectedError Not possible to specify in strict mode. + json(): any; + path(): string; + readFile(encoding: Encoding): ?Promise; + readStream(encoding: Encoding): ?Promise; + session(name: string): ?RNFetchBlobSession; + text(): string | Promise; +} + +declare class FsApi { + RNFetchBlobSession: RNFetchBlobSession; + dirs: Dirs; + appendFile(path: string, data: string | number[], encoding?: Encoding | "uri"): Promise; + asset(path: string): string; + cp(path: string, dest: string): Promise; + createFile(path: string, data: string, encoding: Encoding | "uri"): Promise; + df(): Promise; + exists(path: string): Promise; + hash(path: string, algorithm: HashAlgorithm): Promise; + isDir(path: string): Promise; + ls(path: string): Promise; + lstat(path: string): Promise; + mkdir(path: string): Promise; + mv(path: string, dest: string): Promise; + pathForAppGroup(groupName: string): Promise; + readFile(path: string, encoding: Encoding, bufferSize?: number): Promise; + readStream(path: string, encoding: Encoding, bufferSize?: number, tick?: number): Promise; + scanFile(pairs: {mime: string, path: string}[]): Promise; + session(name: string): RNFetchBlobSession; + slice(src: string, dest: string, start: number, end: number): Promise; + stat(path: string): Promise; + unlink(path: string): Promise; + writeFile(path: string, data: string | number[], encoding?: Encoding | "uri"): Promise; + writeStream(path: string, encoding: Encoding, append?: boolean): Promise; +} + +declare class IosApi { + excludeFromBackupKey(path: string): Promise; + openDocument(path: string, scheme?: string): Promise; + previewDocument(path: string, scheme?: string): Promise; +} + +declare class RNFetchBlobReadStream { + bufferSize?: number; + closed: boolean; + encoding: Encoding; + path: string; + tick: number; + onData(fn: (chunk: string) => void): void; + onEnd(fn: () => void): void; + onError(fn: (err: Error) => void): void; + open(): void; +} + +declare class RNFetchBlobSession { + static getSession(name: string): RNFetchBlobSession; + static removeSession(name: string): void; + static setSession(name: string, val: RNFetchBlobSession): void; + + name: string; + add(path: string): RNFetchBlobSession; + constructor(name: string, list: string[]): RNFetchBlobSession; + dispose(): Promise; + list(): string[]; + remove(path: string): RNFetchBlobSession; +} + +declare class RNFetchBlobWriteStream { + append: boolean; + encoding: string; + id: string; + close(): void; + write(data: string): Promise; +} + +declare class RNFetchBlob { + android: AndroidApi; + base64: {+decode: (input: string) => string, +encode: (input: string) => string}; + fs: FsApi; + ios: IosApi; + config(options: RNFetchBlobConfig): {fetch: (method: Methods, url: string, headers?: {[key: string]: string}, body?: string | FormField[]) => StatefulPromise}; + fetch(method: Methods, url: string, headers?: {[key: string]: string}, body?: string | FormField[]): StatefulPromise; + session(name: string): RNFetchBlobSession; + wrap(path: string): string; +} + +declare class StatefulPromise extends Promise { + cancel(callback?: (error: ?string, taskId: ?string) => void): void; + expire(callback: () => void): StatefulPromise; + progress(config: {count?: number, interval?: number} | ProgressCallback, callback?: ProgressCallback): StatefulPromise; + stateChange(callback: (state: RNFetchBlobResponseInfo) => void): StatefulPromise; + uploadProgress(config: {count?: number, interval?: number} | UploadProgressCallback, callback?: UploadProgressCallback): StatefulPromise; +} + +export type AddAndroidDownloads = { + description?: string, + mediaScannable?: boolean, + mime?: string, + notification?: boolean, + path?: string, + title?: string, + useDownloadManager?: boolean +}; +export type AndroidDownloadOption = { + description?: string, + mime?: string, + path: string, + showNotification?: boolean, + title?: string +}; +export type AndroidFsStat = { + external_free: number, + external_total: number, + internal_free: number, + internal_total: number +}; +export type Dirs = { + CacheDir: string, + DCIMDir: string, + DocumentDir: string, + DownloadDir: string, + LibraryDir: string, + MainBundleDir: string, + MovieDir: string, + MusicDir: string, + PictureDir: string, + SDCardApplicationDir: string, + SDCardDir: string +}; +export type Encoding = "utf8" | "ascii" | "base64"; +export type FormField = {data: string, filename?: string, name: string, type?: string}; +export type HashAlgorithm = "md5" | "sha1" | "sha224" | "sha256" | "sha384" | "sha512"; +export type IosFsStat = {free: number, total: number}; +export type Methods = "POST" | "GET" | "DELETE" | "PUT" | "post" | "get" | "delete" | "put"; +export type ProgressCallback = (received: number, total: number) => void; +export type RNFetchBlobConfig = { + IOSBackgroundTask?: boolean, + addAndroidDownloads?: AddAndroidDownloads, + appendExt?: string, + fileCache?: boolean, + indicator?: boolean, + overwrite?: boolean, + path?: string, + session?: string, + timeout?: number, + trusty?: boolean, + wifiOnly?: boolean, + followRedirect?: boolean +}; +export type RNFetchBlobResponseInfo = { + headers: {[fieldName: string]: string}, + redirects: string[], + respType: "text" | "blob" | "" | "json", + rnfbEncode: "path" | Encoding, + state: string, + status: number, + taskId: string, + timeout: boolean +}; +export type RNFetchBlobStat = { + filename: string, + lastModified: number, + path: string, + size: number, + type: "directory" | "file" | "asset" +}; +export type UploadProgressCallback = (sent: number, total: number) => void; + +declare export default RNFetchBlob; diff --git a/ios/IOS7Polyfill.h b/ios/IOS7Polyfill.h deleted file mode 100644 index 04a4bfaaf..000000000 --- a/ios/IOS7Polyfill.h +++ /dev/null @@ -1,27 +0,0 @@ -// -// IOS7Polyfill.h -// RNFetchBlob -// -// Created by Ben Hsieh on 2016/9/6. -// Copyright © 2016年 wkh237.github.io. All rights reserved. -// - -#ifndef IOS7Polyfill_h -#define IOS7Polyfill_h - -@interface NSString (Contains) - -- (BOOL)RNFBContainsString:(NSString*)other; - -@end - -@implementation NSString (Contains) - -- (BOOL)RNFBContainsString:(NSString*)other { - NSRange range = [self rangeOfString:other]; - return range.length != 0; -} - - -@end -#endif /* IOS7Polyfill_h */ diff --git a/ios/RNFetchBlob.xcodeproj/project.pbxproj b/ios/RNFetchBlob.xcodeproj/project.pbxproj index acd524413..e08dc8e1e 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 = ""; }; @@ -42,7 +45,6 @@ A19B48241D98102400E6868A /* RNFetchBlobProgress.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFetchBlobProgress.m; sourceTree = ""; }; A1AAE2971D300E3E0051D11C /* RNFetchBlobReqBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNFetchBlobReqBuilder.h; sourceTree = ""; }; A1AAE2981D300E4D0051D11C /* RNFetchBlobReqBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNFetchBlobReqBuilder.m; sourceTree = ""; }; - A1F950181D7E9134002A95A6 /* IOS7Polyfill.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IOS7Polyfill.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -68,11 +70,12 @@ children = ( A19B48241D98102400E6868A /* RNFetchBlobProgress.m */, A19B48231D98100800E6868A /* RNFetchBlobProgress.h */, - 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 +152,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 */, @@ -255,7 +259,7 @@ "$(SRCROOT)/../../React/**", "$(SRCROOT)/../../react-native/React/**", "$(SRCROOT)/../../node_modules/react-native/React/**", - "$(SRCROOT)/../../node_modules/react-native-fetch-blob/ios/RNFetchBlob", + "$(SRCROOT)/../../node_modules/rn-fetch-blob/ios/RNFetchBlob", "$(SRCROOT)/../../RNFetchBlobTest/node_modules/react-native/**", ); OTHER_LDFLAGS = "-ObjC"; @@ -276,7 +280,7 @@ "$(SRCROOT)/../../React/**", "$(SRCROOT)/../../react-native/React/**", "$(SRCROOT)/../../node_modules/react-native/React/**", - "$(SRCROOT)/../../node_modules/react-native-fetch-blob/ios/RNFetchBlob", + "$(SRCROOT)/../../node_modules/rn-fetch-blob/ios/RNFetchBlob", "$(SRCROOT)/../../RNFetchBlobTest/node_modules/react-native/**", ); OTHER_LDFLAGS = "-ObjC"; 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 47bda76c5..d82739399 100644 --- a/ios/RNFetchBlob/RNFetchBlob.m +++ b/ios/RNFetchBlob/RNFetchBlob.m @@ -38,10 +38,14 @@ - (dispatch_queue_t) methodQueue { + (RCTBridge *)getRCTBridge { - RCTRootView * rootView = [[UIApplication sharedApplication] keyWindow].rootViewController.view; + RCTRootView * rootView = (RCTRootView*) [[UIApplication sharedApplication] keyWindow].rootViewController.view; return rootView.bridge; } ++ (BOOL)requiresMainQueueSetup { + return NO; +} + RCT_EXPORT_MODULE(); - (id) init { @@ -64,9 +68,14 @@ - (id) init { - (NSDictionary *)constantsToExport { return @{ - @"MainBundleDir" : [RNFetchBlobFS getMainBundleDir], + @"CacheDir" : [RNFetchBlobFS getCacheDir], @"DocumentDir": [RNFetchBlobFS getDocumentDir], - @"CacheDir" : [RNFetchBlobFS getCacheDir] + @"DownloadDir" : [RNFetchBlobFS getDownloadDir], + @"LibraryDir" : [RNFetchBlobFS getLibraryDir], + @"MainBundleDir" : [RNFetchBlobFS getMainBundleDir], + @"MovieDir" : [RNFetchBlobFS getMovieDir], + @"MusicDir" : [RNFetchBlobFS getMusicDir], + @"PictureDir" : [RNFetchBlobFS getPictureDir], }; } @@ -96,8 +105,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 +141,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]; } }]; } @@ -148,7 +165,7 @@ - (NSDictionary *)constantsToExport fileContent = [[NSData alloc] initWithData:[data dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES]]; } else if([[encoding lowercaseString] isEqualToString:@"base64"]) { - fileContent = [[NSData alloc] initWithBase64EncodedData:data options:0]; + fileContent = [[NSData alloc] initWithBase64EncodedData:data options:NSDataBase64DecodingIgnoreUnknownCharacters]; } else if([[encoding lowercaseString] isEqualToString:@"uri"]) { NSString * orgPath = [data stringByReplacingOccurrencesOfString:FILE_PREFIX withString:@""]; @@ -216,6 +233,18 @@ - (NSDictionary *)constantsToExport } } +#pragma mark - fs.syncPathAppGroup +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(syncPathAppGroup:(NSString *)groupName) { + NSURL *pathUrl = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:groupName]; + NSString *path = [pathUrl path]; + + if(path) { + return path; + } else { + return @""; + } +} + #pragma mark - fs.exists RCT_EXPORT_METHOD(exists:(NSString *)path callback:(RCTResponseSenderBlock)callback) { [RNFetchBlobFS exists:path callback:callback]; @@ -518,7 +547,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]); } @@ -528,14 +557,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 @@ -575,9 +604,12 @@ - (NSDictionary *)constantsToExport if(scheme == nil || [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:scheme]]) { dispatch_sync(dispatch_get_main_queue(), ^{ - [documentController presentPreviewAnimated:YES]; + if([documentController presentPreviewAnimated:YES]) { + resolve(@[[NSNull null]]); + } else { + reject(@"EINVAL", @"document is not supported", nil); + } }); - resolve(@[[NSNull null]]); } else { reject(@"EINVAL", @"scheme is not supported", nil); } @@ -607,7 +639,12 @@ - (NSDictionary *)constantsToExport - (UIViewController *) documentInteractionControllerViewControllerForPreview: (UIDocumentInteractionController *) controller { UIWindow *window = [UIApplication sharedApplication].keyWindow; - return window.rootViewController; + UIViewController *currentlyPresentedView = [window.rootViewController presentedViewController]; + if (currentlyPresentedView == nil) + { + return window.rootViewController; + } + return currentlyPresentedView; } # pragma mark - check expired network events 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 6f7fef4b2..1376d692e 100644 --- a/ios/RNFetchBlobConst.m +++ b/ios/RNFetchBlobConst.m @@ -7,38 +7,39 @@ // #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_WIFI_ONLY = @"wifiOnly"; +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 7cf0e4faa..514120138 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; @@ -46,10 +46,15 @@ @property (nonatomic) BOOL appendData; // get dirs -+ (NSString *) getMainBundleDir; -+ (NSString *) getTempPath; + (NSString *) getCacheDir; + (NSString *) getDocumentDir; ++ (NSString *) getDownloadDir; ++ (NSString *) getLibraryDir; ++ (NSString *) getMainBundleDir; ++ (NSString *) getMovieDir; ++ (NSString *) getMusicDir; ++ (NSString *) getPictureDir; ++ (NSString *) getTempPath; + (NSString *) getTempPath:(NSString*)taskId withExtension:(NSString *)ext; + (NSString *) getPathOfAsset:(NSString *)assetURI; + (NSString *) getPathForAppGroup:(NSString *)groupName; diff --git a/ios/RNFetchBlobFS.m b/ios/RNFetchBlobFS.m index 1d16ffa40..c013d80e4 100644 --- a/ios/RNFetchBlobFS.m +++ b/ios/RNFetchBlobFS.m @@ -10,7 +10,6 @@ #import "RNFetchBlob.h" #import "RNFetchBlobFS.h" #import "RNFetchBlobConst.h" -#import "IOS7Polyfill.h" @import AssetsLibrary; #import @@ -104,6 +103,14 @@ + (NSString *) getDocumentDir { return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; } ++ (NSString *) getDownloadDir { + return [NSSearchPathForDirectoriesInDomains(NSDownloadsDirectory, NSUserDomainMask, YES) firstObject]; +} + ++ (NSString *) getLibraryDir { + return [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) firstObject]; +} + + (NSString *) getMusicDir { return [NSSearchPathForDirectoriesInDomains(NSMusicDirectory, NSUserDomainMask, YES) firstObject]; } @@ -367,7 +374,7 @@ + (void) writeFile:(NSString *)path NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:path]; NSData * content = nil; - if([encoding RNFBContainsString:@"base64"]) { + if([encoding containsString:@"base64"]) { content = [[NSData alloc] initWithBase64EncodedString:data options:0]; } else if([encoding isEqualToString:@"uri"]) { diff --git a/ios/RNFetchBlobNetwork.h b/ios/RNFetchBlobNetwork.h index d3b4654a5..ff4cd9c6c 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,28 @@ #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; +@property(nonnull, nonatomic) NSMutableDictionary *rebindProgressDict; +@property(nonnull, nonatomic) NSMutableDictionary *rebindUploadProgressDict; ++ (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 34ebabb95..f2e40b6b4 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,130 +32,45 @@ // //////////////////////////////////////// -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) { - @synchronized ([RNFetchBlobNetwork class]) { - 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; + self.rebindProgressDict = [NSMutableDictionary dictionary]; + self.rebindUploadProgressDict = [NSMutableDictionary dictionary]; } + return self; } -+ (void) enableProgressReport:(NSString *) taskId config:(RNFetchBlobProgress *)config -{ - if(progressTable == nil) - { - progressTable = [[NSMutableDictionary alloc] init]; - } - [progressTable setValue:config forKey:taskId]; -} ++ (RNFetchBlobNetwork* _Nullable)sharedInstance { + static id _sharedInstance = nil; + static dispatch_once_t onceToken; -+ (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 -{ - - 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]; + RNFetchBlobRequest *request = [[RNFetchBlobRequest alloc] init]; + [request sendRequest:options + contentLength:contentLength + bridge:bridgeRef + taskId:taskId + withRequest:req + taskOperationQueue:self.taskQueue + callback:callback]; - [taskTable setObject:@{ @"session" : task, @"isCancelled" : @NO } forKey:taskId]; - [task resume]; - - // network status indicator - if ([[options objectForKey:CONFIG_INDICATOR] boolValue] == YES) { - dispatch_async(dispatch_get_main_queue(), ^{ - [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES]; - }); + @synchronized([RNFetchBlobNetwork class]) { + [self.requestsTable setObject:request forKey:taskId]; + [self checkProgressConfig]; } - __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]; - - } - - // clear expired task entries - [expirationTable removeAllObjects]; - expirationTable = [[NSMapTable alloc] init]; - +- (void) checkProgressConfig { + //reconfig progress + [self.rebindProgressDict enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, RNFetchBlobProgress * _Nonnull config, BOOL * _Nonnull stop) { + [self enableProgressReport:key config:config]; + }]; + [self.rebindProgressDict removeAllObjects]; + + //reconfig uploadProgress + [self.rebindUploadProgressDict enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, RNFetchBlobProgress * _Nonnull config, BOOL * _Nonnull stop) { + [self enableUploadProgress:key config:config]; + }]; + [self.rebindUploadProgressDict removeAllObjects]; } -//////////////////////////////////////// -// -// 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:@""]; - } - } - } - 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]; + if (config) { + @synchronized ([RNFetchBlobNetwork class]) { + if (![self.requestsTable objectForKey:taskId]) { + [self.rebindProgressDict setValue:config forKey:taskId]; + } else { + [self.requestsTable objectForKey:taskId].progressConfig = config; } - 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 +- (void) enableUploadProgress:(NSString *) taskId config:(RNFetchBlobProgress *)config { - // 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 -{ - 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 = @""; - - dispatch_async(dispatch_get_main_queue(), ^{ - [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; - }); - - if(respInfo == nil) - { - respInfo = [NSNull null]; - } - - if(error != nil) - { - errMsg = [error localizedDescription]; - } - NSDictionary * taskSession = [taskTable objectForKey:taskId]; - BOOL isCancelled = [[taskSession valueForKey:@"isCancelled"] boolValue]; - if(isCancelled) { - errMsg = @"task cancelled"; - } - - 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]) { + if (![self.requestsTable objectForKey:taskId]) { + [self.rebindUploadProgressDict setValue:config forKey:taskId]; + } else { + [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 -{ - 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] - } - ]; - } } -+ (void) cancelRequest:(NSString *)taskId +- (void) cancelRequest:(NSString *)taskId { - NSDictionary * task = [taskTable objectForKey:taskId]; + NSURLSessionDataTask * task; - if(task != nil) { - NSURLSessionDataTask * session = [task objectForKey:@"session"]; - if(session.state == NSURLSessionTaskStateRunning) { - [task setValue:@NO forKey:@"isCancelled"]; - [session cancel]; - } + @synchronized ([RNFetchBlobNetwork class]) { + task = [self.requestsTable objectForKey:taskId].task; } -} - - -- (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]); + if (task && task.state == NSURLSessionTaskStateRunning) { + [task cancel]; } } - -- (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 b6048f553..68ee34f71 100644 --- a/ios/RNFetchBlobReqBuilder.m +++ b/ios/RNFetchBlobReqBuilder.m @@ -11,7 +11,6 @@ #import "RNFetchBlobNetwork.h" #import "RNFetchBlobConst.h" #import "RNFetchBlobFS.h" -#import "IOS7Polyfill.h" #if __has_include() #import @@ -69,7 +68,7 @@ +(void) buildMultipartRequest:(NSDictionary *)options [mheaders setValue:[NSString stringWithFormat:@"%lu",[postData length]] forKey:@"Content-Length"]; [mheaders setValue:@"100-continue" forKey:@"Expect"]; // appaned boundary to content-type - [mheaders setValue:[NSString stringWithFormat:@"multipart/form-data; charset=utf-8; boundary=%@", boundary] forKey:@"content-type"]; + [mheaders setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary] forKey:@"content-type"]; [request setHTTPMethod: method]; [request setAllHTTPHeaderFields:mheaders]; onComplete(request, [formData length]); @@ -117,7 +116,7 @@ +(void) buildOctetRequest:(NSDictionary *)options orgPath = [RNFetchBlobFS getPathOfAsset:orgPath]; if([orgPath hasPrefix:AL_PREFIX]) { - + [RNFetchBlobFS readFile:orgPath encoding:nil onComplete:^(id content, NSString* code, NSString * err) { if(err != nil) { @@ -131,7 +130,7 @@ +(void) buildOctetRequest:(NSDictionary *)options onComplete(request, [((NSData *)content) length]); } }]; - + return; } size = [[[NSFileManager defaultManager] attributesOfItemAtPath:orgPath error:nil] fileSize]; @@ -150,7 +149,7 @@ +(void) buildOctetRequest:(NSDictionary *)options __block NSString * cType = [[self class]getHeaderIgnoreCases:@"content-type" fromHeaders:mheaders]; // when content-type is application/octet* decode body string using BASE64 decoder - if([[cType lowercaseString] hasPrefix:@"application/octet"] || [[cType lowercaseString] RNFBContainsString:@";base64"]) + if([[cType lowercaseString] hasPrefix:@"application/octet"] || [[cType lowercaseString] containsString:@";base64"]) { __block NSString * ncType = [[cType stringByReplacingOccurrencesOfString:@";base64" withString:@""]stringByReplacingOccurrencesOfString:@";BASE64" withString:@""]; if([mheaders valueForKey:@"content-type"] != nil) @@ -277,7 +276,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..ac9e03ae2 --- /dev/null +++ b/ios/RNFetchBlobRequest.m @@ -0,0 +1,484 @@ +// +// 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 + + +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; + } + + 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 { + destPath = [RNFetchBlobFS getTempPath:cacheKey withExtension:[self.options valueForKey:CONFIG_FILE_EXT]]; + } + } else { + respData = [[NSMutableData alloc] init]; + respFile = NO; + } + + NSURLSessionDataTask *task = [session dataTaskWithRequest:req]; + [task resume]; + self.task = task; + + // network status indicator + if ([[options objectForKey:CONFIG_INDICATOR] boolValue]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [[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] containsString:@"multipart/x-mixed-replace;"]; + } + + if(respCType) + { + NSArray * extraBlobCTypes = [options objectForKey:CONFIG_EXTRA_BLOB_CTYPE]; + + if ([respCType containsString:@"text/"]) { + respType = @"text"; + } else if ([respCType containsString:@"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 containsString:[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 containsString:@"?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:@"%lld", (long long) receivedBytes], + @"total": [NSString stringWithFormat:@"%lld", (long 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; + + // 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"; + } else { + 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 a93dba81d..7de11230e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "react-native-fetch-blob", - "version": "0.10.8", + "name": "rn-fetch-blob", + "version": "0.13.0-beta.1", "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,8 @@ }, "dependencies": { "base-64": "0.1.0", - "glob": "7.0.6" + "glob": "7.0.6", + "lodash": "4.17.15" }, "keywords": [ "react-native", @@ -21,18 +22,13 @@ "filestream", "image header" ], - "rnpm": { - "commands": { - "prelink": "node ./node_modules/react-native-fetch-blob/scripts/prelink.js" - } - }, "repository": { - "url": "https://github.com/wkh237/react-native-fetch-blob.git" + "url": "https://github.com/joltup/rn-fetch-blob.git" }, - "author": "wkh237 ", + "author": "Joltup", "license": "MIT", "contributors": [ "Ben ", - "" + "wkh237 " ] } diff --git a/polyfill/Blob.js b/polyfill/Blob.js index 53662a798..c99754dc2 100644 --- a/polyfill/Blob.js +++ b/polyfill/Blob.js @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style license that can be // found in the LICENSE file. -import RNFetchBlob from '../index.js' +import {NativeModules} from 'react-native'; import fs from '../fs.js' import getUUID from '../utils/uuid' import Log from '../utils/log.js' import EventTarget from './EventTarget' +const RNFetchBlob = NativeModules.RNFetchBlob const log = new Log('Blob') const blobCacheDir = fs.dirs.DocumentDir + '/RNFetchBlob-blobs/' diff --git a/polyfill/Fetch.js b/polyfill/Fetch.js index 7be52e084..ac02a5d07 100644 --- a/polyfill/Fetch.js +++ b/polyfill/Fetch.js @@ -1,9 +1,10 @@ -import RNFetchBlob from '../index.js' +import {NativeModules} from 'react-native'; import Log from '../utils/log.js' import fs from '../fs' import unicode from '../utils/unicode' import Blob from './Blob' +const RNFetchBlob = NativeModules.RNFetchBlob const log = new Log('FetchPolyfill') log.disable() @@ -40,6 +41,7 @@ class RNFetchBlobFetchPolyfill { promise = Blob.build(body).then((b) => { blobCache = b options.headers['Content-Type'] = 'multipart/form-data;boundary=' + b.multipartBoundary + options.headers['content-type'] = 'multipart/form-data;boundary=' + b.multipartBoundary return Promise.resolve(RNFetchBlob.wrap(b._ref)) }) } @@ -73,7 +75,7 @@ class RNFetchBlobFetchPolyfill { // release blob cache created when sending request if(blobCache !== null && blobCache instanceof Blob) blobCache.close() - return Promise.resolve(new RNFetchBlobFetchRepsonse(resp)) + return Promise.resolve(new RNFetchBlobFetchResponse(resp)) }) }) @@ -97,7 +99,7 @@ class RNFetchBlobFetchPolyfill { } -class RNFetchBlobFetchRepsonse { +class RNFetchBlobFetchResponse { constructor(resp:FetchBlobResponse) { let info = resp.info() diff --git a/polyfill/FileReader.js b/polyfill/FileReader.js index b72df17f7..ef0ebfc0b 100644 --- a/polyfill/FileReader.js +++ b/polyfill/FileReader.js @@ -2,7 +2,6 @@ // Use of this source code is governed by a MIT-style license that can be // found in the LICENSE file. -import RNFetchBlob from '../index.js' import ProgressEvent from './ProgressEvent.js' import EventTarget from './EventTarget' import Blob from './Blob' diff --git a/polyfill/XMLHttpRequest.js b/polyfill/XMLHttpRequest.js index 9036b2bc8..d52e47e19 100644 --- a/polyfill/XMLHttpRequest.js +++ b/polyfill/XMLHttpRequest.js @@ -2,13 +2,14 @@ // Use of this source code is governed by a MIT-style license that can be // found in the LICENSE file. -import RNFetchBlob from '../index.js' +import {NativeModules} from 'react-native'; import XMLHttpRequestEventTarget from './XMLHttpRequestEventTarget.js' import Log from '../utils/log.js' import Blob from './Blob.js' import ProgressEvent from './ProgressEvent.js' import URIUtil from '../utils/uri' +const RNFetchBlob = NativeModules.RNFetchBlob const log = new Log('XMLHttpRequest') log.disable() diff --git a/react-native-fetch-blob.podspec b/react-native-fetch-blob.podspec deleted file mode 100644 index f702d287b..000000000 --- a/react-native-fetch-blob.podspec +++ /dev/null @@ -1,13 +0,0 @@ -Pod::Spec.new do |s| - s.name = "react-native-fetch-blob" - s.version = "0.10.6" - s.summary = "A project committed to make file acess and data transfer easier, effiecient for React Native developers." - s.requires_arc = true - s.license = 'MIT' - s.homepage = 'n/a' - s.authors = { "wkh237" => "xeiyan@gmail.com" } - s.source = { :git => "https://github.com/wkh237/react-native-fetch-blob", :tag => 'v0.10.6'} - s.source_files = 'ios/**/*.{h,m}' - s.platform = :ios, "7.0" - s.dependency 'React/Core' -end diff --git a/rn-fetch-blob.podspec b/rn-fetch-blob.podspec new file mode 100644 index 000000000..4edbd5cc9 --- /dev/null +++ b/rn-fetch-blob.podspec @@ -0,0 +1,16 @@ +require "json" +package = JSON.parse(File.read('package.json')) + +Pod::Spec.new do |s| + s.name = package['name'] + s.version = package['version'] + s.summary = package['description'] + s.requires_arc = true + s.license = 'MIT' + s.homepage = 'n/a' + s.source = { :git => "https://github.com/joltup/rn-fetch-blob" } + s.author = 'Joltup' + s.source_files = 'ios/**/*.{h,m}' + s.platform = :ios, "8.0" + s.dependency 'React-Core' +end diff --git a/scripts/prelink.js b/scripts/prelink.js deleted file mode 100644 index 435cbc54d..000000000 --- a/scripts/prelink.js +++ /dev/null @@ -1,71 +0,0 @@ -try { - var fs = require('fs'); - var glob = require('glob'); - var addAndroidPermissions = process.env.RNFB_ANDROID_PERMISSIONS == 'true'; - var MANIFEST_PATH = glob.sync(process.cwd() + '/android/app/src/main/**/AndroidManifest.xml')[0]; - var PACKAGE_JSON = process.cwd() + '/package.json'; - var package = JSON.parse(fs.readFileSync(PACKAGE_JSON)); - var APP_NAME = package.name; - var PACKAGE_GRADLE = process.cwd() + '/node_modules/react-native-fetch-blob/android/build.gradle' - var VERSION = checkVersion(); - - console.log('RNFetchBlob detected app version => ' + VERSION); - - if(VERSION < 0.28) { - console.log('You project version is '+ VERSION + ' which may not compatible to react-native-fetch-blob 7.0+, please consider upgrade your application template to react-native 0.27+.') - // add OkHttp3 dependency fo pre 0.28 project - var main = fs.readFileSync(PACKAGE_GRADLE); - console.log('adding OkHttp3 dependency to pre 0.28 project .. ') - main = String(main).replace('//{RNFetchBlob_PRE_0.28_DEPDENDENCY}', "compile 'com.squareup.okhttp3:okhttp:3.4.1'"); - fs.writeFileSync(PACKAGE_GRADLE, main); - console.log('adding OkHttp3 dependency to pre 0.28 project .. ok') - } - - console.log('Add Android permissions => ' + (addAndroidPermissions == "true")) - - if(addAndroidPermissions) { - - // set file access permission for Android < 6.0 - fs.readFile(MANIFEST_PATH, function(err, data) { - - if(err) - console.log('failed to locate AndroidManifest.xml file, you may have to add file access permission manually.'); - else { - - console.log('RNFetchBlob patching AndroidManifest.xml .. '); - // append fs permission - data = String(data).replace( - '', - '\n ' - ) - // append DOWNLOAD_COMPLETE intent permission - data = String(data).replace( - '', - '\n ' - ) - fs.writeFileSync(MANIFEST_PATH, data); - console.log('RNFetchBlob patching AndroidManifest.xml .. ok'); - - } - - }) - } - else { - console.log( - '\033[95mreact-native-fetch-blob \033[97mwill not automatically add Android permissions after \033[92m0.9.4 '+ - '\033[97mplease run the following command if you want to add default permissions :\n\n' + - '\033[96m\tRNFB_ANDROID_PERMISSIONS=true react-native link \n') - } - - function checkVersion() { - console.log('RNFetchBlob checking app version ..'); - return parseFloat(/\d\.\d+(?=\.)/.exec(package.dependencies['react-native'])); - } - -} catch(err) { - console.log( - '\033[95mreact-native-fetch-blob\033[97m link \033[91mFAILED \033[97m\nCould not automatically link package :'+ - err.stack + - 'please follow the instructions to manually link the library : ' + - '\033[4mhttps://github.com/wkh237/react-native-fetch-blob/wiki/Manually-Link-Package\n') -} 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 = {