react-native-fetch-blob
Version:
A module provides upload, download, and files access API. Supports file stream read/write for process large files.
568 lines (520 loc) • 16.6 kB
JavaScript
// Copyright 2016 wkh237@github. All rights reserved.
// Use of this source code is governed by a MIT-style license that can be
// found in the LICENSE file.
import {
NativeModules,
DeviceEventEmitter,
NativeAppEventEmitter,
Platform,
AsyncStorage,
AppState,
} from 'react-native'
import type {
RNFetchBlobNative,
RNFetchBlobConfig,
RNFetchBlobStream,
RNFetchBlobResponseInfo
} from './types'
import URIUtil from './utils/uri'
import StatefulPromise from './class/StatefulPromise.js'
import fs from './fs'
import getUUID from './utils/uuid'
import base64 from 'base-64'
import polyfill from './polyfill'
import _ from 'lodash'
import android from './android'
import ios from './ios'
import JSONStream from './json-stream'
const {
RNFetchBlobSession,
readStream,
createFile,
unlink,
exists,
mkdir,
session,
writeStream,
readFile,
ls,
isDir,
mv,
cp
} = fs
const Blob = polyfill.Blob
const emitter = DeviceEventEmitter
const RNFetchBlob = NativeModules.RNFetchBlob
// when app resumes, check if there's any expired network task and trigger
// their .expire event
if(Platform.OS === 'ios') {
AppState.addEventListener('change', (e) => {
if(e === 'active')
RNFetchBlob.emitExpiredEvent(()=>{})
})
}
// register message channel event handler.
emitter.addListener("RNFetchBlobMessage", (e) => {
if(e.event === 'warn') {
console.warn(e.detail)
}
else if (e.event === 'error') {
throw e.detail
}
else {
console.log("RNFetchBlob native message", e.detail)
}
})
// 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.',
'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
}
/**
* Calling this method will inject configurations into followed `fetch` method.
* @param {RNFetchBlobConfig} 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 : `RNFetchBlob-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 30000ms.
*
* @return {function} This method returns a `fetch` method instance.
*/
function config (options:RNFetchBlobConfig) {
return { fetch : fetch.bind(options) }
}
/**
* Fetch from file system, use the same interface as RNFB.fetch
* @param {RNFetchBlobConfig} [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
let cursor = 0
let total = -1
let cacheData = ''
let info = null
let _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 `RNFetchBlobConfig` 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 `RNFetchBlob-file://` (can be changed)
* @return {Promise}
* This promise instance also contains a Customized method `progress`for
* register progress event handler.
*/
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 = {}
let [method, url, headers, body] = [...args]
// # 241 normalize null or undefined headers, in case nil or null string
// pass to native context
headers = _.reduce(headers, (result, value, key) => {
result[key] = value || ''
return result
}, {});
// fetch from file system
if(URIUtil.isFileURI(url)) {
return fetchFile(options, method, url, headers, body)
}
// from remote HTTP(S)
let promise = new Promise((resolve, reject) => {
let nativeMethodName = Array.isArray(body) ? 'fetchBlobForm' : 'fetchBlob'
// on progress event listener
subscription = emitter.addListener('RNFetchBlobProgress', (e) => {
if(e.taskId === taskId && promise.onProgress) {
promise.onProgress(e.written, e.total, e.chunk)
}
})
subscriptionUpload = emitter.addListener('RNFetchBlobProgress-upload', (e) => {
if(e.taskId === taskId && promise.onUploadProgress) {
promise.onUploadProgress(e.written, e.total)
}
})
stateEvent = emitter.addListener('RNFetchBlobState', (e) => {
if(e.taskId === taskId)
respInfo = e
promise.onStateChange && promise.onStateChange(e)
})
subscription = emitter.addListener('RNFetchBlobExpire', (e) => {
if(e.taskId === taskId && promise.onExpire) {
promise.onExpire(e)
}
})
partEvent = emitter.addListener('RNFetchBlobServerPush', (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.isRNFetchBlobPolyfill) {
body = body.getRNFetchBlobRef()
}
let req = RNFetchBlob[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.
*/
req(options, taskId, method, url, headers || {}, body, (err, rawType, data) => {
// 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(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)
session(options.session).add(data)
}
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
RNFetchBlob.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
RNFetchBlob.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()
RNFetchBlob.cancelRequest(taskId, fn)
}
promise.taskId = taskId
return promise
}
/**
* RNFetchBlob response object class.
*/
class FetchBlobResponse {
taskId : string;
path : () => string | null;
type : 'base64' | 'path' | 'utf8';
data : any;
blob : (contentType:string, sliceSize:number) => Promise<Blob>;
text : () => string | Promise<any>;
json : () => any;
base64 : () => any;
flush : () => void;
respInfo : RNFetchBlobResponseInfo;
session : (name:string) => RNFetchBlobSession | null;
readFile : (encode: 'base64' | 'utf8' | 'ascii') => ?Promise<any>;
readStream : (
encode: 'utf8' | 'ascii' | 'base64',
) => RNFetchBlobStream | null;
constructor(taskId:string, info:RNFetchBlobResponseInfo, data:any) {
this.data = data
this.taskId = taskId
this.type = info.rnfbEncode
this.respInfo = info
this.info = ():RNFetchBlobResponseInfo => {
return this.respInfo
}
this.array = ():Promise<Array> => {
let cType = info.headers['Content-Type'] || info.headers['content-type']
return new Promise((resolve, reject) => {
switch(this.type) {
case 'base64':
// TODO : base64 to array buffer
break
case 'path':
fs.readFile(this.data, 'ascii').then(resolve)
break
default:
// TODO : text to array buffer
break
}
})
}
/**
* Convert result to javascript RNFetchBlob object.
* @return {Promise<Blob>} Return a promise resolves Blob object.
*/
this.blob = ():Promise<Blob> => {
let Blob = polyfill.Blob
let cType = info.headers['Content-Type'] || info.headers['content-type']
return new Promise((resolve, reject) => {
switch(this.type) {
case 'base64':
Blob.build(this.data, { type : cType + ';BASE64' }).then(resolve)
break
case 'path':
polyfill.Blob.build(wrap(this.data), { type : cType }).then(resolve)
break
default:
polyfill.Blob.build(this.data, { type : 'text/plain' }).then(resolve)
break
}
})
}
/**
* Convert result to text.
* @return {string} Decoded base64 string.
*/
this.text = ():string | Promise<any> => {
let res = this.data
switch(this.type) {
case 'base64':
return base64.decode(this.data)
case 'path':
return fs.readFile(this.data, 'base64').then((b64) => Promise.resolve(base64.decode(b64)))
default:
return this.data
}
}
/**
* Convert result to JSON object.
* @return {object} Parsed javascript object.
*/
this.json = ():any => {
switch(this.type) {
case 'base64':
return JSON.parse(base64.decode(this.data))
case 'path':
return fs.readFile(this.data, 'utf8')
.then((text) => Promise.resolve(JSON.parse(text)))
default:
return JSON.parse(this.data)
}
}
/**
* Return BASE64 string directly.
* @return {string} BASE64 string of response body.
*/
this.base64 = ():string | Promise<any> => {
switch(this.type) {
case 'base64':
return this.data
case 'path':
return fs.readFile(this.data, 'base64')
default:
return base64.encode(this.data)
}
}
/**
* Remove cahced file
* @return {Promise}
*/
this.flush = () => {
let path = this.path()
if(!path || this.type !== 'path')
return
return unlink(path)
}
/**
* get path of response temp file
* @return {string} File path of temp file.
*/
this.path = () => {
if(this.type === 'path')
return this.data
return null
}
this.session = (name:string):RNFetchBlobSession | null => {
if(this.type === 'path')
return session(name).add(this.data)
else {
console.warn('only file paths can be add into session.')
return null
}
}
/**
* Start read stream from cached file
* @param {String} encoding Encode type, should be one of `base64`, `ascrii`, `utf8`.
* @param {Function} fn On data event handler
* @return {void}
*/
this.readStream = (encode: 'base64' | 'utf8' | 'ascii'):RNFetchBlobStream | null => {
if(this.type === 'path') {
return readStream(this.data, encode)
}
else {
console.warn('RNFetchblob', 'this response data does not contains any available stream')
return null
}
}
/**
* Read file content with given encoding, if the response does not contains
* a file path, show warning message
* @param {String} encoding Encode type, should be one of `base64`, `ascrii`, `utf8`.
* @return {String}
*/
this.readFile = (encode: 'base64' | 'utf8' | 'ascii') => {
if(this.type === 'path') {
encode = encode || 'utf8'
return readFile(this.data, encode)
}
else {
console.warn('RNFetchblob', 'this response does not contains a readable file')
return null
}
}
}
}
export default {
fetch,
base64,
android,
ios,
config,
session,
fs,
wrap,
polyfill,
JSONStream
}