UNPKG

react-native-blob-util

Version:

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

363 lines (333 loc) 12.3 kB
// 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 fs from '../fs.js'; import getUUID from '../utils/uuid'; import Log from '../utils/log.js'; import URIUtil from "../utils/uri"; import EventTarget from './EventTarget'; const log = new Log('Blob'); const blobCacheDir = fs.dirs.DocumentDir + '/ReactNativeBlobUtil-blobs/'; log.disable(); // log.level(3) /** * A ReactNativeBlobUtil style Blob polyfill class, this is a Blob which compatible to * Response object attain fron ReactNativeBlobUtil.fetch. */ export default class Blob extends EventTarget { cacheName: string; type: string; size: number; isReactNativeBlobUtilPolyfill: boolean = true; multipartBoundary: string = null; _ref: string = null; _blobCreated: boolean = false; _onCreated: Array<any> = []; _closed: boolean = false; /** * Static method that remove all files in Blob cache folder. * @nonstandard * @return {Promise} */ static clearCache() { return fs.unlink(blobCacheDir).then(() => fs.mkdir(blobCacheDir)); } static build(data: any, cType: any): Promise<Blob> { return new Promise((resolve, reject) => { new Blob(data, cType).onCreated(resolve); }); } get blobPath() { return this._ref; } static setLog(level: number) { if (level === -1) log.disable(); else log.level(level); } /** * ReactNativeBlobUtil Blob polyfill, create a Blob directly from file path, BASE64 * encoded data, and string. The conversion is done implicitly according to * given `mime`. However, the blob creation is asynchronously, to register * event `onCreated` is need to ensure the Blob is creadted. * @param {any} data Content of Blob object * @param {any} mime Content type settings of Blob object, `text/plain` * by default * @param {boolean} defer When this argument set to `true`, blob constructor * will not invoke blob created event automatically. */ constructor(data: any, cType: any, defer: boolean) { super(); cType = cType || {}; this.cacheName = getBlobName(); this.isReactNativeBlobUtilPolyfill = true; this.isDerived = defer; this.type = cType.type || 'text/plain'; log.verbose('Blob constructor called', 'mime', this.type, 'type', typeof data, 'length', data ? data.length : 0); this._ref = blobCacheDir + this.cacheName; let p = null; if (!data) data = ''; if (data.isReactNativeBlobUtilPolyfill) { log.verbose('create Blob cache file from Blob object'); let size = 0; this._ref = String(data.getReactNativeBlobUtilRef()); let orgPath = this._ref; p = fs.exists(orgPath) .then((exist) => { if (exist) return fs.writeFile(orgPath, data, 'uri') .then((size) => Promise.resolve(size)) .catch((err) => { throw `ReactNativeBlobUtil Blob file creation error, ${err}`; }); else throw `could not create Blob from path ${orgPath}, file not exists`; }); } // process FormData else if (data instanceof FormData) { log.verbose('create Blob cache file from FormData', data); let boundary = `ReactNativeBlobUtil-${this.cacheName}-${Date.now()}`; this.multipartBoundary = boundary; let parts = data.getParts(); let formArray = []; if (!parts) { p = fs.writeFile(this._ref, '', 'utf8'); } else { for (let i in parts) { formArray.push('\r\n--' + boundary + '\r\n'); let part = parts[i]; for (let j in part.headers) { formArray.push(j + ': ' + part.headers[j] + '\r\n'); } formArray.push('\r\n'); if (part.isReactNativeBlobUtilPolyfill) formArray.push(part); else formArray.push(part.string); } log.verbose('FormData array', formArray); formArray.push('\r\n--' + boundary + '--\r\n'); p = createMixedBlobData(this._ref, formArray); } } // if the data is a string starts with `ReactNativeBlobUtil-file://`, append the // Blob data from file path else if (typeof data === 'string' && data.startsWith('ReactNativeBlobUtil-file://')) { log.verbose('create Blob cache file from file path', data); // set this flag so that we know this blob is a wrapper of an existing file this._isReference = true; this._ref = String(data).replace('ReactNativeBlobUtil-file://', ''); let orgPath = this._ref; if (defer) return; else { p = fs.stat(orgPath) .then((stat) => { return Promise.resolve(stat.size); }); } } // content from variable need create file else if (typeof data === 'string') { let encoding = 'utf8'; let mime = String(this.type); // when content type contains application/octet* or *;base64, ReactNativeBlobUtil // fs will treat it as BASE64 encoded string binary data if (/(application\/octet|\;base64)/i.test(mime)) encoding = 'base64'; else data = data.toString(); // create cache file this.type = String(this.type).replace(/;base64/ig, ''); log.verbose('create Blob cache file from string', 'encode', encoding); p = fs.writeFile(this._ref, data, encoding) .then((size) => { return Promise.resolve(size); }); } // TODO : ArrayBuffer support // else if (data instanceof ArrayBuffer ) { // // } // when input is an array of mixed data types, create a file cache else if (Array.isArray(data)) { log.verbose('create Blob cache file from mixed array', data); p = createMixedBlobData(this._ref, data); } else { data = data.toString(); p = fs.writeFile(this._ref, data, 'utf8') .then((size) => Promise.resolve(size)); } p && p.then((size) => { this.size = size; this._invokeOnCreateEvent(); }) .catch((err) => { log.error('ReactNativeBlobUtil could not create Blob : ' + this._ref, err); }); } /** * Since Blob content will asynchronously write to a file during creation, * use this method to register an event handler for Blob initialized event. * @nonstandard * @param {(b:Blob) => void} An event handler invoked when Blob created * @return {Blob} The Blob object instance itself */ onCreated(fn: () => void): Blob { log.verbose('#register blob onCreated', this._blobCreated); if (!this._blobCreated) this._onCreated.push(fn); else { fn(this); } return this; } markAsDerived() { this._isDerived = true; } get isDerived() { return this._isDerived || false; } /** * Get file reference of the Blob object. * @nonstandard * @return {string} Blob file reference which can be consumed by ReactNativeBlobUtil fs */ getReactNativeBlobUtilRef() { return this._ref; } /** * Create a Blob object which is sliced from current object * @param {number} start Start byte number * @param {number} end End byte number * @param {string} contentType Optional, content type of new Blob object * @return {Blob} */ slice(start: ?number, end: ?number, contentType: ?string = ''): Blob { if (this._closed) throw 'Blob has been released.'; log.verbose('slice called', start, end, contentType); let resPath = blobCacheDir + getBlobName(); let pass = false; log.debug('fs.slice new blob will at', resPath); let result = new Blob(URIUtil.wrap(resPath), {type: contentType}, true); fs.exists(blobCacheDir) .then((exist) => { if (exist) return Promise.resolve(); return fs.mkdir(blobCacheDir); }) .then(() => fs.slice(this._ref, resPath, start, end)) .then((dest) => { log.debug('fs.slice done', dest); result._invokeOnCreateEvent(); pass = true; }) .catch((err) => { console.warn('Blob.slice failed:', err); pass = true; }); log.debug('slice returning new Blob'); return result; } /** * Read data of the Blob object, this is not standard method. * @nonstandard * @param {string} encoding Read data with encoding * @return {Promise} */ readBlob(encoding: string): Promise<any> { if (this._closed) throw 'Blob has been released.'; return fs.readFile(this._ref, encoding || 'utf8'); } /** * Release the resource of the Blob object. * @nonstandard * @return {Promise} */ close() { if (this._closed) return Promise.reject('Blob has been released.'); this._closed = true; return fs.unlink(this._ref).catch((err) => { console.warn(err); }); } safeClose() { if (this._closed) return Promise.reject('Blob has been released.'); this._closed = true; if (!this._isReference) { return fs.unlink(this._ref).catch((err) => { console.warn(err); }); } else { return Promise.resolve(); } } _invokeOnCreateEvent() { log.verbose('invoke create event', this._onCreated); this._blobCreated = true; let fns = this._onCreated; for (let i in fns) { if (typeof fns[i] === 'function') { fns[i](this); } } delete this._onCreated; } } /** * Get a temp filename for Blob object * @return {string} Temporary filename */ function getBlobName() { return 'blob-' + getUUID(); } /** * Create a file according to given array. The element in array can be a number, * Blob, String, Array. * @param {string} ref File path reference * @param {Array} dataArray An array contains different types of data. * @return {Promise} */ function createMixedBlobData(ref, dataArray) { // create an empty file for store blob data let p = fs.writeFile(ref, ''); let args = []; let size = 0; for (let i in dataArray) { let part = dataArray[i]; if (!part) continue; if (part.isReactNativeBlobUtilPolyfill) { args.push([ref, part._ref, 'uri']); } else if (typeof part === 'string') args.push([ref, part, 'utf8']); // TODO : ArrayBuffer // else if (part instanceof ArrayBuffer) { // // } else if (Array.isArray(part)) args.push([ref, part, 'ascii']); } // start write blob data for (let i in args) { p = p.then(function (written) { let arg = this; if (written) size += written; log.verbose('mixed blob write', args[i], written); return fs.appendFile(...arg); }.bind(args[i])); } return p.then(() => Promise.resolve(size)); }