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