Ingen beskrivning

index.js 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. // Copyright 2016 wkh237@github. All rights reserved.
  2. // Use of this source code is governed by a MIT-style license that can be
  3. // found in the LICENSE file.
  4. // @flow
  5. import {
  6. NativeModules,
  7. DeviceEventEmitter,
  8. NativeAppEventEmitter,
  9. Platform,
  10. AsyncStorage,
  11. AppState,
  12. } from 'react-native'
  13. import type {
  14. RNFetchBlobNative,
  15. RNFetchBlobConfig,
  16. RNFetchBlobStream,
  17. RNFetchBlobResponseInfo
  18. } from './types'
  19. import URIUtil from './utils/uri'
  20. import StatefulPromise from './class/StatefulPromise.js'
  21. import fs from './fs'
  22. import getUUID from './utils/uuid'
  23. import base64 from 'base-64'
  24. import polyfill from './polyfill'
  25. import android from './android'
  26. import ios from './ios'
  27. import net from './net'
  28. import JSONStream from './json-stream'
  29. const {
  30. RNFetchBlobSession,
  31. readStream,
  32. createFile,
  33. unlink,
  34. exists,
  35. mkdir,
  36. session,
  37. writeStream,
  38. readFile,
  39. ls,
  40. isDir,
  41. mv,
  42. cp
  43. } = fs
  44. const Blob = polyfill.Blob
  45. const emitter = DeviceEventEmitter
  46. const RNFetchBlob = NativeModules.RNFetchBlob
  47. AppState.addEventListener('change', (e) => {
  48. console.log('app state changed', e)
  49. if(e === 'active')
  50. RNFetchBlob.emitExpiredEvent(()=>{})
  51. })
  52. // register message channel event handler.
  53. emitter.addListener("RNFetchBlobMessage", (e) => {
  54. if(e.event === 'warn') {
  55. console.warn(e.detail)
  56. }
  57. else if (e.event === 'error') {
  58. throw e.detail
  59. }
  60. else {
  61. console.log("RNFetchBlob native message", e.detail)
  62. }
  63. })
  64. // Show warning if native module not detected
  65. if(!RNFetchBlob || !RNFetchBlob.fetchBlobForm || !RNFetchBlob.fetchBlob) {
  66. console.warn(
  67. 'react-native-fetch-blob could not find valid native module.',
  68. 'please make sure you have linked native modules using `rnpm link`,',
  69. 'and restart RN packager or manually compile IOS/Android project.'
  70. )
  71. }
  72. function wrap(path:string):string {
  73. return 'RNFetchBlob-file://' + path
  74. }
  75. /**
  76. * Calling this method will inject configurations into followed `fetch` method.
  77. * @param {RNFetchBlobConfig} options
  78. * Fetch API configurations, contains the following options :
  79. * @property {boolean} fileCache
  80. * When fileCache is `true`, response data will be saved in
  81. * storage with a random generated file name, rather than
  82. * a BASE64 encoded string.
  83. * @property {string} appendExt
  84. * Set this property to change file extension of random-
  85. * generated file name.
  86. * @property {string} path
  87. * If this property has a valid string format, resonse data
  88. * will be saved to specific file path. Default string format
  89. * is : `RNFetchBlob-file://path-to-file`
  90. * @property {string} key
  91. * If this property is set, it will be converted to md5, to
  92. * check if a file with this name exists.
  93. * If it exists, the absolute path is returned (no network
  94. * activity takes place )
  95. * If it doesn't exist, the file is downloaded as usual
  96. * @property {number} timeout
  97. * Request timeout in millionseconds, by default it's 30000ms.
  98. *
  99. * @return {function} This method returns a `fetch` method instance.
  100. */
  101. function config (options:RNFetchBlobConfig) {
  102. return { fetch : fetch.bind(options) }
  103. }
  104. /**
  105. * Fetch from file system, use the same interface as RNFB.fetch
  106. * @param {RNFetchBlobConfig} [options={}] Fetch configurations
  107. * @param {string} method Should be one of `get`, `post`, `put`
  108. * @param {string} url A file URI string
  109. * @param {string} headers Arguments of file system API
  110. * @param {any} body Data to put or post to file systen.
  111. * @return {Promise}
  112. */
  113. function fetchFile(options = {}, method, url, headers = {}, body):Promise {
  114. if(!URIUtil.isFileURI(url)) {
  115. throw `could not fetch file from an invalid URI : ${url}`
  116. }
  117. url = URIUtil.unwrapFileURI(url)
  118. let promise = null
  119. let cursor = 0
  120. let total = -1
  121. let cacheData = ''
  122. let info = null
  123. let _progress, _uploadProgress, _stateChange
  124. switch(method.toLowerCase()) {
  125. case 'post':
  126. break
  127. case 'put':
  128. break
  129. // read data from file system
  130. default:
  131. promise = fs.stat(url)
  132. .then((stat) => {
  133. total = stat.size
  134. return fs.readStream(url,
  135. headers.encoding || 'utf8',
  136. Math.floor(headers.bufferSize) || 409600,
  137. Math.floor(headers.interval) || 100
  138. )
  139. })
  140. .then((stream) => new Promise((resolve, reject) => {
  141. stream.open()
  142. info = {
  143. state : "2",
  144. headers : { 'source' : 'system-fs' },
  145. status : 200,
  146. respType : 'text',
  147. rnfbEncode : headers.encoding || 'utf8'
  148. }
  149. _stateChange(info)
  150. stream.onData((chunk) => {
  151. _progress && _progress(cursor, total, chunk)
  152. if(headers.noCache)
  153. return
  154. cacheData += chunk
  155. })
  156. stream.onError((err) => { reject(err) })
  157. stream.onEnd(() => {
  158. resolve(new FetchBlobResponse(null, info, cacheData))
  159. })
  160. }))
  161. break
  162. }
  163. promise.progress = (fn) => {
  164. _progress = fn
  165. return promise
  166. }
  167. promise.stateChange = (fn) => {
  168. _stateChange = fn
  169. return promise
  170. }
  171. promise.uploadProgress = (fn) => {
  172. _uploadProgress = fn
  173. return promise
  174. }
  175. return promise
  176. }
  177. /**
  178. * Create a HTTP request by settings, the `this` context is a `RNFetchBlobConfig` object.
  179. * @param {string} method HTTP method, should be `GET`, `POST`, `PUT`, `DELETE`
  180. * @param {string} url Request target url string.
  181. * @param {object} headers HTTP request headers.
  182. * @param {string} body
  183. * Request body, can be either a BASE64 encoded data string,
  184. * or a file path with prefix `RNFetchBlob-file://` (can be changed)
  185. * @return {Promise}
  186. * This promise instance also contains a Customized method `progress`for
  187. * register progress event handler.
  188. */
  189. function fetch(...args:any):Promise {
  190. // create task ID for receiving progress event
  191. let taskId = getUUID()
  192. let options = this || {}
  193. let subscription, subscriptionUpload, stateEvent, partEvent
  194. let respInfo = {}
  195. let [method, url, headers, body] = [...args]
  196. // fetch from file system
  197. if(URIUtil.isFileURI(url)) {
  198. return fetchFile(options, method, url, headers, body)
  199. }
  200. // from remote HTTP(S)
  201. let promise = new Promise((resolve, reject) => {
  202. let nativeMethodName = Array.isArray(body) ? 'fetchBlobForm' : 'fetchBlob'
  203. // on progress event listener
  204. subscription = emitter.addListener('RNFetchBlobProgress', (e) => {
  205. if(e.taskId === taskId && promise.onProgress) {
  206. promise.onProgress(e.written, e.total, e.chunk)
  207. }
  208. })
  209. subscriptionUpload = emitter.addListener('RNFetchBlobProgress-upload', (e) => {
  210. if(e.taskId === taskId && promise.onUploadProgress) {
  211. promise.onUploadProgress(e.written, e.total)
  212. }
  213. })
  214. stateEvent = emitter.addListener('RNFetchBlobState', (e) => {
  215. respInfo = e
  216. if(e.taskId === taskId && promise.onStateChange) {
  217. promise.onStateChange(e)
  218. }
  219. })
  220. subscription = emitter.addListener('RNFetchBlobExpire', (e) => {
  221. console.log(e , 'EXPIRED!!')
  222. if(e.taskId === taskId && promise.onExpire) {
  223. promise.onExpire(e)
  224. }
  225. })
  226. partEvent = emitter.addListener('RNFetchBlobServerPush', (e) => {
  227. if(e.taskId === taskId && promise.onPartData) {
  228. promise.onPartData(e.chunk)
  229. }
  230. })
  231. // When the request body comes from Blob polyfill, we should use special its ref
  232. // as the request body
  233. if( body instanceof Blob && body.isRNFetchBlobPolyfill) {
  234. body = body.getRNFetchBlobRef()
  235. }
  236. let req = RNFetchBlob[nativeMethodName]
  237. /**
  238. * Send request via native module, the response callback accepts three arguments
  239. * @callback
  240. * @param err {any} Error message or object, when the request success, this
  241. * parameter should be `null`.
  242. * @param rawType { 'utf8' | 'base64' | 'path'} RNFB request will be stored
  243. * as UTF8 string, BASE64 string, or a file path reference
  244. * in JS context, and this parameter indicates which one
  245. * dose the response data presents.
  246. * @param data {string} Response data or its reference.
  247. */
  248. req(options, taskId, method, url, headers || {}, body, (err, rawType, data) => {
  249. // task done, remove event listeners
  250. subscription.remove()
  251. subscriptionUpload.remove()
  252. stateEvent.remove()
  253. partEvent.remove()
  254. delete promise['progress']
  255. delete promise['uploadProgress']
  256. delete promise['stateChange']
  257. delete promise['part']
  258. delete promise['cancel']
  259. // delete promise['expire']
  260. promise.cancel = () => {}
  261. if(err)
  262. reject(new Error(err, respInfo))
  263. else {
  264. // response data is saved to storage, create a session for it
  265. if(options.path || options.fileCache || options.addAndroidDownloads
  266. || options.key || options.auto && respInfo.respType === 'blob') {
  267. if(options.session)
  268. session(options.session).add(data)
  269. }
  270. respInfo.rnfbEncode = rawType
  271. resolve(new FetchBlobResponse(taskId, respInfo, data))
  272. }
  273. })
  274. })
  275. // extend Promise object, add `progress`, `uploadProgress`, and `cancel`
  276. // method for register progress event handler and cancel request.
  277. // Add second parameter for performance purpose #140
  278. // When there's only one argument pass to this method, use default `interval`
  279. // and `count`, otherwise use the given on.
  280. // TODO : code refactor, move `uploadProgress` and `progress` to StatefulPromise
  281. promise.progress = (...args) => {
  282. let interval = 250
  283. let count = -1
  284. let fn = () => {}
  285. if(args.length === 2) {
  286. interval = args[0].interval || interval
  287. count = args[0].count || count
  288. fn = args[1]
  289. }
  290. else {
  291. fn = args[0]
  292. }
  293. promise.onProgress = fn
  294. RNFetchBlob.enableProgressReport(taskId, interval, count)
  295. return promise
  296. }
  297. promise.uploadProgress = (...args) => {
  298. let interval = 250
  299. let count = -1
  300. let fn = () => {}
  301. if(args.length === 2) {
  302. interval = args[0].interval || interval
  303. count = args[0].count || count
  304. fn = args[1]
  305. }
  306. else {
  307. fn = args[0]
  308. }
  309. promise.onUploadProgress = fn
  310. RNFetchBlob.enableUploadProgressReport(taskId, interval, count)
  311. return promise
  312. }
  313. promise.part = (fn) => {
  314. promise.onPartData = fn
  315. return promise
  316. }
  317. promise.stateChange = (fn) => {
  318. promise.onStateChange = fn
  319. return promise
  320. }
  321. promise.expire = (fn) => {
  322. promise.onExpire = fn
  323. return promise
  324. }
  325. promise.cancel = (fn) => {
  326. fn = fn || function(){}
  327. subscription.remove()
  328. subscriptionUpload.remove()
  329. stateEvent.remove()
  330. RNFetchBlob.cancelRequest(taskId, fn)
  331. }
  332. promise.taskId = taskId
  333. return promise
  334. }
  335. /**
  336. * RNFetchBlob response object class.
  337. */
  338. class FetchBlobResponse {
  339. taskId : string;
  340. path : () => string | null;
  341. type : 'base64' | 'path' | 'utf8';
  342. data : any;
  343. blob : (contentType:string, sliceSize:number) => Promise<Blob>;
  344. text : () => string | Promise<any>;
  345. json : () => any;
  346. base64 : () => any;
  347. flush : () => void;
  348. respInfo : RNFetchBlobResponseInfo;
  349. session : (name:string) => RNFetchBlobSession | null;
  350. readFile : (encode: 'base64' | 'utf8' | 'ascii') => ?Promise<any>;
  351. readStream : (
  352. encode: 'utf8' | 'ascii' | 'base64',
  353. ) => RNFetchBlobStream | null;
  354. constructor(taskId:string, info:RNFetchBlobResponseInfo, data:any) {
  355. this.data = data
  356. this.taskId = taskId
  357. this.type = info.rnfbEncode
  358. this.respInfo = info
  359. this.info = ():RNFetchBlobResponseInfo => {
  360. return this.respInfo
  361. }
  362. this.array = ():Promise<Array> => {
  363. let cType = info.headers['Content-Type'] || info.headers['content-type']
  364. return new Promise((resolve, reject) => {
  365. switch(this.type) {
  366. case 'base64':
  367. // TODO : base64 to array buffer
  368. break
  369. case 'path':
  370. fs.readFile(this.data, 'ascii').then(resolve)
  371. break
  372. default:
  373. // TODO : text to array buffer
  374. break
  375. }
  376. })
  377. }
  378. /**
  379. * Convert result to javascript RNFetchBlob object.
  380. * @return {Promise<Blob>} Return a promise resolves Blob object.
  381. */
  382. this.blob = ():Promise<Blob> => {
  383. let Blob = polyfill.Blob
  384. let cType = info.headers['Content-Type'] || info.headers['content-type']
  385. return new Promise((resolve, reject) => {
  386. switch(this.type) {
  387. case 'base64':
  388. Blob.build(this.data, { type : cType + ';BASE64' }).then(resolve)
  389. break
  390. case 'path':
  391. polyfill.Blob.build(wrap(this.data), { type : cType }).then(resolve)
  392. break
  393. default:
  394. polyfill.Blob.build(this.data, { type : 'text/plain' }).then(resolve)
  395. break
  396. }
  397. })
  398. }
  399. /**
  400. * Convert result to text.
  401. * @return {string} Decoded base64 string.
  402. */
  403. this.text = ():string | Promise<any> => {
  404. let res = this.data
  405. switch(this.type) {
  406. case 'base64':
  407. return base64.decode(this.data)
  408. case 'path':
  409. return fs.readFile(this.data, 'base64').then((b64) => Promise.resolve(base64.decode(b64)))
  410. default:
  411. return this.data
  412. }
  413. }
  414. /**
  415. * Convert result to JSON object.
  416. * @return {object} Parsed javascript object.
  417. */
  418. this.json = ():any => {
  419. switch(this.type) {
  420. case 'base64':
  421. return JSON.parse(base64.decode(this.data))
  422. case 'path':
  423. return fs.readFile(this.data, 'utf8')
  424. .then((text) => Promise.resolve(JSON.parse(text)))
  425. default:
  426. return JSON.parse(this.data)
  427. }
  428. }
  429. /**
  430. * Return BASE64 string directly.
  431. * @return {string} BASE64 string of response body.
  432. */
  433. this.base64 = ():string | Promise<any> => {
  434. switch(this.type) {
  435. case 'base64':
  436. return this.data
  437. case 'path':
  438. return fs.readFile(this.data, 'base64')
  439. default:
  440. return base64.encode(this.data)
  441. }
  442. }
  443. /**
  444. * Remove cahced file
  445. * @return {Promise}
  446. */
  447. this.flush = () => {
  448. let path = this.path()
  449. if(!path || this.type !== 'path')
  450. return
  451. return unlink(path)
  452. }
  453. /**
  454. * get path of response temp file
  455. * @return {string} File path of temp file.
  456. */
  457. this.path = () => {
  458. if(this.type === 'path')
  459. return this.data
  460. return null
  461. }
  462. this.session = (name:string):RNFetchBlobSession | null => {
  463. if(this.type === 'path')
  464. return session(name).add(this.data)
  465. else {
  466. console.warn('only file paths can be add into session.')
  467. return null
  468. }
  469. }
  470. /**
  471. * Start read stream from cached file
  472. * @param {String} encoding Encode type, should be one of `base64`, `ascrii`, `utf8`.
  473. * @param {Function} fn On data event handler
  474. * @return {void}
  475. */
  476. this.readStream = (encode: 'base64' | 'utf8' | 'ascii'):RNFetchBlobStream | null => {
  477. if(this.type === 'path') {
  478. return readStream(this.data, encode)
  479. }
  480. else {
  481. console.warn('RNFetchblob', 'this response data does not contains any available stream')
  482. return null
  483. }
  484. }
  485. /**
  486. * Read file content with given encoding, if the response does not contains
  487. * a file path, show warning message
  488. * @param {String} encoding Encode type, should be one of `base64`, `ascrii`, `utf8`.
  489. * @return {String}
  490. */
  491. this.readFile = (encode: 'base64' | 'utf8' | 'ascii') => {
  492. if(this.type === 'path') {
  493. encode = encode || 'utf8'
  494. return readFile(this.data, encode)
  495. }
  496. else {
  497. console.warn('RNFetchblob', 'this response does not contains a readable file')
  498. return null
  499. }
  500. }
  501. }
  502. }
  503. export default {
  504. fetch,
  505. base64,
  506. android,
  507. ios,
  508. config,
  509. session,
  510. fs,
  511. wrap,
  512. net,
  513. polyfill,
  514. JSONStream
  515. }