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
JavaScript
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;
}