UNPKG

react-native-blob-util

Version:

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

467 lines (401 loc) 14.2 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 XMLHttpRequestEventTarget from './XMLHttpRequestEventTarget.js'; import Log from '../utils/log.js'; import Blob from './Blob.js'; import ProgressEvent from './ProgressEvent.js'; import URIUtil from "../utils/uri"; import {config} from "../fetch"; const log = new Log('XMLHttpRequest'); log.disable(); // log.level(3) const UNSENT = 0; const OPENED = 1; const HEADERS_RECEIVED = 2; const LOADING = 3; const DONE = 4; export default class XMLHttpRequest extends XMLHttpRequestEventTarget { _onreadystatechange: () => void; upload: XMLHttpRequestEventTarget = new XMLHttpRequestEventTarget(); static binaryContentTypes: Array<string> = [ 'image/', 'video/', 'audio/' ]; // readonly _readyState: number = UNSENT; _uriType: 'net' | 'file' = 'net'; _response: any = ''; _responseText: any = ''; _responseHeaders: any = {}; _responseType: '' | 'arraybuffer' | 'blob' | 'json' | 'text' = ''; // TODO : not suppoted ATM _responseURL: null = ''; _responseXML: null = ''; _status: number = 0; _statusText: string = ''; _timeout: number = 60000; _sendFlag: boolean = false; _uploadStarted: boolean = false; _increment: boolean = false; // ReactNativeBlobUtil compatible data structure _config: ReactNativeBlobUtilConfig = {}; _url: any; _method: string; _headers: any = { 'Content-Type': 'text/plain' }; _cleanUp: () => void = null; _body: any; // ReactNativeBlobUtil promise object, which has `progress`, `uploadProgress`, and // `cancel` methods. _task: any; // constants get UNSENT() { return UNSENT; } get OPENED() { return OPENED; } get HEADERS_RECEIVED() { return HEADERS_RECEIVED; } get LOADING() { return LOADING; } get DONE() { return DONE; } static get UNSENT() { return UNSENT; } static get OPENED() { return OPENED; } static get HEADERS_RECEIVED() { return HEADERS_RECEIVED; } static get LOADING() { return LOADING; } static get DONE() { return DONE; } static setLog(level: number) { if (level === -1) log.disable(); else log.level(level); } static addBinaryContentType(substr: string) { for (let i in XMLHttpRequest.binaryContentTypes) { if (new RegExp(substr, 'i').test(XMLHttpRequest.binaryContentTypes[i])) { return; } } XMLHttpRequest.binaryContentTypes.push(substr); } static removeBinaryContentType(val) { for (let i in XMLHttpRequest.binaryContentTypes) { if (new RegExp(substr, 'i').test(XMLHttpRequest.binaryContentTypes[i])) { XMLHttpRequest.binaryContentTypes.splice(i, 1); return; } } } constructor() { log.verbose('XMLHttpRequest constructor called'); super(); } /** * XMLHttpRequest.open, always async, user and password not supported. When * this method invoked, headers should becomes empty again. * @param {string} method Request method * @param {string} url Request URL * @param {true} async Always async * @param {any} user NOT SUPPORTED * @param {any} password NOT SUPPORTED */ open(method: string, url: string, async: true, user: any, password: any) { log.verbose('XMLHttpRequest open ', method, url, async, user, password); this._method = method; this._url = url; this._headers = {}; this._increment = URIUtil.isJSONStreamURI(this._url); this._url = this._url.replace(/^JSONStream\:\/\//, ''); this._dispatchReadStateChange(XMLHttpRequest.OPENED); } /** * Invoke this function to send HTTP request, and set body. * @param {any} body Body in ReactNativeBlobUtil flavor */ send(body) { this._body = body; if (this._readyState !== XMLHttpRequest.OPENED) throw 'InvalidStateError : XMLHttpRequest is not opened yet.'; let promise = Promise.resolve(); this._sendFlag = true; log.verbose('XMLHttpRequest send ', body); let {_method, _url, _headers} = this; log.verbose('sending request with args', _method, _url, _headers, body); log.verbose(typeof body, body instanceof FormData); if (body instanceof FormData) { log.debug('creating blob and setting header from FormData instance'); body = new Blob(body); this._headers['Content-Type'] = `multipart/form-data; boundary=${body.multipartBoundary}`; } if (body instanceof Blob) { log.debug('sending blob body', body._blobCreated); promise = new Promise((resolve, reject) => { body.onCreated((blob) => { // when the blob is derived (not created by RN developer), the blob // will be released after XMLHttpRequest sent if (blob.isDerived) { this._cleanUp = () => { blob.close(); }; } log.debug('body created send request'); body = URIUtil.wrap(blob.getReactNativeBlobUtilRef()); resolve(); }); }); } else if (typeof body === 'object') { body = JSON.stringify(body); promise = Promise.resolve(); } else { body = body ? body.toString() : body; promise = Promise.resolve(); } promise.then(() => { log.debug('send request invoke', body); for (let h in _headers) { _headers[h] = _headers[h].toString(); } this._task = config({ auto: true, timeout: this._timeout, increment: this._increment, binaryContentTypes: XMLHttpRequest.binaryContentTypes }) .fetch(_method, _url, _headers, body); this._task .stateChange(this._headerReceived) .uploadProgress(this._uploadProgressEvent) .progress(this._progressEvent) .catch(this._onError) .then(this._onDone); }); } overrideMimeType(mime: string) { log.verbose('XMLHttpRequest overrideMimeType', mime); this._headers['Content-Type'] = mime; } setRequestHeader(name, value) { log.verbose('XMLHttpRequest set header', name, value); if (this._readyState !== OPENED || this._sendFlag) { throw `InvalidStateError : Calling setRequestHeader in wrong state ${this._readyState}`; } // UNICODE SHOULD NOT PASS if (typeof name !== 'string' || /[^\u0000-\u00ff]/.test(name)) { throw 'TypeError : header field name should be a string'; } // let invalidPatterns = [ /[\(\)\>\<\@\,\:\\\/\[\]\?\=\}\{\s\ \u007f\;\t\0\v\r]/, /tt/ ]; for (let pattern of invalidPatterns) { if (pattern.test(name) || typeof name !== 'string') { throw `SyntaxError : Invalid header field name ${name}`; } } this._headers[name] = value; } abort() { log.verbose('XMLHttpRequest abort '); if (!this._task) return; this._task.cancel((err) => { let e = { timeStamp: Date.now(), }; if (this.onabort) this.onabort(); if (err) { e.detail = err; e.type = 'error'; this.dispatchEvent('error', e); } else { e.type = 'abort'; this.dispatchEvent('abort', e); } }); } getResponseHeader(field: string): string | null { log.verbose('XMLHttpRequest get header', field, this._responseHeaders); if (!this._responseHeaders) return null; return this._responseHeaders[field] || this._responseHeaders[field.toLowerCase()] || null; } getAllResponseHeaders(): string | null { log.verbose('XMLHttpRequest get all headers', this._responseHeaders); if (!this._responseHeaders) return ''; let result = ''; let respHeaders = this.responseHeaders; for (let i in respHeaders) { result += `${i}: ${respHeaders[i]}${String.fromCharCode(0x0D, 0x0A)}`; } return result.substr(0, result.length - 2); } _headerReceived = (e) => { log.debug('header received ', this._task.taskId, e); this.responseURL = this._url; if (e.state === "2" && e.taskId === this._task.taskId) { this._responseHeaders = e.headers; this._statusText = e.status; this._status = Math.floor(e.status); this._dispatchReadStateChange(XMLHttpRequest.HEADERS_RECEIVED); } } _uploadProgressEvent = (send: number, total: number) => { if (!this._uploadStarted) { this.upload.dispatchEvent('loadstart'); this._uploadStarted = true; } if (send >= total) this.upload.dispatchEvent('load'); this.upload.dispatchEvent('progress', new ProgressEvent(true, send, total)); } _progressEvent = (send: number, total: number, chunk: string) => { log.verbose(this.readyState); if (this._readyState === XMLHttpRequest.HEADERS_RECEIVED) this._dispatchReadStateChange(XMLHttpRequest.LOADING); let lengthComputable = false; if (total && total >= 0) lengthComputable = true; let e = new ProgressEvent(lengthComputable, send, total); if (this._increment) { this._responseText += chunk; } this.dispatchEvent('progress', e); } _onError = (err) => { let statusCode = Math.floor(this.status); if (statusCode >= 100 && statusCode !== 408) { return; } log.debug('XMLHttpRequest error', err); this._statusText = err; this._status = String(err).match(/\d+/); this._status = this._status ? Math.floor(this.status) : 404; this._dispatchReadStateChange(XMLHttpRequest.DONE); if (err && String(err.message).match(/(timed\sout|timedout)/) || this._status == 408) { this.dispatchEvent('timeout'); } this.dispatchEvent('loadend'); this.dispatchEvent('error', { type: 'error', detail: err }); this.clearEventListeners(); } _onDone = (resp) => { log.debug('XMLHttpRequest done', this._url, resp, this); this._statusText = this._status; let responseDataReady = () => { log.debug('request done state = 4'); this.dispatchEvent('load'); this.dispatchEvent('loadend'); this._dispatchReadStateChange(XMLHttpRequest.DONE); this.clearEventListeners(); }; if (resp) { let info = resp.respInfo || {}; log.debug(this._url, info, info.respType); switch (this._responseType) { case 'blob' : resp.blob().then((b) => { this._responseText = resp.text(); this._response = b; responseDataReady(); }); break; case 'arraybuffer': // TODO : to array buffer break; case 'json': this._response = resp.json(); this._responseText = resp.text(); break; default : this._responseText = resp.text(); this._response = this.responseText; responseDataReady(); break; } } } _dispatchReadStateChange(state) { this._readyState = state; if (typeof this._onreadystatechange === 'function') this._onreadystatechange(); } set onreadystatechange(fn: () => void) { log.verbose('XMLHttpRequest set onreadystatechange', fn); this._onreadystatechange = fn; } get onreadystatechange() { return this._onreadystatechange; } get readyState() { log.verbose('get readyState', this._readyState); return this._readyState; } get status() { log.verbose('get status', this._status); return this._status; } get statusText() { log.verbose('get statusText', this._statusText); return this._statusText; } get response() { log.verbose('get response', this._response); return this._response; } get responseText() { log.verbose('get responseText', this._responseText); return this._responseText; } get responseURL() { log.verbose('get responseURL', this._responseURL); return this._responseURL; } get responseHeaders() { log.verbose('get responseHeaders', this._responseHeaders); return this._responseHeaders; } set timeout(val) { this._timeout = val * 1000; log.verbose('set timeout', this._timeout); } get timeout() { log.verbose('get timeout', this._timeout); return this._timeout; } set responseType(val) { log.verbose('set response type', this._responseType); this._responseType = val; } get responseType() { log.verbose('get response type', this._responseType); return this._responseType; } static get isRNFBPolyfill() { return true; } }