No Description

Blob.js 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  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. import RNFetchBlob from '../index.js'
  5. import fs from '../fs.js'
  6. import getUUID from '../utils/uuid'
  7. import Log from '../utils/log.js'
  8. import EventTarget from './EventTarget'
  9. const log = new Log('Blob')
  10. const blobCacheDir = fs.dirs.DocumentDir + '/RNFetchBlob-blobs/'
  11. log.disable()
  12. // log.level(3)
  13. /**
  14. * A RNFetchBlob style Blob polyfill class, this is a Blob which compatible to
  15. * Response object attain fron RNFetchBlob.fetch.
  16. */
  17. export default class Blob extends EventTarget {
  18. cacheName:string;
  19. type:string;
  20. size:number;
  21. isRNFetchBlobPolyfill:boolean = true;
  22. multipartBoundary:string = null;
  23. _ref:string = null;
  24. _blobCreated:boolean = false;
  25. _onCreated:Array<any> = [];
  26. _closed:boolean = false;
  27. /**
  28. * Static method that remove all files in Blob cache folder.
  29. * @nonstandard
  30. * @return {Promise}
  31. */
  32. static clearCache() {
  33. return fs.unlink(blobCacheDir).then(() => fs.mkdir(blobCacheDir))
  34. }
  35. static build(data:any, cType:any):Promise<Blob> {
  36. return new Promise((resolve, reject) => {
  37. new Blob(data, cType).onCreated(resolve)
  38. })
  39. }
  40. get blobPath() {
  41. return this._ref
  42. }
  43. static setLog(level:number) {
  44. if(level === -1)
  45. log.disable()
  46. else
  47. log.level(level)
  48. }
  49. /**
  50. * RNFetchBlob Blob polyfill, create a Blob directly from file path, BASE64
  51. * encoded data, and string. The conversion is done implicitly according to
  52. * given `mime`. However, the blob creation is asynchronously, to register
  53. * event `onCreated` is need to ensure the Blob is creadted.
  54. * @param {any} data Content of Blob object
  55. * @param {any} mime Content type settings of Blob object, `text/plain`
  56. * by default
  57. * @param {boolean} defer When this argument set to `true`, blob constructor
  58. * will not invoke blob created event automatically.
  59. */
  60. constructor(data:any, cType:any, defer:boolean) {
  61. super()
  62. cType = cType || {}
  63. this.cacheName = getBlobName()
  64. this.isRNFetchBlobPolyfill = true
  65. this.isDerived = defer
  66. this.type = cType.type || 'text/plain'
  67. log.verbose('Blob constructor called', 'mime', this.type, 'type', typeof data, 'length', data? data.length:0)
  68. this._ref = blobCacheDir + this.cacheName
  69. let p = null
  70. if(!data)
  71. data = ''
  72. if(data.isRNFetchBlobPolyfill) {
  73. log.verbose('create Blob cache file from Blob object')
  74. let size = 0
  75. this._ref = String(data.getRNFetchBlobRef())
  76. let orgPath = this._ref
  77. p = fs.exists(orgPath)
  78. .then((exist) => {
  79. if(exist)
  80. return fs.writeFile(orgPath, data, 'uri')
  81. .then((size) => Promise.resolve(size))
  82. .catch((err) => {
  83. throw `RNFetchBlob Blob file creation error, ${err}`
  84. })
  85. else
  86. throw `could not create Blob from path ${orgPath}, file not exists`
  87. })
  88. }
  89. // process FormData
  90. else if(data instanceof FormData) {
  91. log.verbose('create Blob cache file from FormData', data)
  92. let boundary = `RNFetchBlob-${this.cacheName}-${Date.now()}`
  93. this.multipartBoundary = boundary
  94. let parts = data.getParts()
  95. let formArray = []
  96. if(!parts) {
  97. p = fs.writeFile(this._ref, '', 'utf8')
  98. }
  99. else {
  100. for(let i in parts) {
  101. formArray.push('\r\n--'+boundary+'\r\n')
  102. let part = parts[i]
  103. for(let j in part.headers) {
  104. formArray.push(j + ': ' +part.headers[j] + '\r\n')
  105. }
  106. formArray.push('\r\n')
  107. if(part.isRNFetchBlobPolyfill)
  108. formArray.push(part)
  109. else
  110. formArray.push(part.string)
  111. }
  112. log.verbose('FormData array', formArray)
  113. formArray.push('\r\n--'+boundary+'--\r\n')
  114. p = createMixedBlobData(this._ref, formArray)
  115. }
  116. }
  117. // if the data is a string starts with `RNFetchBlob-file://`, append the
  118. // Blob data from file path
  119. else if(typeof data === 'string' && data.startsWith('RNFetchBlob-file://')) {
  120. log.verbose('create Blob cache file from file path', data)
  121. // set this flag so that we know this blob is a wrapper of an existing file
  122. this._isReference = true
  123. this._ref = String(data).replace('RNFetchBlob-file://', '')
  124. let orgPath = this._ref
  125. if(defer)
  126. return
  127. else {
  128. p = fs.stat(orgPath)
  129. .then((stat) => {
  130. return Promise.resolve(stat.size)
  131. })
  132. }
  133. }
  134. // content from variable need create file
  135. else if(typeof data === 'string') {
  136. let encoding = 'utf8'
  137. let mime = String(this.type)
  138. // when content type contains application/octet* or *;base64, RNFetchBlob
  139. // fs will treat it as BASE64 encoded string binary data
  140. if(/(application\/octet|\;base64)/i.test(mime))
  141. encoding = 'base64'
  142. else
  143. data = data.toString()
  144. // create cache file
  145. this.type = String(this.type).replace(/;base64/ig, '')
  146. log.verbose('create Blob cache file from string', 'encode', encoding)
  147. p = fs.writeFile(this._ref, data, encoding)
  148. .then((size) => {
  149. return Promise.resolve(size)
  150. })
  151. }
  152. // TODO : ArrayBuffer support
  153. // else if (data instanceof ArrayBuffer ) {
  154. //
  155. // }
  156. // when input is an array of mixed data types, create a file cache
  157. else if(Array.isArray(data)) {
  158. log.verbose('create Blob cache file from mixed array', data)
  159. p = createMixedBlobData(this._ref, data)
  160. }
  161. else {
  162. data = data.toString()
  163. p = fs.writeFile(this._ref, data, 'utf8')
  164. .then((size) => Promise.resolve(size))
  165. }
  166. p && p.then((size) => {
  167. this.size = size
  168. this._invokeOnCreateEvent()
  169. })
  170. .catch((err) => {
  171. log.error('RNFetchBlob could not create Blob : '+ this._ref, err)
  172. })
  173. }
  174. /**
  175. * Since Blob content will asynchronously write to a file during creation,
  176. * use this method to register an event handler for Blob initialized event.
  177. * @nonstandard
  178. * @param {(b:Blob) => void} An event handler invoked when Blob created
  179. * @return {Blob} The Blob object instance itself
  180. */
  181. onCreated(fn:() => void):Blob {
  182. log.verbose('#register blob onCreated', this._blobCreated)
  183. if(!this._blobCreated)
  184. this._onCreated.push(fn)
  185. else {
  186. fn(this)
  187. }
  188. return this
  189. }
  190. markAsDerived() {
  191. this._isDerived = true
  192. }
  193. get isDerived() {
  194. return this._isDerived || false
  195. }
  196. /**
  197. * Get file reference of the Blob object.
  198. * @nonstandard
  199. * @return {string} Blob file reference which can be consumed by RNFetchBlob fs
  200. */
  201. getRNFetchBlobRef() {
  202. return this._ref
  203. }
  204. /**
  205. * Create a Blob object which is sliced from current object
  206. * @param {number} start Start byte number
  207. * @param {number} end End byte number
  208. * @param {string} contentType Optional, content type of new Blob object
  209. * @return {Blob}
  210. */
  211. slice(start:?number, end:?number, contentType:?string=''):Blob {
  212. if(this._closed)
  213. throw 'Blob has been released.'
  214. log.verbose('slice called', start, end, contentType)
  215. let resPath = blobCacheDir + getBlobName()
  216. let pass = false
  217. log.debug('fs.slice new blob will at', resPath)
  218. let result = new Blob(RNFetchBlob.wrap(resPath), { type : contentType }, true)
  219. fs.exists(blobCacheDir)
  220. .then((exist) => {
  221. if(exist)
  222. return Promise.resolve()
  223. return fs.mkdir(blobCacheDir)
  224. })
  225. .then(() => fs.slice(this._ref, resPath, start, end))
  226. .then((dest) => {
  227. log.debug('fs.slice done', dest)
  228. result._invokeOnCreateEvent()
  229. pass = true
  230. })
  231. .catch((err) => {
  232. console.warn('Blob.slice failed:', err)
  233. pass = true
  234. })
  235. log.debug('slice returning new Blob')
  236. return result
  237. }
  238. /**
  239. * Read data of the Blob object, this is not standard method.
  240. * @nonstandard
  241. * @param {string} encoding Read data with encoding
  242. * @return {Promise}
  243. */
  244. readBlob(encoding:string):Promise<any> {
  245. if(this._closed)
  246. throw 'Blob has been released.'
  247. return fs.readFile(this._ref, encoding || 'utf8')
  248. }
  249. /**
  250. * Release the resource of the Blob object.
  251. * @nonstandard
  252. * @return {Promise}
  253. */
  254. close() {
  255. if(this._closed)
  256. return Promise.reject('Blob has been released.')
  257. this._closed = true
  258. return fs.unlink(this._ref).catch((err) => {
  259. console.warn(err)
  260. })
  261. }
  262. safeClose() {
  263. if(this._closed)
  264. return Promise.reject('Blob has been released.')
  265. this._closed = true
  266. if(!this._isReference) {
  267. return fs.unlink(this._ref).catch((err) => {
  268. console.warn(err)
  269. })
  270. }
  271. else {
  272. return Promise.resolve()
  273. }
  274. }
  275. _invokeOnCreateEvent() {
  276. log.verbose('invoke create event', this._onCreated)
  277. this._blobCreated = true
  278. let fns = this._onCreated
  279. for(let i in fns) {
  280. if(typeof fns[i] === 'function') {
  281. fns[i](this)
  282. }
  283. }
  284. delete this._onCreated
  285. }
  286. }
  287. /**
  288. * Get a temp filename for Blob object
  289. * @return {string} Temporary filename
  290. */
  291. function getBlobName() {
  292. return 'blob-' + getUUID()
  293. }
  294. /**
  295. * Create a file according to given array. The element in array can be a number,
  296. * Blob, String, Array.
  297. * @param {string} ref File path reference
  298. * @param {Array} dataArray An array contains different types of data.
  299. * @return {Promise}
  300. */
  301. function createMixedBlobData(ref, dataArray) {
  302. // create an empty file for store blob data
  303. let p = fs.writeFile(ref, '')
  304. let args = []
  305. let size = 0
  306. for(let i in dataArray) {
  307. let part = dataArray[i]
  308. if(!part)
  309. continue
  310. if(part.isRNFetchBlobPolyfill) {
  311. args.push([ref, part._ref, 'uri'])
  312. }
  313. else if(typeof part === 'string')
  314. args.push([ref, part, 'utf8'])
  315. // TODO : ArrayBuffer
  316. // else if (part instanceof ArrayBuffer) {
  317. //
  318. // }
  319. else if (Array.isArray(part))
  320. args.push([ref, part, 'ascii'])
  321. }
  322. // start write blob data
  323. for(let i in args) {
  324. p = p.then(function(written){
  325. let arg = this
  326. if(written)
  327. size += written
  328. log.verbose('mixed blob write', args[i], written)
  329. return fs.appendFile(...arg)
  330. }.bind(args[i]))
  331. }
  332. return p.then(() => Promise.resolve(size))
  333. }