UNPKG

react-native-blob-util

Version:

A module provides upload, download, and files access API. Supports file stream read/write for process large files.

351 lines (312 loc) 12.9 kB
import {ReactNativeBlobUtilConfig} from './types'; import URIUtil from './utils/uri'; import fs from './fs'; import getUUID from './utils/uuid'; import {NativeEventEmitter} from 'react-native'; import {FetchBlobResponse} from './class/ReactNativeBlobUtilBlobResponse'; import CanceledFetchError from './class/ReactNativeBlobUtilCanceledFetchError'; import ReactNativeBlobUtil from './codegenSpecs/NativeBlobUtils'; const eventEmitter = new NativeEventEmitter(ReactNativeBlobUtil); // register message channel event handler. eventEmitter.addListener('ReactNativeBlobUtilMessage', (e) => { if (typeof e === 'string') e = JSON.parse(e); if (e.event === 'warn') { console.warn(e.detail); } else if (e.event === 'error') { throw e.detail; } else { console.log('ReactNativeBlobUtil native message', e.detail); } }); /** * Calling this method will inject configurations into followed `fetch` method. * @param {ReactNativeBlobUtilConfig} options * Fetch API configurations, contains the following options : * @property {boolean} fileCache * When fileCache is `true`, response data will be saved in * storage with a random generated file name, rather than * a BASE64 encoded string. * @property {string} appendExt * Set this property to change file extension of random- * generated file name. * @property {string} path * If this property has a valid string format, resonse data * will be saved to specific file path. Default string format * is : `ReactNativeBlobUtil-file://path-to-file` * @property {string} key * If this property is set, it will be converted to md5, to * check if a file with this name exists. * If it exists, the absolute path is returned (no network * 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 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. */ export function config(options: ReactNativeBlobUtilConfig) { return {fetch: fetch.bind(options)}; } /** * Fetch from file system, use the same interface as RNFB.fetch * @param {ReactNativeBlobUtilConfig} [options={}] Fetch configurations * @param {string} method Should be one of `get`, `post`, `put` * @param {string} url A file URI string * @param {string} headers Arguments of file system API * @param {any} body Data to put or post to file systen. * @return {Promise} */ function fetchFile(options = {}, method, url, headers = {}, body): Promise { if (!URIUtil.isFileURI(url)) { throw `could not fetch file from an invalid URI : ${url}`; } url = URIUtil.unwrapFileURI(url); let promise = null, cursor = 0, total = -1, cacheData = '', info = null, _progress, _uploadProgress, _stateChange; switch (method.toLowerCase()) { case 'post': break; case 'put': break; // read data from file system default: promise = fs.stat(url) .then((stat) => { total = stat.size; return fs.readStream(url, headers.encoding || 'utf8', Math.floor(headers.bufferSize) || 409600, Math.floor(headers.interval) || 100 ); }) .then((stream) => new Promise((resolve, reject) => { stream.open(); info = { state: '2', headers: {'source': 'system-fs'}, status: 200, respType: 'text', rnfbEncode: headers.encoding || 'utf8' }; _stateChange(info); stream.onData((chunk) => { _progress && _progress(cursor, total, chunk); if (headers.noCache) return; cacheData += chunk; }); stream.onError((err) => { reject(err); }); stream.onEnd(() => { resolve(new FetchBlobResponse(null, info, cacheData)); }); })); break; } promise.progress = (fn) => { _progress = fn; return promise; }; promise.stateChange = (fn) => { _stateChange = fn; return promise; }; promise.uploadProgress = (fn) => { _uploadProgress = fn; return promise; }; return promise; } /** * Create a HTTP request by settings, the `this` context is a `ReactNativeBlobUtilConfig` object. * @param {string} method HTTP method, should be `GET`, `POST`, `PUT`, `DELETE` * @param {string} url Request target url string. * @param {object} headers HTTP request headers. * @param {string} body * Request body, can be either a BASE64 encoded data string, * or a file path with prefix `ReactNativeBlobUtil-file://` (can be changed) * @return {Promise} * This promise instance also contains a Customized method `progress`for * register progress event handler. */ export function fetch(...args: any): Promise { // create task ID for receiving progress event let taskId = getUUID(); let options = this || {}; let subscription, subscriptionUpload, stateEvent, partEvent; let respInfo = {'uninit': true}; let [method, url, headers, body] = [...args]; // # 241 normalize null or undefined headers, in case nil or null string // pass to native context headers = headers && Object.keys(headers).reduce((result, key) => { result[key] = headers[key] || ''; return result; }, {}); // fetch from file system if (URIUtil.isFileURI(url)) { 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 subscription = eventEmitter.addListener('ReactNativeBlobUtilProgress', (e) => { if (typeof e === 'string') e = JSON.parse(e); if (e.taskId === taskId && promise.onProgress) { promise.onProgress(e.written, e.total, e.chunk); } }); subscriptionUpload = eventEmitter.addListener('ReactNativeBlobUtilProgress-upload', (e) => { if (typeof e === 'string') e = JSON.parse(e); if (e.taskId === taskId && promise.onUploadProgress) { promise.onUploadProgress(e.written, e.total); } }); stateEvent = eventEmitter.addListener('ReactNativeBlobUtilState', (e) => { if (typeof e === 'string') e = JSON.parse(e); if (e.taskId === taskId) respInfo = e; promise.onStateChange && promise.onStateChange(e); }); subscription = eventEmitter.addListener('ReactNativeBlobUtilExpire', (e) => { if (typeof e === 'string') e = JSON.parse(e); if (e.taskId === taskId && promise.onExpire) { promise.onExpire(e); } }); partEvent = eventEmitter.addListener('ReactNativeBlobUtilServerPush', (e) => { if (typeof e === 'string') e = JSON.parse(e); if (e.taskId === taskId && promise.onPartData) { promise.onPartData(e.chunk); } }); // When the request body comes from Blob polyfill, we should use special its ref // as the request body if (body instanceof Blob && body.isReactNativeBlobUtilPolyfill) { body = body.getReactNativeBlobUtilRef(); } let req = ReactNativeBlobUtil[nativeMethodName]; /** * Send request via native module, the response callback accepts three arguments * @callback * @param err {any} Error message or object, when the request success, this * parameter should be `null`. * @param rawType { 'utf8' | 'base64' | 'path'} RNFB request will be stored * as UTF8 string, BASE64 string, or a file path reference * in JS context, and this parameter indicates which one * dose the response data presents. * @param data {string} Response data or its reference. * @param responseInfo {Object.<>} */ req(options, taskId, method, url, headers || {}, body, (err, rawType, data, responseInfo) => { // task done, remove event listeners subscription.remove(); subscriptionUpload.remove(); stateEvent.remove(); partEvent.remove(); delete promise.progress; delete promise.uploadProgress; delete promise.stateChange; delete promise.part; delete promise.cancel; // delete promise['expire'] promise.cancel = () => { }; if(!responseInfo) responseInfo = {}; // should not be null / undefined if (err) reject(new Error(err, respInfo)); else { // response data is saved to storage, create a session for it if (options.path || options.fileCache || options.addAndroidDownloads || options.key || options.auto && respInfo.respType === 'blob') { if (options.session) fs.session(options.session).add(data); } if ('uninit' in respInfo && respInfo.uninit) // event didn't fire yet so we override it here respInfo = responseInfo; respInfo.rnfbEncode = rawType; resolve(new FetchBlobResponse(taskId, respInfo, data)); } }); }); // extend Promise object, add `progress`, `uploadProgress`, and `cancel` // method for register progress event handler and cancel request. // Add second parameter for performance purpose #140 // When there's only one argument pass to this method, use default `interval` // and `count`, otherwise use the given on. // TODO : code refactor, move `uploadProgress` and `progress` to StatefulPromise promise.progress = (...args) => { let interval = 250; let count = -1; let fn = () => { }; if (args.length === 2) { interval = args[0].interval || interval; count = args[0].count || count; fn = args[1]; } else { fn = args[0]; } promise.onProgress = fn; ReactNativeBlobUtil.enableProgressReport(taskId, interval, count); return promise; }; promise.uploadProgress = (...args) => { let interval = 250; let count = -1; let fn = () => { }; if (args.length === 2) { interval = args[0].interval || interval; count = args[0].count || count; fn = args[1]; } else { fn = args[0]; } promise.onUploadProgress = fn; ReactNativeBlobUtil.enableUploadProgressReport(taskId, interval, count); return promise; }; promise.part = (fn) => { promise.onPartData = fn; return promise; }; promise.stateChange = (fn) => { promise.onStateChange = fn; return promise; }; promise.expire = (fn) => { promise.onExpire = fn; return promise; }; promise.cancel = (fn) => { fn = fn || function () { }; subscription.remove(); subscriptionUpload.remove(); stateEvent.remove(); ReactNativeBlobUtil.cancelRequest(taskId, fn); promiseReject(new CanceledFetchError('canceled')); }; promise.taskId = taskId; return promise; }