Implement Blob support for XMLHttpRequest
Summary: This PR is a followup to https://github.com/facebook/react-native/pull/11417 and should be merged after that one is merged. 1. Add support for creating blobs from strings, not just other blobs 1. Add the `File` constructor which is a superset of `Blob` 1. Add the `FileReader` API which can be used to read blobs as strings or data url (base64) 1. Add support for uploading and downloading blobs via `XMLHttpRequest` and `fetch` 1. Add ability to download local files on Android so you can do `fetch(uri).then(res => res.blob())` to get a blob for a local file (iOS already supported this) 1. Clone the repo https://github.com/expo/react-native-blob-test 1. Change the `package.json` and update `react-native` dependency to point to this branch, then run `npm install` 1. Run the `server.js` file with `node server.js` 1. Open the `index.common.js` file and replace `localhost` with your computer's IP address 1. Start the packager with `yarn start` and run the app on your device If everything went well, all tests should pass, and you should see a screen like this: ![screen shot 2017-06-08 at 7 53 08 pm](https://user-images.githubusercontent.com/1174278/26936407-435bbce2-4c8c-11e7-9ae3-eb104e46961e.png)! Pull to rerun all tests or tap on specific test to re-run it [GENERAL] [FEATURE] [Blob] - Implement blob support for XMLHttpRequest Closes https://github.com/facebook/react-native/pull/11573 Reviewed By: shergin Differential Revision: D6082054 Pulled By: hramos fbshipit-source-id: cc9c174fdefdfaf6e5d9fd7b300120a01a50e8c1
This commit is contained in:
parent
3fc33bb54f
commit
be56a3efee
|
@ -8,19 +8,12 @@
|
||||||
*
|
*
|
||||||
* @providesModule Blob
|
* @providesModule Blob
|
||||||
* @flow
|
* @flow
|
||||||
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const invariant = require('fbjs/lib/invariant');
|
import type {BlobData, BlobOptions} from 'BlobTypes';
|
||||||
/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error
|
|
||||||
* found when Flow v0.54 was deployed. To see the error delete this comment and
|
|
||||||
* run Flow. */
|
|
||||||
const uuid = require('uuid');
|
|
||||||
|
|
||||||
const { BlobModule } = require('NativeModules');
|
|
||||||
|
|
||||||
import type { BlobProps } from 'BlobTypes';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opaque JS representation of some binary data in native.
|
* Opaque JS representation of some binary data in native.
|
||||||
|
@ -60,51 +53,16 @@ import type { BlobProps } from 'BlobTypes';
|
||||||
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob
|
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob
|
||||||
*/
|
*/
|
||||||
class Blob {
|
class Blob {
|
||||||
/**
|
_data: ?BlobData;
|
||||||
* Size of the data contained in the Blob object, in bytes.
|
|
||||||
*/
|
|
||||||
size: number;
|
|
||||||
/*
|
|
||||||
* String indicating the MIME type of the data contained in the Blob.
|
|
||||||
* If the type is unknown, this string is empty.
|
|
||||||
*/
|
|
||||||
type: string;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Unique id to identify the blob on native side (non-standard)
|
|
||||||
*/
|
|
||||||
blobId: string;
|
|
||||||
/*
|
|
||||||
* Offset to indicate part of blob, used when sliced (non-standard)
|
|
||||||
*/
|
|
||||||
offset: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct blob instance from blob data from native.
|
|
||||||
* Used internally by modules like XHR, WebSocket, etc.
|
|
||||||
*/
|
|
||||||
static create(props: BlobProps): Blob {
|
|
||||||
return Object.assign(Object.create(Blob.prototype), props);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for JS consumers.
|
* Constructor for JS consumers.
|
||||||
* Currently we only support creating Blobs from other Blobs.
|
* Currently we only support creating Blobs from other Blobs.
|
||||||
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob
|
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob
|
||||||
*/
|
*/
|
||||||
constructor(parts: Array<Blob>, options: any) {
|
constructor(parts: Array<Blob | string> = [], options?: BlobOptions) {
|
||||||
const blobId = uuid();
|
const BlobManager = require('BlobManager');
|
||||||
let size = 0;
|
this.data = BlobManager.createFromParts(parts, options).data;
|
||||||
parts.forEach((part) => {
|
|
||||||
invariant(part instanceof Blob, 'Can currently only create a Blob from other Blobs');
|
|
||||||
size += part.size;
|
|
||||||
});
|
|
||||||
BlobModule.createFromParts(parts, blobId);
|
|
||||||
return Blob.create({
|
|
||||||
blobId,
|
|
||||||
offset: 0,
|
|
||||||
size,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -112,9 +70,22 @@ class Blob {
|
||||||
* the data in the specified range of bytes of the source Blob.
|
* the data in the specified range of bytes of the source Blob.
|
||||||
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice
|
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice
|
||||||
*/
|
*/
|
||||||
|
set data(data: ?BlobData) {
|
||||||
|
this._data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
get data(): BlobData {
|
||||||
|
if (!this._data) {
|
||||||
|
throw new Error('Blob has been closed and is no longer available');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._data;
|
||||||
|
}
|
||||||
|
|
||||||
slice(start?: number, end?: number): Blob {
|
slice(start?: number, end?: number): Blob {
|
||||||
let offset = this.offset;
|
const BlobManager = require('BlobManager');
|
||||||
let size = this.size;
|
let {offset, size} = this.data;
|
||||||
|
|
||||||
if (typeof start === 'number') {
|
if (typeof start === 'number') {
|
||||||
if (start > size) {
|
if (start > size) {
|
||||||
start = size;
|
start = size;
|
||||||
|
@ -129,8 +100,8 @@ class Blob {
|
||||||
size = end - start;
|
size = end - start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Blob.create({
|
return BlobManager.createFromOptions({
|
||||||
blobId: this.blobId,
|
blobId: this.data.blobId,
|
||||||
offset,
|
offset,
|
||||||
size,
|
size,
|
||||||
});
|
});
|
||||||
|
@ -149,7 +120,24 @@ class Blob {
|
||||||
* `new Blob([blob, ...])` actually copies the data in memory.
|
* `new Blob([blob, ...])` actually copies the data in memory.
|
||||||
*/
|
*/
|
||||||
close() {
|
close() {
|
||||||
BlobModule.release(this.blobId);
|
const BlobManager = require('BlobManager');
|
||||||
|
BlobManager.release(this.data.blobId);
|
||||||
|
this.data = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size of the data contained in the Blob object, in bytes.
|
||||||
|
*/
|
||||||
|
get size(): number {
|
||||||
|
return this.data.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* String indicating the MIME type of the data contained in the Blob.
|
||||||
|
* If the type is unknown, this string is empty.
|
||||||
|
*/
|
||||||
|
get type(): string {
|
||||||
|
return this.data.type || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2013-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
* @providesModule BlobManager
|
||||||
|
* @flow
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const Blob = require('Blob');
|
||||||
|
const BlobRegistry = require('BlobRegistry');
|
||||||
|
const {BlobModule} = require('NativeModules');
|
||||||
|
|
||||||
|
import type {BlobData, BlobOptions} from 'BlobTypes';
|
||||||
|
|
||||||
|
/*eslint-disable no-bitwise */
|
||||||
|
/*eslint-disable eqeqeq */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Based on the rfc4122-compliant solution posted at
|
||||||
|
* http://stackoverflow.com/questions/105034
|
||||||
|
*/
|
||||||
|
function uuidv4(): string {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
||||||
|
const r = (Math.random() * 16) | 0,
|
||||||
|
v = c == 'x' ? r : (r & 0x3) | 0x8;
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module to manage blobs. Wrapper around the native blob module.
|
||||||
|
*/
|
||||||
|
class BlobManager {
|
||||||
|
/**
|
||||||
|
* If the native blob module is available.
|
||||||
|
*/
|
||||||
|
static isAvailable = !!BlobModule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create blob from existing array of blobs.
|
||||||
|
*/
|
||||||
|
static createFromParts(
|
||||||
|
parts: Array<Blob | string>,
|
||||||
|
options?: BlobOptions,
|
||||||
|
): Blob {
|
||||||
|
const blobId = uuidv4();
|
||||||
|
const items = parts.map(part => {
|
||||||
|
if (
|
||||||
|
part instanceof ArrayBuffer ||
|
||||||
|
(global.ArrayBufferView && part instanceof global.ArrayBufferView)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Creating blobs from 'ArrayBuffer' and 'ArrayBufferView' are not supported",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (part instanceof Blob) {
|
||||||
|
return {
|
||||||
|
data: part.data,
|
||||||
|
type: 'blob',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
data: String(part),
|
||||||
|
type: 'string',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const size = items.reduce((acc, curr) => {
|
||||||
|
if (curr.type === 'string') {
|
||||||
|
return acc + global.unescape(encodeURI(curr.data)).length;
|
||||||
|
} else {
|
||||||
|
return acc + curr.data.size;
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
BlobModule.createFromParts(items, blobId);
|
||||||
|
|
||||||
|
return BlobManager.createFromOptions({
|
||||||
|
blobId,
|
||||||
|
offset: 0,
|
||||||
|
size,
|
||||||
|
type: options ? options.type : '',
|
||||||
|
lastModified: options ? options.lastModified : Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create blob instance from blob data from native.
|
||||||
|
* Used internally by modules like XHR, WebSocket, etc.
|
||||||
|
*/
|
||||||
|
static createFromOptions(options: BlobData): Blob {
|
||||||
|
BlobRegistry.register(options.blobId);
|
||||||
|
return Object.assign(Object.create(Blob.prototype), {data: options});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deallocate resources for a blob.
|
||||||
|
*/
|
||||||
|
static release(blobId: string): void {
|
||||||
|
BlobRegistry.unregister(blobId);
|
||||||
|
if (BlobRegistry.has(blobId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BlobModule.release(blobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject the blob content handler in the networking module to support blob
|
||||||
|
* requests and responses.
|
||||||
|
*/
|
||||||
|
static addNetworkingHandler(): void {
|
||||||
|
BlobModule.addNetworkingHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate the websocket should return a blob for incoming binary
|
||||||
|
* messages.
|
||||||
|
*/
|
||||||
|
static addWebSocketHandler(socketId: number): void {
|
||||||
|
BlobModule.addWebSocketHandler(socketId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate the websocket should no longer return a blob for incoming
|
||||||
|
* binary messages.
|
||||||
|
*/
|
||||||
|
static removeWebSocketHandler(socketId: number): void {
|
||||||
|
BlobModule.removeWebSocketHandler(socketId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a blob message to a websocket.
|
||||||
|
*/
|
||||||
|
static sendOverSocket(blob: Blob, socketId: number): void {
|
||||||
|
BlobModule.sendOverSocket(blob.data, socketId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BlobManager;
|
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2013-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
* @providesModule BlobRegistry
|
||||||
|
* @flow
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
const registry: {[key: string]: number} = {};
|
||||||
|
|
||||||
|
const register = (id: string) => {
|
||||||
|
if (registry[id]) {
|
||||||
|
registry[id]++;
|
||||||
|
} else {
|
||||||
|
registry[id] = 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const unregister = (id: string) => {
|
||||||
|
if (registry[id]) {
|
||||||
|
registry[id]--;
|
||||||
|
if (registry[id] <= 0) {
|
||||||
|
delete registry[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const has = (id: string) => {
|
||||||
|
return registry[id] && registry[id] > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
register,
|
||||||
|
unregister,
|
||||||
|
has,
|
||||||
|
};
|
|
@ -8,18 +8,21 @@
|
||||||
*
|
*
|
||||||
* @providesModule BlobTypes
|
* @providesModule BlobTypes
|
||||||
* @flow
|
* @flow
|
||||||
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
export type BlobProps = {
|
export type BlobData = {
|
||||||
blobId: string,
|
blobId: string,
|
||||||
offset: number,
|
offset: number,
|
||||||
size: number,
|
size: number,
|
||||||
|
name?: string,
|
||||||
type?: string,
|
type?: string,
|
||||||
|
lastModified?: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FileProps = BlobProps & {
|
export type BlobOptions = {
|
||||||
name: string,
|
type: string,
|
||||||
lastModified: number,
|
lastModified: number,
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2013-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
* @providesModule File
|
||||||
|
* @flow
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const Blob = require('Blob');
|
||||||
|
|
||||||
|
const invariant = require('fbjs/lib/invariant');
|
||||||
|
|
||||||
|
import type {BlobOptions} from 'BlobTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The File interface provides information about files.
|
||||||
|
*/
|
||||||
|
class File extends Blob {
|
||||||
|
/**
|
||||||
|
* Constructor for JS consumers.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
parts: Array<Blob | string>,
|
||||||
|
name: string,
|
||||||
|
options?: BlobOptions,
|
||||||
|
) {
|
||||||
|
invariant(
|
||||||
|
parts != null && name != null,
|
||||||
|
'Failed to construct `File`: Must pass both `parts` and `name` arguments.',
|
||||||
|
);
|
||||||
|
|
||||||
|
super(parts, options);
|
||||||
|
this.data.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the file.
|
||||||
|
*/
|
||||||
|
get name(): string {
|
||||||
|
invariant(this.data.name != null, 'Files must have a name set.');
|
||||||
|
return this.data.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Last modified time of the file.
|
||||||
|
*/
|
||||||
|
get lastModified(): number {
|
||||||
|
return this.data.lastModified || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = File;
|
|
@ -0,0 +1,156 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
* @providesModule FileReader
|
||||||
|
* @flow
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const EventTarget = require('event-target-shim');
|
||||||
|
const Blob = require('Blob');
|
||||||
|
const {FileReaderModule} = require('NativeModules');
|
||||||
|
|
||||||
|
type ReadyState =
|
||||||
|
| 0 // EMPTY
|
||||||
|
| 1 // LOADING
|
||||||
|
| 2; // DONE
|
||||||
|
|
||||||
|
type ReaderResult = string | ArrayBuffer;
|
||||||
|
|
||||||
|
const READER_EVENTS = [
|
||||||
|
'abort',
|
||||||
|
'error',
|
||||||
|
'load',
|
||||||
|
'loadstart',
|
||||||
|
'loadend',
|
||||||
|
'progress',
|
||||||
|
];
|
||||||
|
|
||||||
|
const EMPTY = 0;
|
||||||
|
const LOADING = 1;
|
||||||
|
const DONE = 2;
|
||||||
|
|
||||||
|
class FileReader extends EventTarget(...READER_EVENTS) {
|
||||||
|
static EMPTY = EMPTY;
|
||||||
|
static LOADING = LOADING;
|
||||||
|
static DONE = DONE;
|
||||||
|
|
||||||
|
EMPTY = EMPTY;
|
||||||
|
LOADING = LOADING;
|
||||||
|
DONE = DONE;
|
||||||
|
|
||||||
|
_readyState: ReadyState;
|
||||||
|
_error: ?Error;
|
||||||
|
_result: ?ReaderResult;
|
||||||
|
_aborted: boolean = false;
|
||||||
|
_subscriptions: Array<*> = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
_reset(): void {
|
||||||
|
this._readyState = EMPTY;
|
||||||
|
this._error = null;
|
||||||
|
this._result = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearSubscriptions(): void {
|
||||||
|
this._subscriptions.forEach(sub => sub.remove());
|
||||||
|
this._subscriptions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_setReadyState(newState: ReadyState) {
|
||||||
|
this._readyState = newState;
|
||||||
|
this.dispatchEvent({type: 'readystatechange'});
|
||||||
|
if (newState === DONE) {
|
||||||
|
if (this._aborted) {
|
||||||
|
this.dispatchEvent({type: 'abort'});
|
||||||
|
} else if (this._error) {
|
||||||
|
this.dispatchEvent({type: 'error'});
|
||||||
|
} else {
|
||||||
|
this.dispatchEvent({type: 'load'});
|
||||||
|
}
|
||||||
|
this.dispatchEvent({type: 'loadend'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readAsArrayBuffer() {
|
||||||
|
throw new Error('FileReader.readAsArrayBuffer is not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
readAsDataURL(blob: Blob) {
|
||||||
|
this._aborted = false;
|
||||||
|
|
||||||
|
FileReaderModule.readAsDataURL(blob.data).then(
|
||||||
|
(text: string) => {
|
||||||
|
if (this._aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._result = text;
|
||||||
|
this._setReadyState(DONE);
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
if (this._aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._error = error;
|
||||||
|
this._setReadyState(DONE);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
readAsText(blob: Blob, encoding: string = 'UTF-8') {
|
||||||
|
this._aborted = false;
|
||||||
|
|
||||||
|
FileReaderModule.readAsText(blob.data, encoding).then(
|
||||||
|
(text: string) => {
|
||||||
|
if (this._aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._result = text;
|
||||||
|
this._setReadyState(DONE);
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
if (this._aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._error = error;
|
||||||
|
this._setReadyState(DONE);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
abort() {
|
||||||
|
this._aborted = true;
|
||||||
|
// only call onreadystatechange if there is something to abort, as per spec
|
||||||
|
if (this._readyState !== EMPTY && this._readyState !== DONE) {
|
||||||
|
this._reset();
|
||||||
|
this._setReadyState(DONE);
|
||||||
|
}
|
||||||
|
// Reset again after, in case modified in handler
|
||||||
|
this._reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
get readyState(): ReadyState {
|
||||||
|
return this._readyState;
|
||||||
|
}
|
||||||
|
|
||||||
|
get error(): ?Error {
|
||||||
|
return this._error;
|
||||||
|
}
|
||||||
|
|
||||||
|
get result(): ?ReaderResult {
|
||||||
|
return this._result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FileReader;
|
|
@ -7,12 +7,18 @@
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
19BA88FE1F84391700741C5A /* RCTFileReaderModule.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = ADDFBA6A1F33455F0064C998 /* RCTFileReaderModule.h */; };
|
||||||
|
19BA88FF1F84392900741C5A /* RCTFileReaderModule.h in Headers */ = {isa = PBXBuildFile; fileRef = ADDFBA6A1F33455F0064C998 /* RCTFileReaderModule.h */; };
|
||||||
|
19BA89001F84392F00741C5A /* RCTFileReaderModule.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = ADDFBA6A1F33455F0064C998 /* RCTFileReaderModule.h */; };
|
||||||
|
19BA89011F84393D00741C5A /* RCTFileReaderModule.m in Sources */ = {isa = PBXBuildFile; fileRef = ADDFBA6B1F33455F0064C998 /* RCTFileReaderModule.m */; };
|
||||||
AD0871131E215B28007D136D /* RCTBlobManager.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */; };
|
AD0871131E215B28007D136D /* RCTBlobManager.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */; };
|
||||||
AD0871161E215EC9007D136D /* RCTBlobManager.h in Headers */ = {isa = PBXBuildFile; fileRef = AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */; };
|
AD0871161E215EC9007D136D /* RCTBlobManager.h in Headers */ = {isa = PBXBuildFile; fileRef = AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */; };
|
||||||
AD0871181E215ED1007D136D /* RCTBlobManager.h in Headers */ = {isa = PBXBuildFile; fileRef = AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */; };
|
AD0871181E215ED1007D136D /* RCTBlobManager.h in Headers */ = {isa = PBXBuildFile; fileRef = AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */; };
|
||||||
AD08711A1E2162C8007D136D /* RCTBlobManager.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */; };
|
AD08711A1E2162C8007D136D /* RCTBlobManager.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */; };
|
||||||
AD9A43C31DFC7126008DC588 /* RCTBlobManager.m in Sources */ = {isa = PBXBuildFile; fileRef = AD9A43C21DFC7126008DC588 /* RCTBlobManager.m */; };
|
AD9A43C31DFC7126008DC588 /* RCTBlobManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = AD9A43C21DFC7126008DC588 /* RCTBlobManager.mm */; };
|
||||||
ADD01A711E09404A00F6D226 /* RCTBlobManager.m in Sources */ = {isa = PBXBuildFile; fileRef = AD9A43C21DFC7126008DC588 /* RCTBlobManager.m */; };
|
ADD01A711E09404A00F6D226 /* RCTBlobManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = AD9A43C21DFC7126008DC588 /* RCTBlobManager.mm */; };
|
||||||
|
ADDFBA6C1F33455F0064C998 /* RCTFileReaderModule.h in Headers */ = {isa = PBXBuildFile; fileRef = ADDFBA6A1F33455F0064C998 /* RCTFileReaderModule.h */; };
|
||||||
|
ADDFBA6D1F33455F0064C998 /* RCTFileReaderModule.m in Sources */ = {isa = PBXBuildFile; fileRef = ADDFBA6B1F33455F0064C998 /* RCTFileReaderModule.m */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
@ -22,6 +28,7 @@
|
||||||
dstPath = include/RCTBlob;
|
dstPath = include/RCTBlob;
|
||||||
dstSubfolderSpec = 16;
|
dstSubfolderSpec = 16;
|
||||||
files = (
|
files = (
|
||||||
|
19BA88FE1F84391700741C5A /* RCTFileReaderModule.h in Copy Headers */,
|
||||||
AD08711A1E2162C8007D136D /* RCTBlobManager.h in Copy Headers */,
|
AD08711A1E2162C8007D136D /* RCTBlobManager.h in Copy Headers */,
|
||||||
);
|
);
|
||||||
name = "Copy Headers";
|
name = "Copy Headers";
|
||||||
|
@ -33,6 +40,7 @@
|
||||||
dstPath = include/RCTBlob;
|
dstPath = include/RCTBlob;
|
||||||
dstSubfolderSpec = 16;
|
dstSubfolderSpec = 16;
|
||||||
files = (
|
files = (
|
||||||
|
19BA89001F84392F00741C5A /* RCTFileReaderModule.h in Copy Headers */,
|
||||||
AD0871131E215B28007D136D /* RCTBlobManager.h in Copy Headers */,
|
AD0871131E215B28007D136D /* RCTBlobManager.h in Copy Headers */,
|
||||||
);
|
);
|
||||||
name = "Copy Headers";
|
name = "Copy Headers";
|
||||||
|
@ -42,17 +50,21 @@
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
358F4ED71D1E81A9004DF814 /* libRCTBlob.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTBlob.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
358F4ED71D1E81A9004DF814 /* libRCTBlob.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTBlob.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBlobManager.h; sourceTree = "<group>"; };
|
AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTBlobManager.h; sourceTree = "<group>"; };
|
||||||
AD9A43C21DFC7126008DC588 /* RCTBlobManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBlobManager.m; sourceTree = "<group>"; };
|
AD9A43C21DFC7126008DC588 /* RCTBlobManager.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTBlobManager.mm; sourceTree = "<group>"; };
|
||||||
ADD01A681E09402E00F6D226 /* libRCTBlob-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libRCTBlob-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
ADD01A681E09402E00F6D226 /* libRCTBlob-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libRCTBlob-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
ADDFBA6A1F33455F0064C998 /* RCTFileReaderModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTFileReaderModule.h; sourceTree = "<group>"; };
|
||||||
|
ADDFBA6B1F33455F0064C998 /* RCTFileReaderModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTFileReaderModule.m; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
358F4ECE1D1E81A9004DF814 = {
|
358F4ECE1D1E81A9004DF814 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
ADDFBA6A1F33455F0064C998 /* RCTFileReaderModule.h */,
|
||||||
|
ADDFBA6B1F33455F0064C998 /* RCTFileReaderModule.m */,
|
||||||
AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */,
|
AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */,
|
||||||
AD9A43C21DFC7126008DC588 /* RCTBlobManager.m */,
|
AD9A43C21DFC7126008DC588 /* RCTBlobManager.mm */,
|
||||||
358F4ED81D1E81A9004DF814 /* Products */,
|
358F4ED81D1E81A9004DF814 /* Products */,
|
||||||
);
|
);
|
||||||
indentWidth = 2;
|
indentWidth = 2;
|
||||||
|
@ -77,6 +89,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
AD0871161E215EC9007D136D /* RCTBlobManager.h in Headers */,
|
AD0871161E215EC9007D136D /* RCTBlobManager.h in Headers */,
|
||||||
|
ADDFBA6C1F33455F0064C998 /* RCTFileReaderModule.h in Headers */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -84,6 +97,7 @@
|
||||||
isa = PBXHeadersBuildPhase;
|
isa = PBXHeadersBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
19BA88FF1F84392900741C5A /* RCTFileReaderModule.h in Headers */,
|
||||||
AD0871181E215ED1007D136D /* RCTBlobManager.h in Headers */,
|
AD0871181E215ED1007D136D /* RCTBlobManager.h in Headers */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -132,7 +146,7 @@
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastUpgradeCheck = 0730;
|
LastUpgradeCheck = 0730;
|
||||||
ORGANIZATIONNAME = "Silk Labs";
|
ORGANIZATIONNAME = Facebook;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
358F4ED61D1E81A9004DF814 = {
|
358F4ED61D1E81A9004DF814 = {
|
||||||
CreatedOnToolsVersion = 7.3;
|
CreatedOnToolsVersion = 7.3;
|
||||||
|
@ -166,7 +180,8 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
AD9A43C31DFC7126008DC588 /* RCTBlobManager.m in Sources */,
|
ADDFBA6D1F33455F0064C998 /* RCTFileReaderModule.m in Sources */,
|
||||||
|
AD9A43C31DFC7126008DC588 /* RCTBlobManager.mm in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -174,7 +189,8 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
ADD01A711E09404A00F6D226 /* RCTBlobManager.m in Sources */,
|
19BA89011F84393D00741C5A /* RCTFileReaderModule.m in Sources */,
|
||||||
|
ADD01A711E09404A00F6D226 /* RCTBlobManager.mm in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,4 +13,18 @@
|
||||||
|
|
||||||
@interface RCTBlobManager : NSObject <RCTBridgeModule, RCTURLRequestHandler>
|
@interface RCTBlobManager : NSObject <RCTBridgeModule, RCTURLRequestHandler>
|
||||||
|
|
||||||
|
- (NSString *)store:(NSData *)data;
|
||||||
|
|
||||||
|
- (void)store:(NSData *)data withId:(NSString *)blobId;
|
||||||
|
|
||||||
|
- (NSData *)resolve:(NSDictionary<NSString *, id> *)blob;
|
||||||
|
|
||||||
|
- (NSData *)resolve:(NSString *)blobId offset:(NSInteger)offset size:(NSInteger)size;
|
||||||
|
|
||||||
|
- (NSData *)resolveURL:(NSURL *)url;
|
||||||
|
|
||||||
|
- (void)remove:(NSString *)blobId;
|
||||||
|
|
||||||
|
- (void)createFromParts:(NSArray<NSDictionary<NSString *, id> *> *)parts withId:(NSString *)blobId;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -1,218 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) 2015-present, Facebook, Inc.
|
|
||||||
* All rights reserved.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the BSD-style license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree. An additional grant
|
|
||||||
* of patent rights can be found in the PATENTS file in the same directory.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import "RCTBlobManager.h"
|
|
||||||
|
|
||||||
#import <React/RCTConvert.h>
|
|
||||||
#import <React/RCTWebSocketModule.h>
|
|
||||||
|
|
||||||
static NSString *const kBlobUriScheme = @"blob";
|
|
||||||
|
|
||||||
@interface _RCTBlobContentHandler : NSObject <RCTWebSocketContentHandler>
|
|
||||||
|
|
||||||
- (instancetype)initWithBlobManager:(RCTBlobManager *)blobManager;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
|
|
||||||
@implementation RCTBlobManager
|
|
||||||
{
|
|
||||||
NSMutableDictionary<NSString *, NSData *> *_blobs;
|
|
||||||
_RCTBlobContentHandler *_contentHandler;
|
|
||||||
NSOperationQueue *_queue;
|
|
||||||
}
|
|
||||||
|
|
||||||
RCT_EXPORT_MODULE(BlobModule)
|
|
||||||
|
|
||||||
@synthesize bridge = _bridge;
|
|
||||||
|
|
||||||
+ (BOOL)requiresMainQueueSetup
|
|
||||||
{
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSDictionary<NSString *, id> *)constantsToExport
|
|
||||||
{
|
|
||||||
return @{
|
|
||||||
@"BLOB_URI_SCHEME": kBlobUriScheme,
|
|
||||||
@"BLOB_URI_HOST": [NSNull null],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
- (dispatch_queue_t)methodQueue
|
|
||||||
{
|
|
||||||
return [[_bridge webSocketModule] methodQueue];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSString *)store:(NSData *)data
|
|
||||||
{
|
|
||||||
NSString *blobId = [NSUUID UUID].UUIDString;
|
|
||||||
[self store:data withId:blobId];
|
|
||||||
return blobId;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)store:(NSData *)data withId:(NSString *)blobId
|
|
||||||
{
|
|
||||||
if (!_blobs) {
|
|
||||||
_blobs = [NSMutableDictionary new];
|
|
||||||
}
|
|
||||||
|
|
||||||
_blobs[blobId] = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSData *)resolve:(NSDictionary<NSString *, id> *)blob
|
|
||||||
{
|
|
||||||
NSString *blobId = [RCTConvert NSString:blob[@"blobId"]];
|
|
||||||
NSNumber *offset = [RCTConvert NSNumber:blob[@"offset"]];
|
|
||||||
NSNumber *size = [RCTConvert NSNumber:blob[@"size"]];
|
|
||||||
|
|
||||||
return [self resolve:blobId
|
|
||||||
offset:offset ? [offset integerValue] : 0
|
|
||||||
size:size ? [size integerValue] : -1];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSData *)resolve:(NSString *)blobId offset:(NSInteger)offset size:(NSInteger)size
|
|
||||||
{
|
|
||||||
NSData *data = _blobs[blobId];
|
|
||||||
if (!data) {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
if (offset != 0 || (size != -1 && size != data.length)) {
|
|
||||||
data = [data subdataWithRange:NSMakeRange(offset, size)];
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
RCT_EXPORT_METHOD(enableBlobSupport:(nonnull NSNumber *)socketID)
|
|
||||||
{
|
|
||||||
if (!_contentHandler) {
|
|
||||||
_contentHandler = [[_RCTBlobContentHandler alloc] initWithBlobManager:self];
|
|
||||||
}
|
|
||||||
[[_bridge webSocketModule] setContentHandler:_contentHandler forSocketID:socketID];
|
|
||||||
}
|
|
||||||
|
|
||||||
RCT_EXPORT_METHOD(disableBlobSupport:(nonnull NSNumber *)socketID)
|
|
||||||
{
|
|
||||||
[[_bridge webSocketModule] setContentHandler:nil forSocketID:socketID];
|
|
||||||
}
|
|
||||||
|
|
||||||
RCT_EXPORT_METHOD(sendBlob:(NSDictionary *)blob socketID:(nonnull NSNumber *)socketID)
|
|
||||||
{
|
|
||||||
[[_bridge webSocketModule] sendData:[self resolve:blob] forSocketID:socketID];
|
|
||||||
}
|
|
||||||
|
|
||||||
RCT_EXPORT_METHOD(createFromParts:(NSArray<NSDictionary<NSString *, id> *> *)parts withId:(NSString *)blobId)
|
|
||||||
{
|
|
||||||
NSMutableData *data = [NSMutableData new];
|
|
||||||
for (NSDictionary<NSString *, id> *part in parts) {
|
|
||||||
NSData *partData = [self resolve:part];
|
|
||||||
[data appendData:partData];
|
|
||||||
}
|
|
||||||
[self store:data withId:blobId];
|
|
||||||
}
|
|
||||||
|
|
||||||
RCT_EXPORT_METHOD(release:(NSString *)blobId)
|
|
||||||
{
|
|
||||||
[_blobs removeObjectForKey:blobId];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - RCTURLRequestHandler methods
|
|
||||||
|
|
||||||
- (BOOL)canHandleRequest:(NSURLRequest *)request
|
|
||||||
{
|
|
||||||
return [request.URL.scheme caseInsensitiveCompare:kBlobUriScheme] == NSOrderedSame;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (id)sendRequest:(NSURLRequest *)request withDelegate:(id<RCTURLRequestDelegate>)delegate
|
|
||||||
{
|
|
||||||
// Lazy setup
|
|
||||||
if (!_queue) {
|
|
||||||
_queue = [NSOperationQueue new];
|
|
||||||
_queue.maxConcurrentOperationCount = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
__weak __block NSBlockOperation *weakOp;
|
|
||||||
__block NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
|
|
||||||
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL
|
|
||||||
MIMEType:nil
|
|
||||||
expectedContentLength:-1
|
|
||||||
textEncodingName:nil];
|
|
||||||
|
|
||||||
[delegate URLRequest:weakOp didReceiveResponse:response];
|
|
||||||
|
|
||||||
NSURLComponents *components = [[NSURLComponents alloc] initWithURL:request.URL resolvingAgainstBaseURL:NO];
|
|
||||||
|
|
||||||
NSString *blobId = components.path;
|
|
||||||
NSInteger offset = 0;
|
|
||||||
NSInteger size = -1;
|
|
||||||
|
|
||||||
if (components.queryItems) {
|
|
||||||
for (NSURLQueryItem *queryItem in components.queryItems) {
|
|
||||||
if ([queryItem.name isEqualToString:@"offset"]) {
|
|
||||||
offset = [queryItem.value integerValue];
|
|
||||||
}
|
|
||||||
if ([queryItem.name isEqualToString:@"size"]) {
|
|
||||||
size = [queryItem.value integerValue];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NSData *data;
|
|
||||||
if (blobId) {
|
|
||||||
data = [self resolve:blobId offset:offset size:size];
|
|
||||||
}
|
|
||||||
NSError *error;
|
|
||||||
if (data) {
|
|
||||||
[delegate URLRequest:weakOp didReceiveData:data];
|
|
||||||
} else {
|
|
||||||
error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorBadURL userInfo:nil];
|
|
||||||
}
|
|
||||||
[delegate URLRequest:weakOp didCompleteWithError:error];
|
|
||||||
}];
|
|
||||||
|
|
||||||
weakOp = op;
|
|
||||||
[_queue addOperation:op];
|
|
||||||
return op;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)cancelRequest:(NSOperation *)op
|
|
||||||
{
|
|
||||||
[op cancel];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation _RCTBlobContentHandler {
|
|
||||||
__weak RCTBlobManager *_blobManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (instancetype)initWithBlobManager:(RCTBlobManager *)blobManager
|
|
||||||
{
|
|
||||||
if (self = [super init]) {
|
|
||||||
_blobManager = blobManager;
|
|
||||||
}
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (id)processMessage:(id)message forSocketID:(NSNumber *)socketID withType:(NSString *__autoreleasing _Nonnull *)type
|
|
||||||
{
|
|
||||||
if (![message isKindOfClass:[NSData class]]) {
|
|
||||||
*type = @"text";
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
*type = @"blob";
|
|
||||||
return @{
|
|
||||||
@"blobId": [_blobManager store:message],
|
|
||||||
@"offset": @0,
|
|
||||||
@"size": @(((NSData *)message).length),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
|
@ -0,0 +1,290 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import "RCTBlobManager.h"
|
||||||
|
|
||||||
|
#import <mutex>
|
||||||
|
|
||||||
|
#import <React/RCTConvert.h>
|
||||||
|
#import <React/RCTNetworking.h>
|
||||||
|
#import <React/RCTWebSocketModule.h>
|
||||||
|
|
||||||
|
static NSString *const kBlobURIScheme = @"blob";
|
||||||
|
|
||||||
|
@interface RCTBlobManager () <RCTNetworkingRequestHandler, RCTNetworkingResponseHandler, RCTWebSocketContentHandler>
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation RCTBlobManager
|
||||||
|
{
|
||||||
|
// Blobs should be thread safe since they are used from the websocket and networking module,
|
||||||
|
// make sure to use proper locking when accessing this.
|
||||||
|
NSMutableDictionary<NSString *, NSData *> *_blobs;
|
||||||
|
std::mutex _blobsMutex;
|
||||||
|
|
||||||
|
NSOperationQueue *_queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RCT_EXPORT_MODULE(BlobModule)
|
||||||
|
|
||||||
|
@synthesize bridge = _bridge;
|
||||||
|
|
||||||
|
- (void)setBridge:(RCTBridge *)bridge
|
||||||
|
{
|
||||||
|
_bridge = bridge;
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(_blobsMutex);
|
||||||
|
_blobs = [NSMutableDictionary new];
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (BOOL)requiresMainQueueSetup
|
||||||
|
{
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSDictionary<NSString *, id> *)constantsToExport
|
||||||
|
{
|
||||||
|
return @{
|
||||||
|
@"BLOB_URI_SCHEME": kBlobURIScheme,
|
||||||
|
@"BLOB_URI_HOST": [NSNull null],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)store:(NSData *)data
|
||||||
|
{
|
||||||
|
NSString *blobId = [NSUUID UUID].UUIDString;
|
||||||
|
[self store:data withId:blobId];
|
||||||
|
return blobId;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)store:(NSData *)data withId:(NSString *)blobId
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_blobsMutex);
|
||||||
|
_blobs[blobId] = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSData *)resolve:(NSDictionary<NSString *, id> *)blob
|
||||||
|
{
|
||||||
|
NSString *blobId = [RCTConvert NSString:blob[@"blobId"]];
|
||||||
|
NSNumber *offset = [RCTConvert NSNumber:blob[@"offset"]];
|
||||||
|
NSNumber *size = [RCTConvert NSNumber:blob[@"size"]];
|
||||||
|
return [self resolve:blobId
|
||||||
|
offset:offset ? [offset integerValue] : 0
|
||||||
|
size:size ? [size integerValue] : -1];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSData *)resolve:(NSString *)blobId offset:(NSInteger)offset size:(NSInteger)size
|
||||||
|
{
|
||||||
|
NSData *data;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_blobsMutex);
|
||||||
|
data = _blobs[blobId];
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
if (offset != 0 || (size != -1 && size != data.length)) {
|
||||||
|
data = [data subdataWithRange:NSMakeRange(offset, size)];
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSData *)resolveURL:(NSURL *)url
|
||||||
|
{
|
||||||
|
NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
|
||||||
|
|
||||||
|
NSString *blobId = components.path;
|
||||||
|
NSInteger offset = 0;
|
||||||
|
NSInteger size = -1;
|
||||||
|
|
||||||
|
if (components.queryItems) {
|
||||||
|
for (NSURLQueryItem *queryItem in components.queryItems) {
|
||||||
|
if ([queryItem.name isEqualToString:@"offset"]) {
|
||||||
|
offset = [queryItem.value integerValue];
|
||||||
|
}
|
||||||
|
if ([queryItem.name isEqualToString:@"size"]) {
|
||||||
|
size = [queryItem.value integerValue];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blobId) {
|
||||||
|
return [self resolve:blobId offset:offset size:size];
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)remove:(NSString *)blobId
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(_blobsMutex);
|
||||||
|
[_blobs removeObjectForKey:blobId];
|
||||||
|
}
|
||||||
|
|
||||||
|
RCT_EXPORT_METHOD(addNetworkingHandler)
|
||||||
|
{
|
||||||
|
dispatch_async(_bridge.networking.methodQueue, ^{
|
||||||
|
[self->_bridge.networking addRequestHandler:self];
|
||||||
|
[self->_bridge.networking addResponseHandler:self];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
RCT_EXPORT_METHOD(addWebSocketHandler:(nonnull NSNumber *)socketID)
|
||||||
|
{
|
||||||
|
dispatch_async(_bridge.webSocketModule.methodQueue, ^{
|
||||||
|
[self->_bridge.webSocketModule setContentHandler:self forSocketID:socketID];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
RCT_EXPORT_METHOD(removeWebSocketHandler:(nonnull NSNumber *)socketID)
|
||||||
|
{
|
||||||
|
dispatch_async(_bridge.webSocketModule.methodQueue, ^{
|
||||||
|
[self->_bridge.webSocketModule setContentHandler:nil forSocketID:socketID];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
|
||||||
|
RCT_EXPORT_METHOD(sendOverSocket:(NSDictionary *)blob socketID:(nonnull NSNumber *)socketID)
|
||||||
|
{
|
||||||
|
dispatch_async(_bridge.webSocketModule.methodQueue, ^{
|
||||||
|
[self->_bridge.webSocketModule sendData:[self resolve:blob] forSocketID:socketID];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
RCT_EXPORT_METHOD(createFromParts:(NSArray<NSDictionary<NSString *, id> *> *)parts withId:(NSString *)blobId)
|
||||||
|
{
|
||||||
|
NSMutableData *data = [NSMutableData new];
|
||||||
|
for (NSDictionary<NSString *, id> *part in parts) {
|
||||||
|
NSString *type = [RCTConvert NSString:part[@"type"]];
|
||||||
|
|
||||||
|
if ([type isEqualToString:@"blob"]) {
|
||||||
|
NSData *partData = [self resolve:part[@"data"]];
|
||||||
|
[data appendData:partData];
|
||||||
|
} else if ([type isEqualToString:@"string"]) {
|
||||||
|
NSData *partData = [[RCTConvert NSString:part[@"data"]] dataUsingEncoding:NSUTF8StringEncoding];
|
||||||
|
[data appendData:partData];
|
||||||
|
} else {
|
||||||
|
[NSException raise:@"Invalid type for blob" format:@"%@ is invalid", type];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[self store:data withId:blobId];
|
||||||
|
}
|
||||||
|
|
||||||
|
RCT_EXPORT_METHOD(release:(NSString *)blobId)
|
||||||
|
{
|
||||||
|
[self remove:blobId];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - RCTURLRequestHandler methods
|
||||||
|
|
||||||
|
- (BOOL)canHandleRequest:(NSURLRequest *)request
|
||||||
|
{
|
||||||
|
return [request.URL.scheme caseInsensitiveCompare:kBlobURIScheme] == NSOrderedSame;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id)sendRequest:(NSURLRequest *)request withDelegate:(id<RCTURLRequestDelegate>)delegate
|
||||||
|
{
|
||||||
|
// Lazy setup
|
||||||
|
if (!_queue) {
|
||||||
|
_queue = [NSOperationQueue new];
|
||||||
|
_queue.maxConcurrentOperationCount = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
__weak __typeof(self) weakSelf = self;
|
||||||
|
__weak __block NSBlockOperation *weakOp;
|
||||||
|
__block NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
|
||||||
|
__typeof(self) strongSelf = weakSelf;
|
||||||
|
if (!strongSelf) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL
|
||||||
|
MIMEType:nil
|
||||||
|
expectedContentLength:-1
|
||||||
|
textEncodingName:nil];
|
||||||
|
|
||||||
|
[delegate URLRequest:weakOp didReceiveResponse:response];
|
||||||
|
|
||||||
|
NSData *data = [strongSelf resolveURL:response.URL];
|
||||||
|
NSError *error;
|
||||||
|
if (data) {
|
||||||
|
[delegate URLRequest:weakOp didReceiveData:data];
|
||||||
|
} else {
|
||||||
|
error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorBadURL userInfo:nil];
|
||||||
|
}
|
||||||
|
[delegate URLRequest:weakOp didCompleteWithError:error];
|
||||||
|
}];
|
||||||
|
|
||||||
|
weakOp = op;
|
||||||
|
[_queue addOperation:op];
|
||||||
|
return op;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)cancelRequest:(NSOperation *)op
|
||||||
|
{
|
||||||
|
[op cancel];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - RCTNetworkingRequestHandler methods
|
||||||
|
|
||||||
|
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
|
||||||
|
- (BOOL)canHandleNetworkingRequest:(NSDictionary *)data
|
||||||
|
{
|
||||||
|
return data[@"blob"] != nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
|
||||||
|
- (NSDictionary *)handleNetworkingRequest:(NSDictionary *)data
|
||||||
|
{
|
||||||
|
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
|
||||||
|
NSDictionary *blob = [RCTConvert NSDictionary:data[@"blob"]];
|
||||||
|
|
||||||
|
NSString *contentType = @"application/octet-stream";
|
||||||
|
NSString *blobType = [RCTConvert NSString:blob[@"type"]];
|
||||||
|
if (blobType != nil && blobType.length > 0) {
|
||||||
|
contentType = blob[@"type"];
|
||||||
|
}
|
||||||
|
|
||||||
|
return @{@"body": [self resolve:blob], @"contentType": contentType};
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)canHandleNetworkingResponse:(NSString *)responseType
|
||||||
|
{
|
||||||
|
return [responseType isEqualToString:@"blob"];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id)handleNetworkingResponse:(NSURLResponse *)response data:(NSData *)data
|
||||||
|
{
|
||||||
|
return @{
|
||||||
|
@"blobId": [self store:data],
|
||||||
|
@"offset": @0,
|
||||||
|
@"size": @(data.length),
|
||||||
|
@"name": [response suggestedFilename],
|
||||||
|
@"type": [response MIMEType],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - RCTWebSocketContentHandler methods
|
||||||
|
|
||||||
|
- (id)processWebsocketMessage:(id)message
|
||||||
|
forSocketID:(NSNumber *)socketID
|
||||||
|
withType:(NSString *__autoreleasing _Nonnull *)type
|
||||||
|
{
|
||||||
|
if (![message isKindOfClass:[NSData class]]) {
|
||||||
|
*type = @"text";
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
*type = @"blob";
|
||||||
|
return @{
|
||||||
|
@"blobId": [self store:message],
|
||||||
|
@"offset": @0,
|
||||||
|
@"size": @(((NSData *)message).length),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import <React/RCTBridgeModule.h>
|
||||||
|
|
||||||
|
@interface RCTFileReaderModule : NSObject <RCTBridgeModule>
|
||||||
|
|
||||||
|
@end
|
|
@ -0,0 +1,71 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#import "RCTFileReaderModule.h"
|
||||||
|
|
||||||
|
#import <React/RCTBridge.h>
|
||||||
|
#import <React/RCTConvert.h>
|
||||||
|
|
||||||
|
#import "RCTBlobManager.h"
|
||||||
|
|
||||||
|
|
||||||
|
@implementation RCTFileReaderModule
|
||||||
|
|
||||||
|
RCT_EXPORT_MODULE(FileReaderModule)
|
||||||
|
|
||||||
|
@synthesize bridge = _bridge;
|
||||||
|
|
||||||
|
RCT_EXPORT_METHOD(readAsText:(NSDictionary<NSString *, id> *)blob
|
||||||
|
encoding:(NSString *)encoding
|
||||||
|
resolve:(RCTPromiseResolveBlock)resolve
|
||||||
|
reject:(RCTPromiseRejectBlock)reject)
|
||||||
|
{
|
||||||
|
RCTBlobManager *blobManager = [[self bridge] moduleForClass:[RCTBlobManager class]];
|
||||||
|
NSData *data = [blobManager resolve:blob];
|
||||||
|
|
||||||
|
if (data == nil) {
|
||||||
|
reject(RCTErrorUnspecified,
|
||||||
|
[NSString stringWithFormat:@"Unable to resolve data for blob: %@", [RCTConvert NSString:blob[@"blobId"]]], nil);
|
||||||
|
} else {
|
||||||
|
NSStringEncoding stringEncoding;
|
||||||
|
|
||||||
|
if (encoding == nil) {
|
||||||
|
stringEncoding = NSUTF8StringEncoding;
|
||||||
|
} else {
|
||||||
|
stringEncoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding((CFStringRef) encoding));
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *text = [[NSString alloc] initWithData:data encoding:stringEncoding];
|
||||||
|
|
||||||
|
resolve(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RCT_EXPORT_METHOD(readAsDataURL:(NSDictionary<NSString *, id> *)blob
|
||||||
|
resolve:(RCTPromiseResolveBlock)resolve
|
||||||
|
reject:(RCTPromiseRejectBlock)reject)
|
||||||
|
{
|
||||||
|
RCTBlobManager *blobManager = [[self bridge] moduleForClass:[RCTBlobManager class]];
|
||||||
|
NSData *data = [blobManager resolve:blob];
|
||||||
|
|
||||||
|
if (data == nil) {
|
||||||
|
reject(RCTErrorUnspecified,
|
||||||
|
[NSString stringWithFormat:@"Unable to resolve data for blob: %@", [RCTConvert NSString:blob[@"blobId"]]], nil);
|
||||||
|
} else {
|
||||||
|
NSString *type = [RCTConvert NSString:blob[@"type"]];
|
||||||
|
NSString *text = [NSString stringWithFormat:@"data:%@;base64,%@",
|
||||||
|
type != nil && [type length] > 0 ? type : @"application/octet-stream",
|
||||||
|
[data base64EncodedStringWithOptions:0]];
|
||||||
|
|
||||||
|
resolve(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
|
@ -52,16 +52,16 @@ if (BlobModule && typeof BlobModule.BLOB_URI_SCHEME === 'string') {
|
||||||
*/
|
*/
|
||||||
class URL {
|
class URL {
|
||||||
constructor() {
|
constructor() {
|
||||||
throw new Error('Creating BlobURL objects is not supported yet.');
|
throw new Error('Creating URL objects is not supported yet.');
|
||||||
}
|
}
|
||||||
|
|
||||||
static createObjectURL(blob: Blob) {
|
static createObjectURL(blob: Blob) {
|
||||||
if (BLOB_URL_PREFIX === null) {
|
if (BLOB_URL_PREFIX === null) {
|
||||||
throw new Error('Cannot create URL for blob!');
|
throw new Error('Cannot create URL for blob!');
|
||||||
}
|
}
|
||||||
return `${BLOB_URL_PREFIX}${blob.blobId}?offset=${blob.offset}&size=${
|
return `${BLOB_URL_PREFIX}${blob.data.blobId}?offset=${
|
||||||
blob.size
|
blob.data.offset
|
||||||
}`;
|
}&size=${blob.size}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static revokeObjectURL(url: string) {
|
static revokeObjectURL(url: string) {
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
const BlobModule = {
|
||||||
|
createFromParts() {},
|
||||||
|
release() {},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = BlobModule;
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
const FileReaderModule = {
|
||||||
|
async readAsText() {
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
async readAsDataURL() {
|
||||||
|
return 'data:text/plain;base64,NDI=';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = FileReaderModule;
|
|
@ -0,0 +1,84 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
* @emails oncall+react_native
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
jest.setMock('NativeModules', {
|
||||||
|
BlobModule: require('../__mocks__/BlobModule'),
|
||||||
|
});
|
||||||
|
|
||||||
|
var Blob = require('Blob');
|
||||||
|
|
||||||
|
describe('Blob', function() {
|
||||||
|
it('should create empty blob', () => {
|
||||||
|
const blob = new Blob();
|
||||||
|
expect(blob).toBeInstanceOf(Blob);
|
||||||
|
expect(blob.data.offset).toBe(0);
|
||||||
|
expect(blob.data.size).toBe(0);
|
||||||
|
expect(blob.size).toBe(0);
|
||||||
|
expect(blob.type).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create blob from other blobs and strings', () => {
|
||||||
|
const blobA = new Blob();
|
||||||
|
const blobB = new Blob();
|
||||||
|
const textA = 'i \u2665 dogs';
|
||||||
|
const textB = '\uD800\uDC00';
|
||||||
|
const textC =
|
||||||
|
'Z\u0351\u036B\u0343\u036A\u0302\u036B\u033D\u034F\u0334\u0319\u0324' +
|
||||||
|
'\u031E\u0349\u035A\u032F\u031E\u0320\u034DA\u036B\u0357\u0334\u0362' +
|
||||||
|
'\u0335\u031C\u0330\u0354L\u0368\u0367\u0369\u0358\u0320G\u0311\u0357' +
|
||||||
|
'\u030E\u0305\u035B\u0341\u0334\u033B\u0348\u034D\u0354\u0339O\u0342' +
|
||||||
|
'\u030C\u030C\u0358\u0328\u0335\u0339\u033B\u031D\u0333!\u033F\u030B' +
|
||||||
|
'\u0365\u0365\u0302\u0363\u0310\u0301\u0301\u035E\u035C\u0356\u032C' +
|
||||||
|
'\u0330\u0319\u0317';
|
||||||
|
|
||||||
|
blobA.data.size = 34540;
|
||||||
|
blobB.data.size = 65452;
|
||||||
|
|
||||||
|
const blob = new Blob([blobA, blobB, textA, textB, textC]);
|
||||||
|
|
||||||
|
expect(blob.size).toBe(
|
||||||
|
blobA.size +
|
||||||
|
blobB.size +
|
||||||
|
global.Buffer.byteLength(textA, 'UTF-8') +
|
||||||
|
global.Buffer.byteLength(textB, 'UTF-8') +
|
||||||
|
global.Buffer.byteLength(textC, 'UTF-8'),
|
||||||
|
);
|
||||||
|
expect(blob.type).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should slice a blob', () => {
|
||||||
|
const blob = new Blob();
|
||||||
|
|
||||||
|
blob.data.size = 34546;
|
||||||
|
|
||||||
|
const sliceA = blob.slice(0, 2354);
|
||||||
|
|
||||||
|
expect(sliceA.data.offset).toBe(0);
|
||||||
|
expect(sliceA.size).toBe(2354);
|
||||||
|
expect(sliceA.type).toBe('');
|
||||||
|
|
||||||
|
const sliceB = blob.slice(2384, 7621);
|
||||||
|
|
||||||
|
expect(sliceB.data.offset).toBe(2384);
|
||||||
|
expect(sliceB.size).toBe(7621 - 2384);
|
||||||
|
expect(sliceB.type).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close a blob', () => {
|
||||||
|
const blob = new Blob();
|
||||||
|
|
||||||
|
blob.close();
|
||||||
|
|
||||||
|
expect(() => blob.size).toThrow();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
* @emails oncall+react_native
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
jest.setMock('NativeModules', {
|
||||||
|
BlobModule: require('../__mocks__/BlobModule'),
|
||||||
|
});
|
||||||
|
|
||||||
|
var Blob = require('Blob');
|
||||||
|
var BlobManager = require('BlobManager');
|
||||||
|
|
||||||
|
describe('BlobManager', function() {
|
||||||
|
it('should create blob from parts', () => {
|
||||||
|
const blob = BlobManager.createFromParts([], {type: 'text/html'});
|
||||||
|
expect(blob).toBeInstanceOf(Blob);
|
||||||
|
expect(blob.type).toBe('text/html');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,46 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
* @emails oncall+react_native
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
jest.setMock('NativeModules', {
|
||||||
|
BlobModule: require('../__mocks__/BlobModule'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const File = require('File');
|
||||||
|
|
||||||
|
describe('File', function() {
|
||||||
|
it('should create empty file', () => {
|
||||||
|
const file = new File([], 'test.jpg');
|
||||||
|
expect(file).toBeInstanceOf(File);
|
||||||
|
expect(file.data.offset).toBe(0);
|
||||||
|
expect(file.data.size).toBe(0);
|
||||||
|
expect(file.size).toBe(0);
|
||||||
|
expect(file.type).toBe('');
|
||||||
|
expect(file.name).toBe('test.jpg');
|
||||||
|
expect(file.lastModified).toEqual(expect.any(Number));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create empty file with type', () => {
|
||||||
|
const file = new File([], 'test.jpg', {type: 'image/jpeg'});
|
||||||
|
expect(file.type).toBe('image/jpeg');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create empty file with lastModified', () => {
|
||||||
|
const file = new File([], 'test.jpg', {lastModified: 1337});
|
||||||
|
expect(file.lastModified).toBe(1337);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw on invalid arguments', () => {
|
||||||
|
expect(() => new File()).toThrow();
|
||||||
|
expect(() => new File([])).toThrow();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,42 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
* @emails oncall+react_native
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
jest.unmock('event-target-shim').setMock('NativeModules', {
|
||||||
|
BlobModule: require('../__mocks__/BlobModule'),
|
||||||
|
FileReaderModule: require('../__mocks__/FileReaderModule'),
|
||||||
|
});
|
||||||
|
|
||||||
|
var Blob = require('Blob');
|
||||||
|
var FileReader = require('FileReader');
|
||||||
|
|
||||||
|
describe('FileReader', function() {
|
||||||
|
it('should read blob as text', async () => {
|
||||||
|
const e = await new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = resolve;
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsText(new Blob());
|
||||||
|
});
|
||||||
|
expect(e.target.result).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read blob as data URL', async () => {
|
||||||
|
const e = await new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = resolve;
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(new Blob());
|
||||||
|
});
|
||||||
|
expect(e.target.result).toBe('data:text/plain;base64,NDI=');
|
||||||
|
});
|
||||||
|
});
|
|
@ -171,6 +171,8 @@ polyfillGlobal('Request', () => require('fetch').Request);
|
||||||
polyfillGlobal('Response', () => require('fetch').Response);
|
polyfillGlobal('Response', () => require('fetch').Response);
|
||||||
polyfillGlobal('WebSocket', () => require('WebSocket'));
|
polyfillGlobal('WebSocket', () => require('WebSocket'));
|
||||||
polyfillGlobal('Blob', () => require('Blob'));
|
polyfillGlobal('Blob', () => require('Blob'));
|
||||||
|
polyfillGlobal('File', () => require('File'));
|
||||||
|
polyfillGlobal('FileReader', () => require('FileReader'));
|
||||||
polyfillGlobal('URL', () => require('URL'));
|
polyfillGlobal('URL', () => require('URL'));
|
||||||
|
|
||||||
// Set up alert
|
// Set up alert
|
||||||
|
|
|
@ -10,6 +10,22 @@
|
||||||
#import <React/RCTEventEmitter.h>
|
#import <React/RCTEventEmitter.h>
|
||||||
#import <React/RCTNetworkTask.h>
|
#import <React/RCTNetworkTask.h>
|
||||||
|
|
||||||
|
@protocol RCTNetworkingRequestHandler <NSObject>
|
||||||
|
|
||||||
|
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
|
||||||
|
- (BOOL)canHandleNetworkingRequest:(NSDictionary *)data;
|
||||||
|
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
|
||||||
|
- (NSDictionary *)handleNetworkingRequest:(NSDictionary *)data;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@protocol RCTNetworkingResponseHandler <NSObject>
|
||||||
|
|
||||||
|
- (BOOL)canHandleNetworkingResponse:(NSString *)responseType;
|
||||||
|
- (id)handleNetworkingResponse:(NSURLResponse *)response data:(NSData *)data;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
@interface RCTNetworking : RCTEventEmitter
|
@interface RCTNetworking : RCTEventEmitter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,6 +40,14 @@
|
||||||
- (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request
|
- (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request
|
||||||
completionBlock:(RCTURLRequestCompletionBlock)completionBlock;
|
completionBlock:(RCTURLRequestCompletionBlock)completionBlock;
|
||||||
|
|
||||||
|
- (void)addRequestHandler:(id<RCTNetworkingRequestHandler>)handler;
|
||||||
|
|
||||||
|
- (void)addResponseHandler:(id<RCTNetworkingResponseHandler>)handler;
|
||||||
|
|
||||||
|
- (void)removeRequestHandler:(id<RCTNetworkingRequestHandler>)handler;
|
||||||
|
|
||||||
|
- (void)removeResponseHandler:(id<RCTNetworkingResponseHandler>)handler;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface RCTBridge (RCTNetworking)
|
@interface RCTBridge (RCTNetworking)
|
||||||
|
|
|
@ -18,6 +18,8 @@ const convertRequestBody = require('convertRequestBody');
|
||||||
|
|
||||||
import type {RequestBody} from 'convertRequestBody';
|
import type {RequestBody} from 'convertRequestBody';
|
||||||
|
|
||||||
|
import type { NativeResponseType } from './XMLHttpRequest';
|
||||||
|
|
||||||
class RCTNetworking extends NativeEventEmitter {
|
class RCTNetworking extends NativeEventEmitter {
|
||||||
|
|
||||||
isAvailable: boolean = true;
|
isAvailable: boolean = true;
|
||||||
|
@ -32,7 +34,7 @@ class RCTNetworking extends NativeEventEmitter {
|
||||||
url: string,
|
url: string,
|
||||||
headers: Object,
|
headers: Object,
|
||||||
data: RequestBody,
|
data: RequestBody,
|
||||||
responseType: 'text' | 'base64',
|
responseType: NativeResponseType,
|
||||||
incrementalUpdates: boolean,
|
incrementalUpdates: boolean,
|
||||||
timeout: number,
|
timeout: number,
|
||||||
callback: (requestId: number) => any,
|
callback: (requestId: number) => any,
|
||||||
|
|
|
@ -131,12 +131,20 @@ static NSString *RCTGenerateFormBoundary()
|
||||||
NSMutableDictionary<NSNumber *, RCTNetworkTask *> *_tasksByRequestID;
|
NSMutableDictionary<NSNumber *, RCTNetworkTask *> *_tasksByRequestID;
|
||||||
std::mutex _handlersLock;
|
std::mutex _handlersLock;
|
||||||
NSArray<id<RCTURLRequestHandler>> *_handlers;
|
NSArray<id<RCTURLRequestHandler>> *_handlers;
|
||||||
|
NSMutableArray<id<RCTNetworkingRequestHandler>> *_requestHandlers;
|
||||||
|
NSMutableArray<id<RCTNetworkingResponseHandler>> *_responseHandlers;
|
||||||
}
|
}
|
||||||
|
|
||||||
@synthesize methodQueue = _methodQueue;
|
@synthesize methodQueue = _methodQueue;
|
||||||
|
|
||||||
RCT_EXPORT_MODULE()
|
RCT_EXPORT_MODULE()
|
||||||
|
|
||||||
|
- (void)invalidate
|
||||||
|
{
|
||||||
|
_requestHandlers = nil;
|
||||||
|
_responseHandlers = nil;
|
||||||
|
}
|
||||||
|
|
||||||
- (NSArray<NSString *> *)supportedEvents
|
- (NSArray<NSString *> *)supportedEvents
|
||||||
{
|
{
|
||||||
return @[@"didCompleteNetworkResponse",
|
return @[@"didCompleteNetworkResponse",
|
||||||
|
@ -297,6 +305,8 @@ RCT_EXPORT_MODULE()
|
||||||
*
|
*
|
||||||
* - {"formData": [...]}: list of data payloads that will be combined into a multipart/form-data request
|
* - {"formData": [...]}: list of data payloads that will be combined into a multipart/form-data request
|
||||||
*
|
*
|
||||||
|
* - {"blob": {...}}: an object representing a blob
|
||||||
|
*
|
||||||
* If successful, the callback be called with a result dictionary containing the following (optional) keys:
|
* If successful, the callback be called with a result dictionary containing the following (optional) keys:
|
||||||
*
|
*
|
||||||
* - @"body" (NSData): the body of the request
|
* - @"body" (NSData): the body of the request
|
||||||
|
@ -312,6 +322,15 @@ RCT_EXPORT_MODULE()
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return callback(nil, nil);
|
return callback(nil, nil);
|
||||||
}
|
}
|
||||||
|
for (id<RCTNetworkingRequestHandler> handler in _requestHandlers) {
|
||||||
|
if ([handler canHandleNetworkingRequest:query]) {
|
||||||
|
// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
|
||||||
|
NSDictionary *body = [handler handleNetworkingRequest:query];
|
||||||
|
if (body) {
|
||||||
|
return callback(nil, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
NSData *body = [RCTConvert NSData:query[@"string"]];
|
NSData *body = [RCTConvert NSData:query[@"string"]];
|
||||||
if (body) {
|
if (body) {
|
||||||
return callback(nil, @{@"body": body});
|
return callback(nil, @{@"body": body});
|
||||||
|
@ -417,6 +436,7 @@ RCT_EXPORT_MODULE()
|
||||||
|
|
||||||
- (void)sendData:(NSData *)data
|
- (void)sendData:(NSData *)data
|
||||||
responseType:(NSString *)responseType
|
responseType:(NSString *)responseType
|
||||||
|
response:(NSURLResponse *)response
|
||||||
forTask:(RCTNetworkTask *)task
|
forTask:(RCTNetworkTask *)task
|
||||||
{
|
{
|
||||||
RCTAssertThread(_methodQueue, @"sendData: must be called on method queue");
|
RCTAssertThread(_methodQueue, @"sendData: must be called on method queue");
|
||||||
|
@ -425,23 +445,31 @@ RCT_EXPORT_MODULE()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString *responseString;
|
id responseData = nil;
|
||||||
if ([responseType isEqualToString:@"text"]) {
|
for (id<RCTNetworkingResponseHandler> handler in _responseHandlers) {
|
||||||
// No carry storage is required here because the entire data has been loaded.
|
if ([handler canHandleNetworkingResponse:responseType]) {
|
||||||
responseString = [RCTNetworking decodeTextData:data fromResponse:task.response withCarryData:nil];
|
responseData = [handler handleNetworkingResponse:response data:data];
|
||||||
if (!responseString) {
|
break;
|
||||||
RCTLogWarn(@"Received data was not a string, or was not a recognised encoding.");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} else if ([responseType isEqualToString:@"base64"]) {
|
|
||||||
responseString = [data base64EncodedStringWithOptions:0];
|
|
||||||
} else {
|
|
||||||
RCTLogWarn(@"Invalid responseType: %@", responseType);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NSArray<id> *responseJSON = @[task.requestID, responseString];
|
if (!responseData) {
|
||||||
[self sendEventWithName:@"didReceiveNetworkData" body:responseJSON];
|
if ([responseType isEqualToString:@"text"]) {
|
||||||
|
// No carry storage is required here because the entire data has been loaded.
|
||||||
|
responseData = [RCTNetworking decodeTextData:data fromResponse:task.response withCarryData:nil];
|
||||||
|
if (!responseData) {
|
||||||
|
RCTLogWarn(@"Received data was not a string, or was not a recognised encoding.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if ([responseType isEqualToString:@"base64"]) {
|
||||||
|
responseData = [data base64EncodedStringWithOptions:0];
|
||||||
|
} else {
|
||||||
|
RCTLogWarn(@"Invalid responseType: %@", responseType);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[self sendEventWithName:@"didReceiveNetworkData" body:@[task.requestID, responseData]];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)sendRequest:(NSURLRequest *)request
|
- (void)sendRequest:(NSURLRequest *)request
|
||||||
|
@ -523,7 +551,10 @@ RCT_EXPORT_MODULE()
|
||||||
// Unless we were sending incremental (text) chunks to JS, all along, now
|
// Unless we were sending incremental (text) chunks to JS, all along, now
|
||||||
// is the time to send the request body to JS.
|
// is the time to send the request body to JS.
|
||||||
if (!(incrementalUpdates && [responseType isEqualToString:@"text"])) {
|
if (!(incrementalUpdates && [responseType isEqualToString:@"text"])) {
|
||||||
[strongSelf sendData:data responseType:responseType forTask:task];
|
[strongSelf sendData:data
|
||||||
|
responseType:responseType
|
||||||
|
response:response
|
||||||
|
forTask:task];
|
||||||
}
|
}
|
||||||
NSArray *responseJSON = @[task.requestID,
|
NSArray *responseJSON = @[task.requestID,
|
||||||
RCTNullIfNil(error.localizedDescription),
|
RCTNullIfNil(error.localizedDescription),
|
||||||
|
@ -553,6 +584,32 @@ RCT_EXPORT_MODULE()
|
||||||
|
|
||||||
#pragma mark - Public API
|
#pragma mark - Public API
|
||||||
|
|
||||||
|
- (void)addRequestHandler:(id<RCTNetworkingRequestHandler>)handler
|
||||||
|
{
|
||||||
|
if (!_requestHandlers) {
|
||||||
|
_requestHandlers = [NSMutableArray new];
|
||||||
|
}
|
||||||
|
[_requestHandlers addObject:handler];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)addResponseHandler:(id<RCTNetworkingResponseHandler>)handler
|
||||||
|
{
|
||||||
|
if (!_responseHandlers) {
|
||||||
|
_responseHandlers = [NSMutableArray new];
|
||||||
|
}
|
||||||
|
[_responseHandlers addObject:handler];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)removeRequestHandler:(id<RCTNetworkingRequestHandler>)handler
|
||||||
|
{
|
||||||
|
[_requestHandlers removeObject:handler];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)removeResponseHandler:(id<RCTNetworkingResponseHandler>)handler
|
||||||
|
{
|
||||||
|
[_responseHandlers removeObject:handler];
|
||||||
|
}
|
||||||
|
|
||||||
- (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request completionBlock:(RCTURLRequestCompletionBlock)completionBlock
|
- (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request completionBlock:(RCTURLRequestCompletionBlock)completionBlock
|
||||||
{
|
{
|
||||||
id<RCTURLRequestHandler> handler = [self handlerForRequest:request];
|
id<RCTURLRequestHandler> handler = [self handlerForRequest:request];
|
||||||
|
|
|
@ -23,9 +23,11 @@ const invariant = require('fbjs/lib/invariant');
|
||||||
* found when Flow v0.54 was deployed. To see the error delete this comment and
|
* found when Flow v0.54 was deployed. To see the error delete this comment and
|
||||||
* run Flow. */
|
* run Flow. */
|
||||||
const warning = require('fbjs/lib/warning');
|
const warning = require('fbjs/lib/warning');
|
||||||
|
const BlobManager = require('BlobManager');
|
||||||
|
|
||||||
type ResponseType = '' | 'arraybuffer' | 'blob' | 'document' | 'json' | 'text';
|
export type NativeResponseType = 'base64' | 'blob' | 'text';
|
||||||
type Response = ?Object | string;
|
export type ResponseType = '' | 'arraybuffer' | 'blob' | 'document' | 'json' | 'text';
|
||||||
|
export type Response = ?Object | string;
|
||||||
|
|
||||||
type XHRInterceptor = {
|
type XHRInterceptor = {
|
||||||
requestSent(
|
requestSent(
|
||||||
|
@ -54,6 +56,11 @@ type XHRInterceptor = {
|
||||||
): void,
|
): void,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The native blob module is optional so inject it here if available.
|
||||||
|
if (BlobManager.isAvailable) {
|
||||||
|
BlobManager.addNetworkingHandler();
|
||||||
|
}
|
||||||
|
|
||||||
const UNSENT = 0;
|
const UNSENT = 0;
|
||||||
const OPENED = 1;
|
const OPENED = 1;
|
||||||
const HEADERS_RECEIVED = 2;
|
const HEADERS_RECEIVED = 2;
|
||||||
|
@ -200,6 +207,10 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
||||||
SUPPORTED_RESPONSE_TYPES[responseType] || responseType === 'document',
|
SUPPORTED_RESPONSE_TYPES[responseType] || responseType === 'document',
|
||||||
`The provided value '${responseType}' is unsupported in this environment.`
|
`The provided value '${responseType}' is unsupported in this environment.`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (responseType === 'blob') {
|
||||||
|
invariant(BlobManager.isAvailable, 'Native module BlobModule is required for blob support');
|
||||||
|
}
|
||||||
this._responseType = responseType;
|
this._responseType = responseType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,10 +253,11 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'blob':
|
case 'blob':
|
||||||
this._cachedResponse = new global.Blob(
|
if (typeof this._response === 'object' && this._response) {
|
||||||
[base64.toByteArray(this._response).buffer],
|
this._cachedResponse = BlobManager.createFromOptions(this._response);
|
||||||
{type: this.getResponseHeader('content-type') || ''}
|
} else {
|
||||||
);
|
throw new Error(`Invalid response for blob: ${this._response}`);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'json':
|
case 'json':
|
||||||
|
@ -493,10 +505,13 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
||||||
(args) => this.__didCompleteResponse(...args)
|
(args) => this.__didCompleteResponse(...args)
|
||||||
));
|
));
|
||||||
|
|
||||||
let nativeResponseType = 'text';
|
let nativeResponseType: NativeResponseType = 'text';
|
||||||
if (this._responseType === 'arraybuffer' || this._responseType === 'blob') {
|
if (this._responseType === 'arraybuffer') {
|
||||||
nativeResponseType = 'base64';
|
nativeResponseType = 'base64';
|
||||||
}
|
}
|
||||||
|
if (this._responseType === 'blob') {
|
||||||
|
nativeResponseType = 'blob';
|
||||||
|
}
|
||||||
|
|
||||||
invariant(this._method, 'Request method needs to be defined.');
|
invariant(this._method, 'Request method needs to be defined.');
|
||||||
invariant(this._url, 'Request URL needs to be defined.');
|
invariant(this._url, 'Request URL needs to be defined.');
|
||||||
|
|
|
@ -8,25 +8,30 @@
|
||||||
*
|
*
|
||||||
* @providesModule convertRequestBody
|
* @providesModule convertRequestBody
|
||||||
* @flow
|
* @flow
|
||||||
|
* @format
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const binaryToBase64 = require('binaryToBase64');
|
const binaryToBase64 = require('binaryToBase64');
|
||||||
|
|
||||||
|
const Blob = require('Blob');
|
||||||
const FormData = require('FormData');
|
const FormData = require('FormData');
|
||||||
|
|
||||||
export type RequestBody =
|
export type RequestBody =
|
||||||
string
|
| string
|
||||||
|
| Blob
|
||||||
| FormData
|
| FormData
|
||||||
| {uri: string}
|
| {uri: string}
|
||||||
| ArrayBuffer
|
| ArrayBuffer
|
||||||
| $ArrayBufferView
|
| $ArrayBufferView;
|
||||||
;
|
|
||||||
|
|
||||||
function convertRequestBody(body: RequestBody): Object {
|
function convertRequestBody(body: RequestBody): Object {
|
||||||
if (typeof body === 'string') {
|
if (typeof body === 'string') {
|
||||||
return {string: body};
|
return {string: body};
|
||||||
}
|
}
|
||||||
|
if (body instanceof Blob) {
|
||||||
|
return {blob: body.data};
|
||||||
|
}
|
||||||
if (body instanceof FormData) {
|
if (body instanceof FormData) {
|
||||||
return {formData: body.getParts()};
|
return {formData: body.getParts()};
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@protocol RCTWebSocketContentHandler <NSObject>
|
@protocol RCTWebSocketContentHandler <NSObject>
|
||||||
|
|
||||||
- (id)processMessage:(id __nullable)message forSocketID:(NSNumber *)socketID
|
- (id)processWebsocketMessage:(id __nullable)message
|
||||||
withType:(NSString *__nonnull __autoreleasing *__nonnull)type;
|
forSocketID:(NSNumber *)socketID
|
||||||
|
withType:(NSString *__nonnull __autoreleasing *__nonnull)type;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
@implementation RCTWebSocketModule
|
@implementation RCTWebSocketModule
|
||||||
{
|
{
|
||||||
NSMutableDictionary<NSNumber *, RCTSRWebSocket *> *_sockets;
|
NSMutableDictionary<NSNumber *, RCTSRWebSocket *> *_sockets;
|
||||||
NSMutableDictionary<NSNumber *, id> *_contentHandlers;
|
NSMutableDictionary<NSNumber *, id<RCTWebSocketContentHandler>> *_contentHandlers;
|
||||||
}
|
}
|
||||||
|
|
||||||
RCT_EXPORT_MODULE()
|
RCT_EXPORT_MODULE()
|
||||||
|
@ -53,8 +53,9 @@ RCT_EXPORT_MODULE()
|
||||||
@"websocketClosed"];
|
@"websocketClosed"];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc
|
- (void)invalidate
|
||||||
{
|
{
|
||||||
|
_contentHandlers = nil;
|
||||||
for (RCTSRWebSocket *socket in _sockets.allValues) {
|
for (RCTSRWebSocket *socket in _sockets.allValues) {
|
||||||
socket.delegate = nil;
|
socket.delegate = nil;
|
||||||
[socket close];
|
[socket close];
|
||||||
|
@ -135,7 +136,7 @@ RCT_EXPORT_METHOD(close:(nonnull NSNumber *)socketID)
|
||||||
NSNumber *socketID = [webSocket reactTag];
|
NSNumber *socketID = [webSocket reactTag];
|
||||||
id contentHandler = _contentHandlers[socketID];
|
id contentHandler = _contentHandlers[socketID];
|
||||||
if (contentHandler) {
|
if (contentHandler) {
|
||||||
message = [contentHandler processMessage:message forSocketID:socketID withType:&type];
|
message = [contentHandler processWebsocketMessage:message forSocketID:socketID withType:&type];
|
||||||
} else {
|
} else {
|
||||||
if ([message isKindOfClass:[NSData class]]) {
|
if ([message isKindOfClass:[NSData class]]) {
|
||||||
type = @"binary";
|
type = @"binary";
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
const Blob = require('Blob');
|
const Blob = require('Blob');
|
||||||
const EventTarget = require('event-target-shim');
|
const EventTarget = require('event-target-shim');
|
||||||
const NativeEventEmitter = require('NativeEventEmitter');
|
const NativeEventEmitter = require('NativeEventEmitter');
|
||||||
|
const BlobManager = require('BlobManager');
|
||||||
const NativeModules = require('NativeModules');
|
const NativeModules = require('NativeModules');
|
||||||
const Platform = require('Platform');
|
const Platform = require('Platform');
|
||||||
const WebSocketEvent = require('WebSocketEvent');
|
const WebSocketEvent = require('WebSocketEvent');
|
||||||
|
@ -147,19 +148,20 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
|
||||||
throw new Error('binaryType must be either \'blob\' or \'arraybuffer\'');
|
throw new Error('binaryType must be either \'blob\' or \'arraybuffer\'');
|
||||||
}
|
}
|
||||||
if (this._binaryType === 'blob' || binaryType === 'blob') {
|
if (this._binaryType === 'blob' || binaryType === 'blob') {
|
||||||
const BlobModule = NativeModules.BlobModule;
|
invariant(BlobManager.isAvailable, 'Native module BlobModule is required for blob support');
|
||||||
invariant(BlobModule, 'Native module BlobModule is required for blob support');
|
if (binaryType === 'blob') {
|
||||||
if (BlobModule) {
|
BlobManager.addWebSocketHandler(this._socketId);
|
||||||
if (binaryType === 'blob') {
|
} else {
|
||||||
BlobModule.enableBlobSupport(this._socketId);
|
BlobManager.removeWebSocketHandler(this._socketId);
|
||||||
} else {
|
|
||||||
BlobModule.disableBlobSupport(this._socketId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._binaryType = binaryType;
|
this._binaryType = binaryType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get binaryType(): ?BinaryType {
|
||||||
|
return this._binaryType;
|
||||||
|
}
|
||||||
|
|
||||||
close(code?: number, reason?: string): void {
|
close(code?: number, reason?: string): void {
|
||||||
if (this.readyState === this.CLOSING ||
|
if (this.readyState === this.CLOSING ||
|
||||||
this.readyState === this.CLOSED) {
|
this.readyState === this.CLOSED) {
|
||||||
|
@ -176,9 +178,8 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data instanceof Blob) {
|
if (data instanceof Blob) {
|
||||||
const BlobModule = NativeModules.BlobModule;
|
invariant(BlobManager.isAvailable, 'Native module BlobModule is required for blob support');
|
||||||
invariant(BlobModule, 'Native module BlobModule is required for blob support');
|
BlobManager.sendOverSocket(data, this._socketId);
|
||||||
BlobModule.sendBlob(data, this._socketId);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,6 +213,10 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
|
||||||
} else {
|
} else {
|
||||||
WebSocketModule.close(this._socketId);
|
WebSocketModule.close(this._socketId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (BlobManager.isAvailable && this._binaryType === 'blob') {
|
||||||
|
BlobManager.removeWebSocketHandler(this._socketId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_unregisterEvents(): void {
|
_unregisterEvents(): void {
|
||||||
|
@ -231,7 +236,7 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
|
||||||
data = base64.toByteArray(ev.data).buffer;
|
data = base64.toByteArray(ev.data).buffer;
|
||||||
break;
|
break;
|
||||||
case 'blob':
|
case 'blob':
|
||||||
data = Blob.create(ev.data);
|
data = BlobManager.createFromOptions(ev.data);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
this.dispatchEvent(new WebSocketEvent('message', { data }));
|
this.dispatchEvent(new WebSocketEvent('message', { data }));
|
||||||
|
|
|
@ -53,6 +53,8 @@
|
||||||
192F69B91E82409A008692C7 /* RCTConvert_YGValueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 192F69B61E82409A008692C7 /* RCTConvert_YGValueTests.m */; };
|
192F69B91E82409A008692C7 /* RCTConvert_YGValueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 192F69B61E82409A008692C7 /* RCTConvert_YGValueTests.m */; };
|
||||||
192F69BA1E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 192F69B71E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m */; };
|
192F69BA1E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 192F69B71E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m */; };
|
||||||
192F69DA1E8240E2008692C7 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13E501A31D07A502005F35D8 /* libRCTAnimation.a */; };
|
192F69DA1E8240E2008692C7 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13E501A31D07A502005F35D8 /* libRCTAnimation.a */; };
|
||||||
|
19BA88D51F84344F00741C5A /* RCTBlobManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 19BA88D41F84344F00741C5A /* RCTBlobManagerTests.m */; };
|
||||||
|
19BA89031F8439A700741C5A /* libRCTBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5281CA511EEAC9A700AC40CD /* libRCTBlob.a */; };
|
||||||
272E6B3F1BEA849E001FCF37 /* UpdatePropertiesExampleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 272E6B3C1BEA849E001FCF37 /* UpdatePropertiesExampleView.m */; };
|
272E6B3F1BEA849E001FCF37 /* UpdatePropertiesExampleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 272E6B3C1BEA849E001FCF37 /* UpdatePropertiesExampleView.m */; };
|
||||||
27B885561BED29AF00008352 /* RCTRootViewIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 27B885551BED29AF00008352 /* RCTRootViewIntegrationTests.m */; };
|
27B885561BED29AF00008352 /* RCTRootViewIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 27B885551BED29AF00008352 /* RCTRootViewIntegrationTests.m */; };
|
||||||
27F441EC1BEBE5030039B79C /* FlexibleSizeExampleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F441E81BEBE5030039B79C /* FlexibleSizeExampleView.m */; };
|
27F441EC1BEBE5030039B79C /* FlexibleSizeExampleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F441E81BEBE5030039B79C /* FlexibleSizeExampleView.m */; };
|
||||||
|
@ -515,6 +517,7 @@
|
||||||
192F69B51E82409A008692C7 /* RCTAnimationUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAnimationUtilsTests.m; sourceTree = "<group>"; };
|
192F69B51E82409A008692C7 /* RCTAnimationUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAnimationUtilsTests.m; sourceTree = "<group>"; };
|
||||||
192F69B61E82409A008692C7 /* RCTConvert_YGValueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert_YGValueTests.m; sourceTree = "<group>"; };
|
192F69B61E82409A008692C7 /* RCTConvert_YGValueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert_YGValueTests.m; sourceTree = "<group>"; };
|
||||||
192F69B71E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNativeAnimatedNodesManagerTests.m; sourceTree = "<group>"; };
|
192F69B71E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNativeAnimatedNodesManagerTests.m; sourceTree = "<group>"; };
|
||||||
|
19BA88D41F84344F00741C5A /* RCTBlobManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTBlobManagerTests.m; sourceTree = "<group>"; };
|
||||||
272E6B3B1BEA849E001FCF37 /* UpdatePropertiesExampleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = UpdatePropertiesExampleView.h; path = RNTester/NativeExampleViews/UpdatePropertiesExampleView.h; sourceTree = "<group>"; };
|
272E6B3B1BEA849E001FCF37 /* UpdatePropertiesExampleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = UpdatePropertiesExampleView.h; path = RNTester/NativeExampleViews/UpdatePropertiesExampleView.h; sourceTree = "<group>"; };
|
||||||
272E6B3C1BEA849E001FCF37 /* UpdatePropertiesExampleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = UpdatePropertiesExampleView.m; path = RNTester/NativeExampleViews/UpdatePropertiesExampleView.m; sourceTree = "<group>"; };
|
272E6B3C1BEA849E001FCF37 /* UpdatePropertiesExampleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = UpdatePropertiesExampleView.m; path = RNTester/NativeExampleViews/UpdatePropertiesExampleView.m; sourceTree = "<group>"; };
|
||||||
27B885551BED29AF00008352 /* RCTRootViewIntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootViewIntegrationTests.m; sourceTree = "<group>"; };
|
27B885551BED29AF00008352 /* RCTRootViewIntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootViewIntegrationTests.m; sourceTree = "<group>"; };
|
||||||
|
@ -553,6 +556,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
19BA89031F8439A700741C5A /* libRCTBlob.a in Frameworks */,
|
||||||
192F69DA1E8240E2008692C7 /* libRCTAnimation.a in Frameworks */,
|
192F69DA1E8240E2008692C7 /* libRCTAnimation.a in Frameworks */,
|
||||||
14D6D71E1B2222EF001FB087 /* libRCTActionSheet.a in Frameworks */,
|
14D6D71E1B2222EF001FB087 /* libRCTActionSheet.a in Frameworks */,
|
||||||
14D6D7201B2222EF001FB087 /* libRCTGeolocation.a in Frameworks */,
|
14D6D7201B2222EF001FB087 /* libRCTGeolocation.a in Frameworks */,
|
||||||
|
@ -767,6 +771,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
192F69B51E82409A008692C7 /* RCTAnimationUtilsTests.m */,
|
192F69B51E82409A008692C7 /* RCTAnimationUtilsTests.m */,
|
||||||
|
19BA88D41F84344F00741C5A /* RCTBlobManagerTests.m */,
|
||||||
192F69B61E82409A008692C7 /* RCTConvert_YGValueTests.m */,
|
192F69B61E82409A008692C7 /* RCTConvert_YGValueTests.m */,
|
||||||
192F69B71E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m */,
|
192F69B71E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m */,
|
||||||
13B6C1A21C34225900D3FAF5 /* RCTURLUtilsTests.m */,
|
13B6C1A21C34225900D3FAF5 /* RCTURLUtilsTests.m */,
|
||||||
|
@ -884,6 +889,13 @@
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
19BA89021F8439A700741C5A /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
272E6B3A1BEA846C001FCF37 /* NativeExampleViews */ = {
|
272E6B3A1BEA846C001FCF37 /* NativeExampleViews */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1683,6 +1695,7 @@
|
||||||
8385CF041B87479200C6273E /* RCTImageLoaderHelpers.m in Sources */,
|
8385CF041B87479200C6273E /* RCTImageLoaderHelpers.m in Sources */,
|
||||||
192F69B91E82409A008692C7 /* RCTConvert_YGValueTests.m in Sources */,
|
192F69B91E82409A008692C7 /* RCTConvert_YGValueTests.m in Sources */,
|
||||||
BC9C03401DC9F1D600B1C635 /* RCTDevMenuTests.m in Sources */,
|
BC9C03401DC9F1D600B1C635 /* RCTDevMenuTests.m in Sources */,
|
||||||
|
19BA88D51F84344F00741C5A /* RCTBlobManagerTests.m in Sources */,
|
||||||
68FF44381CF6111500720EFD /* RCTBundleURLProviderTests.m in Sources */,
|
68FF44381CF6111500720EFD /* RCTBundleURLProviderTests.m in Sources */,
|
||||||
8385CEF51B873B5C00C6273E /* RCTImageLoaderTests.m in Sources */,
|
8385CEF51B873B5C00C6273E /* RCTImageLoaderTests.m in Sources */,
|
||||||
);
|
);
|
||||||
|
|
|
@ -97,6 +97,7 @@
|
||||||
2DD323E71DA2DE3F000FE1B8 /* libRCTSettings-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DD323C81DA2DD8B000FE1B8 /* libRCTSettings-tvOS.a */; };
|
2DD323E71DA2DE3F000FE1B8 /* libRCTSettings-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DD323C81DA2DD8B000FE1B8 /* libRCTSettings-tvOS.a */; };
|
||||||
2DD323E81DA2DE3F000FE1B8 /* libRCTText-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DD323D01DA2DD8B000FE1B8 /* libRCTText-tvOS.a */; };
|
2DD323E81DA2DE3F000FE1B8 /* libRCTText-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DD323D01DA2DD8B000FE1B8 /* libRCTText-tvOS.a */; };
|
||||||
2DD323E91DA2DE3F000FE1B8 /* libRCTWebSocket-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DD323D51DA2DD8B000FE1B8 /* libRCTWebSocket-tvOS.a */; };
|
2DD323E91DA2DE3F000FE1B8 /* libRCTWebSocket-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DD323D51DA2DD8B000FE1B8 /* libRCTWebSocket-tvOS.a */; };
|
||||||
|
2DD323EA1DA2DE3F000FE1B8 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2DD323D91DA2DD8B000FE1B8 /* libReact.a */; };
|
||||||
3578590A1B28D2CF00341EDB /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 357859011B28D2C500341EDB /* libRCTLinking.a */; };
|
3578590A1B28D2CF00341EDB /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 357859011B28D2C500341EDB /* libRCTLinking.a */; };
|
||||||
39AA31A41DC1DFDC000F7EBB /* RCTUnicodeDecodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 39AA31A31DC1DFDC000F7EBB /* RCTUnicodeDecodeTests.m */; };
|
39AA31A41DC1DFDC000F7EBB /* RCTUnicodeDecodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 39AA31A31DC1DFDC000F7EBB /* RCTUnicodeDecodeTests.m */; };
|
||||||
3D05746D1DE6008900184BB4 /* libRCTPushNotification-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D05746C1DE6008900184BB4 /* libRCTPushNotification-tvOS.a */; };
|
3D05746D1DE6008900184BB4 /* libRCTPushNotification-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D05746C1DE6008900184BB4 /* libRCTPushNotification-tvOS.a */; };
|
||||||
|
@ -117,6 +118,9 @@
|
||||||
83636F8F1B53F22C009F943E /* RCTUIManagerScenarioTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 83636F8E1B53F22C009F943E /* RCTUIManagerScenarioTests.m */; };
|
83636F8F1B53F22C009F943E /* RCTUIManagerScenarioTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 83636F8E1B53F22C009F943E /* RCTUIManagerScenarioTests.m */; };
|
||||||
8385CEF51B873B5C00C6273E /* RCTImageLoaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8385CEF41B873B5C00C6273E /* RCTImageLoaderTests.m */; };
|
8385CEF51B873B5C00C6273E /* RCTImageLoaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8385CEF41B873B5C00C6273E /* RCTImageLoaderTests.m */; };
|
||||||
8385CF041B87479200C6273E /* RCTImageLoaderHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 8385CF031B87479200C6273E /* RCTImageLoaderHelpers.m */; };
|
8385CF041B87479200C6273E /* RCTImageLoaderHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 8385CF031B87479200C6273E /* RCTImageLoaderHelpers.m */; };
|
||||||
|
ADAC7A091E093BB900D77272 /* libRCTBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADBDB9F11DFEC24500ED6528 /* libRCTBlob.a */; };
|
||||||
|
ADBDBA0D1DFEC24D00ED6528 /* libRCTBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADBDB9F11DFEC24500ED6528 /* libRCTBlob.a */; };
|
||||||
|
ADD01A631E093FA900F6D226 /* libRCTBlob-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADD01A471E093FA100F6D226 /* libRCTBlob-tvOS.a */; };
|
||||||
BC9C03401DC9F1D600B1C635 /* RCTDevMenuTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BC9C033F1DC9F1D600B1C635 /* RCTDevMenuTests.m */; };
|
BC9C03401DC9F1D600B1C635 /* RCTDevMenuTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BC9C033F1DC9F1D600B1C635 /* RCTDevMenuTests.m */; };
|
||||||
C654F14C1EB34D0C000B7A9A /* RNTesterTestModule.m in Sources */ = {isa = PBXBuildFile; fileRef = C654F14B1EB34D0C000B7A9A /* RNTesterTestModule.m */; };
|
C654F14C1EB34D0C000B7A9A /* RNTesterTestModule.m in Sources */ = {isa = PBXBuildFile; fileRef = C654F14B1EB34D0C000B7A9A /* RNTesterTestModule.m */; };
|
||||||
C654F16E1EB34D14000B7A9A /* RNTesterTestModule.m in Sources */ = {isa = PBXBuildFile; fileRef = C654F14B1EB34D0C000B7A9A /* RNTesterTestModule.m */; };
|
C654F16E1EB34D14000B7A9A /* RNTesterTestModule.m in Sources */ = {isa = PBXBuildFile; fileRef = C654F14B1EB34D0C000B7A9A /* RNTesterTestModule.m */; };
|
||||||
|
@ -383,6 +387,20 @@
|
||||||
remoteGlobalIDString = 134814201AA4EA6300B7C361;
|
remoteGlobalIDString = 134814201AA4EA6300B7C361;
|
||||||
remoteInfo = RCTSettings;
|
remoteInfo = RCTSettings;
|
||||||
};
|
};
|
||||||
|
ADBDB9F01DFEC24500ED6528 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = ADBDB9E81DFEC24500ED6528 /* RCTBlob.xcodeproj */;
|
||||||
|
proxyType = 2;
|
||||||
|
remoteGlobalIDString = 358F4ED71D1E81A9004DF814;
|
||||||
|
remoteInfo = RCTBlob;
|
||||||
|
};
|
||||||
|
ADD01A461E093FA100F6D226 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = ADBDB9E81DFEC24500ED6528 /* RCTBlob.xcodeproj */;
|
||||||
|
proxyType = 2;
|
||||||
|
remoteGlobalIDString = ADAC7A2E1E093EF800D77272;
|
||||||
|
remoteInfo = "RCTBlob-tvOS";
|
||||||
|
};
|
||||||
D85B829B1AB6D5CE003F4FE2 /* PBXContainerItemProxy */ = {
|
D85B829B1AB6D5CE003F4FE2 /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */;
|
containerPortal = D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */;
|
||||||
|
@ -470,6 +488,7 @@
|
||||||
8385CEF41B873B5C00C6273E /* RCTImageLoaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoaderTests.m; sourceTree = "<group>"; };
|
8385CEF41B873B5C00C6273E /* RCTImageLoaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoaderTests.m; sourceTree = "<group>"; };
|
||||||
8385CF031B87479200C6273E /* RCTImageLoaderHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoaderHelpers.m; sourceTree = "<group>"; };
|
8385CF031B87479200C6273E /* RCTImageLoaderHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoaderHelpers.m; sourceTree = "<group>"; };
|
||||||
8385CF051B8747A000C6273E /* RCTImageLoaderHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTImageLoaderHelpers.h; sourceTree = "<group>"; };
|
8385CF051B8747A000C6273E /* RCTImageLoaderHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTImageLoaderHelpers.h; sourceTree = "<group>"; };
|
||||||
|
ADBDB9E81DFEC24500ED6528 /* RCTBlob.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTBlob.xcodeproj; path = ../../Libraries/Blob/RCTBlob.xcodeproj; sourceTree = "<group>"; };
|
||||||
BC9C033F1DC9F1D600B1C635 /* RCTDevMenuTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDevMenuTests.m; sourceTree = "<group>"; };
|
BC9C033F1DC9F1D600B1C635 /* RCTDevMenuTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDevMenuTests.m; sourceTree = "<group>"; };
|
||||||
C654F14B1EB34D0C000B7A9A /* RNTesterTestModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNTesterTestModule.m; sourceTree = "<group>"; };
|
C654F14B1EB34D0C000B7A9A /* RNTesterTestModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNTesterTestModule.m; sourceTree = "<group>"; };
|
||||||
D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = ../Libraries/Vibration/RCTVibration.xcodeproj; sourceTree = "<group>"; };
|
D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = ../Libraries/Vibration/RCTVibration.xcodeproj; sourceTree = "<group>"; };
|
||||||
|
@ -544,6 +563,7 @@
|
||||||
2DD323E61DA2DE3F000FE1B8 /* libRCTNetwork-tvOS.a in Frameworks */,
|
2DD323E61DA2DE3F000FE1B8 /* libRCTNetwork-tvOS.a in Frameworks */,
|
||||||
3D05746D1DE6008900184BB4 /* libRCTPushNotification-tvOS.a in Frameworks */,
|
3D05746D1DE6008900184BB4 /* libRCTPushNotification-tvOS.a in Frameworks */,
|
||||||
2DD323E71DA2DE3F000FE1B8 /* libRCTSettings-tvOS.a in Frameworks */,
|
2DD323E71DA2DE3F000FE1B8 /* libRCTSettings-tvOS.a in Frameworks */,
|
||||||
|
ADD01A631E093FA900F6D226 /* libRCTBlob-tvOS.a in Frameworks */,
|
||||||
2DD323E81DA2DE3F000FE1B8 /* libRCTText-tvOS.a in Frameworks */,
|
2DD323E81DA2DE3F000FE1B8 /* libRCTText-tvOS.a in Frameworks */,
|
||||||
2DD323E91DA2DE3F000FE1B8 /* libRCTWebSocket-tvOS.a in Frameworks */,
|
2DD323E91DA2DE3F000FE1B8 /* libRCTWebSocket-tvOS.a in Frameworks */,
|
||||||
);
|
);
|
||||||
|
@ -755,6 +775,21 @@
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
14AADF001AC3DB95002390C9 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
14AADF041AC3DB95002390C9 /* libReact.a */,
|
||||||
|
2DD323D91DA2DD8B000FE1B8 /* libReact.a */,
|
||||||
|
3D3C08811DE3424E00C268FA /* libyoga.a */,
|
||||||
|
3D3C08831DE3424E00C268FA /* libyoga.a */,
|
||||||
|
3D05748C1DE6008900184BB4 /* libcxxreact.a */,
|
||||||
|
3D05748E1DE6008900184BB4 /* libcxxreact.a */,
|
||||||
|
3D0574901DE6008900184BB4 /* libjschelpers.a */,
|
||||||
|
3D0574921DE6008900184BB4 /* libjschelpers.a */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
14D6D6EA1B2205C0001FB087 /* OCMock */ = {
|
14D6D6EA1B2205C0001FB087 /* OCMock */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -892,6 +927,15 @@
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
ADBDB9E91DFEC24500ED6528 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ADBDB9F11DFEC24500ED6528 /* libRCTBlob.a */,
|
||||||
|
ADD01A471E093FA100F6D226 /* libRCTBlob-tvOS.a */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D85B82921AB6D5CE003F4FE2 /* Products */ = {
|
D85B82921AB6D5CE003F4FE2 /* Products */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1082,6 +1126,10 @@
|
||||||
ProductGroup = 13E5019D1D07A502005F35D8 /* Products */;
|
ProductGroup = 13E5019D1D07A502005F35D8 /* Products */;
|
||||||
ProjectRef = 13E5019C1D07A502005F35D8 /* RCTAnimation.xcodeproj */;
|
ProjectRef = 13E5019C1D07A502005F35D8 /* RCTAnimation.xcodeproj */;
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ProductGroup = ADBDB9E91DFEC24500ED6528 /* Products */;
|
||||||
|
ProjectRef = ADBDB9E81DFEC24500ED6528 /* RCTBlob.xcodeproj */;
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ProductGroup = 138DEE031B9EDDDB007F4EA5 /* Products */;
|
ProductGroup = 138DEE031B9EDDDB007F4EA5 /* Products */;
|
||||||
ProjectRef = 138DEE021B9EDDDB007F4EA5 /* RCTCameraRoll.xcodeproj */;
|
ProjectRef = 138DEE021B9EDDDB007F4EA5 /* RCTCameraRoll.xcodeproj */;
|
||||||
|
@ -1320,6 +1368,13 @@
|
||||||
remoteRef = 2DD323D41DA2DD8B000FE1B8 /* PBXContainerItemProxy */;
|
remoteRef = 2DD323D41DA2DD8B000FE1B8 /* PBXContainerItemProxy */;
|
||||||
sourceTree = BUILT_PRODUCTS_DIR;
|
sourceTree = BUILT_PRODUCTS_DIR;
|
||||||
};
|
};
|
||||||
|
2DD323D91DA2DD8B000FE1B8 /* libReact.a */ = {
|
||||||
|
isa = PBXReferenceProxy;
|
||||||
|
fileType = archive.ar;
|
||||||
|
path = libReact.a;
|
||||||
|
remoteRef = 2DD323D81DA2DD8B000FE1B8 /* PBXContainerItemProxy */;
|
||||||
|
sourceTree = BUILT_PRODUCTS_DIR;
|
||||||
|
};
|
||||||
357859011B28D2C500341EDB /* libRCTLinking.a */ = {
|
357859011B28D2C500341EDB /* libRCTLinking.a */ = {
|
||||||
isa = PBXReferenceProxy;
|
isa = PBXReferenceProxy;
|
||||||
fileType = archive.ar;
|
fileType = archive.ar;
|
||||||
|
@ -1376,6 +1431,20 @@
|
||||||
remoteRef = 834C36D11AF8DA610019C93C /* PBXContainerItemProxy */;
|
remoteRef = 834C36D11AF8DA610019C93C /* PBXContainerItemProxy */;
|
||||||
sourceTree = BUILT_PRODUCTS_DIR;
|
sourceTree = BUILT_PRODUCTS_DIR;
|
||||||
};
|
};
|
||||||
|
ADBDB9F11DFEC24500ED6528 /* libRCTBlob.a */ = {
|
||||||
|
isa = PBXReferenceProxy;
|
||||||
|
fileType = archive.ar;
|
||||||
|
path = libRCTBlob.a;
|
||||||
|
remoteRef = ADBDB9F01DFEC24500ED6528 /* PBXContainerItemProxy */;
|
||||||
|
sourceTree = BUILT_PRODUCTS_DIR;
|
||||||
|
};
|
||||||
|
ADD01A471E093FA100F6D226 /* libRCTBlob-tvOS.a */ = {
|
||||||
|
isa = PBXReferenceProxy;
|
||||||
|
fileType = archive.ar;
|
||||||
|
path = "libRCTBlob-tvOS.a";
|
||||||
|
remoteRef = ADD01A461E093FA100F6D226 /* PBXContainerItemProxy */;
|
||||||
|
sourceTree = BUILT_PRODUCTS_DIR;
|
||||||
|
};
|
||||||
D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */ = {
|
D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */ = {
|
||||||
isa = PBXReferenceProxy;
|
isa = PBXReferenceProxy;
|
||||||
fileType = archive.ar;
|
fileType = archive.ar;
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import <XCTest/XCTest.h>
|
||||||
|
|
||||||
|
#import <RCTBlob/RCTBlobManager.h>
|
||||||
|
|
||||||
|
@interface RCTBlobManagerTests : XCTestCase
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation RCTBlobManagerTests
|
||||||
|
{
|
||||||
|
RCTBlobManager *_module;
|
||||||
|
NSMutableData *_data;
|
||||||
|
NSString *_blobId;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setUp
|
||||||
|
{
|
||||||
|
[super setUp];
|
||||||
|
|
||||||
|
_module = [RCTBlobManager new];
|
||||||
|
[_module setValue:nil forKey:@"bridge"];
|
||||||
|
NSInteger size = 120;
|
||||||
|
_data = [NSMutableData dataWithCapacity:size];
|
||||||
|
for (NSInteger i = 0; i < size / 4; i++) {
|
||||||
|
uint32_t randomBits = arc4random();
|
||||||
|
[_data appendBytes:(void *)&randomBits length:4];
|
||||||
|
}
|
||||||
|
_blobId = [NSUUID UUID].UUIDString;
|
||||||
|
[_module store:_data withId:_blobId];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testResolve
|
||||||
|
{
|
||||||
|
XCTAssertTrue([_data isEqualToData:[_module resolve:_blobId offset:0 size:_data.length]]);
|
||||||
|
NSData *rangeData = [_data subdataWithRange:NSMakeRange(30, _data.length - 30)];
|
||||||
|
XCTAssertTrue([rangeData isEqualToData:[_module resolve:_blobId offset:30 size:_data.length - 30]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testResolveMap
|
||||||
|
{
|
||||||
|
NSDictionary<NSString *, id> *map = @{
|
||||||
|
@"blobId": _blobId,
|
||||||
|
@"size": @(_data.length),
|
||||||
|
@"offset": @0,
|
||||||
|
};
|
||||||
|
XCTAssertTrue([_data isEqualToData:[_module resolve:map]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testResolveURL
|
||||||
|
{
|
||||||
|
NSURLComponents *components = [NSURLComponents new];
|
||||||
|
[components setPath:_blobId];
|
||||||
|
[components setQuery:[NSString stringWithFormat:@"offset=0&size=%lu", (unsigned long)_data.length]];
|
||||||
|
XCTAssertTrue([_data isEqualToData:[_module resolveURL:[components URL]]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testRemove
|
||||||
|
{
|
||||||
|
XCTAssertNotNil([_module resolve:_blobId offset:0 size:_data.length]);
|
||||||
|
[_module remove:_blobId];
|
||||||
|
XCTAssertNil([_module resolve:_blobId offset:0 size:_data.length]);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)testCreateFromParts
|
||||||
|
{
|
||||||
|
NSDictionary<NSString *, id> *blobData = @{
|
||||||
|
@"blobId": _blobId,
|
||||||
|
@"offset": @0,
|
||||||
|
@"size": @(_data.length),
|
||||||
|
};
|
||||||
|
NSDictionary<NSString *, id> *blob = @{
|
||||||
|
@"data": blobData,
|
||||||
|
@"type": @"blob",
|
||||||
|
};
|
||||||
|
NSString *stringData = @"i \u2665 dogs";
|
||||||
|
NSDictionary<NSString *, id> *string = @{
|
||||||
|
@"data": stringData,
|
||||||
|
@"type": @"string",
|
||||||
|
};
|
||||||
|
NSString *resultId = [NSUUID UUID].UUIDString;
|
||||||
|
NSArray<id> *parts = @[blob, string];
|
||||||
|
|
||||||
|
[_module createFromParts:parts withId:resultId];
|
||||||
|
|
||||||
|
NSMutableData *expectedData = [NSMutableData new];
|
||||||
|
[expectedData appendData:_data];
|
||||||
|
[expectedData appendData:[stringData dataUsingEncoding:NSUTF8StringEncoding]];
|
||||||
|
|
||||||
|
NSData *result = [_module resolve:resultId offset:0 size:expectedData.length];
|
||||||
|
|
||||||
|
XCTAssertTrue([expectedData isEqualToData:result]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
|
@ -166,7 +166,7 @@ Pod::Spec.new do |s|
|
||||||
|
|
||||||
s.subspec "RCTBlob" do |ss|
|
s.subspec "RCTBlob" do |ss|
|
||||||
ss.dependency "React/Core"
|
ss.dependency "React/Core"
|
||||||
ss.source_files = "Libraries/Blob/*.{h,m}"
|
ss.source_files = "Libraries/Blob/*.{h,m,mm}"
|
||||||
ss.preserve_paths = "Libraries/Blob/*.js"
|
ss.preserve_paths = "Libraries/Blob/*.js"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -14,11 +14,13 @@ rn_android_library(
|
||||||
react_native_dep("libraries/fbcore/src/main/java/com/facebook/common/logging:logging"),
|
react_native_dep("libraries/fbcore/src/main/java/com/facebook/common/logging:logging"),
|
||||||
react_native_dep("third-party/java/infer-annotations:infer-annotations"),
|
react_native_dep("third-party/java/infer-annotations:infer-annotations"),
|
||||||
react_native_dep("third-party/java/jsr-305:jsr-305"),
|
react_native_dep("third-party/java/jsr-305:jsr-305"),
|
||||||
|
react_native_dep("third-party/java/okhttp:okhttp3"),
|
||||||
react_native_dep("third-party/java/okio:okio"),
|
react_native_dep("third-party/java/okio:okio"),
|
||||||
react_native_target("java/com/facebook/react:react"),
|
react_native_target("java/com/facebook/react:react"),
|
||||||
react_native_target("java/com/facebook/react/bridge:bridge"),
|
react_native_target("java/com/facebook/react/bridge:bridge"),
|
||||||
react_native_target("java/com/facebook/react/common:common"),
|
react_native_target("java/com/facebook/react/common:common"),
|
||||||
react_native_target("java/com/facebook/react/module/annotations:annotations"),
|
react_native_target("java/com/facebook/react/module/annotations:annotations"),
|
||||||
|
react_native_target("java/com/facebook/react/modules/network:network"),
|
||||||
react_native_target("java/com/facebook/react/modules/websocket:websocket"),
|
react_native_target("java/com/facebook/react/modules/websocket:websocket"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,9 +7,15 @@
|
||||||
*/
|
*/
|
||||||
package com.facebook.react.modules.blob;
|
package com.facebook.react.modules.blob;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.provider.MediaStore;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
import com.facebook.react.bridge.Arguments;
|
import com.facebook.react.bridge.Arguments;
|
||||||
import com.facebook.react.bridge.ReactApplicationContext;
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||||
|
@ -19,13 +25,25 @@ import com.facebook.react.bridge.ReadableMap;
|
||||||
import com.facebook.react.bridge.WritableMap;
|
import com.facebook.react.bridge.WritableMap;
|
||||||
import com.facebook.react.common.MapBuilder;
|
import com.facebook.react.common.MapBuilder;
|
||||||
import com.facebook.react.module.annotations.ReactModule;
|
import com.facebook.react.module.annotations.ReactModule;
|
||||||
|
import com.facebook.react.modules.network.NetworkingModule;
|
||||||
import com.facebook.react.modules.websocket.WebSocketModule;
|
import com.facebook.react.modules.websocket.WebSocketModule;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import okhttp3.MediaType;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.ResponseBody;
|
||||||
import okio.ByteString;
|
import okio.ByteString;
|
||||||
|
|
||||||
@ReactModule(name = BlobModule.NAME)
|
@ReactModule(name = BlobModule.NAME)
|
||||||
|
@ -35,27 +53,100 @@ public class BlobModule extends ReactContextBaseJavaModule {
|
||||||
|
|
||||||
private final Map<String, byte[]> mBlobs = new HashMap<>();
|
private final Map<String, byte[]> mBlobs = new HashMap<>();
|
||||||
|
|
||||||
protected final WebSocketModule.ContentHandler mContentHandler =
|
private final WebSocketModule.ContentHandler mWebSocketContentHandler =
|
||||||
new WebSocketModule.ContentHandler() {
|
new WebSocketModule.ContentHandler() {
|
||||||
@Override
|
@Override
|
||||||
public void onMessage(String text, WritableMap params) {
|
public void onMessage(String text, WritableMap params) {
|
||||||
params.putString("data", text);
|
params.putString("data", text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(ByteString bytes, WritableMap params) {
|
||||||
|
byte[] data = bytes.toByteArray();
|
||||||
|
|
||||||
|
WritableMap blob = Arguments.createMap();
|
||||||
|
|
||||||
|
blob.putString("blobId", store(data));
|
||||||
|
blob.putInt("offset", 0);
|
||||||
|
blob.putInt("size", data.length);
|
||||||
|
|
||||||
|
params.putMap("data", blob);
|
||||||
|
params.putString("type", "blob");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final NetworkingModule.UriHandler mNetworkingUriHandler =
|
||||||
|
new NetworkingModule.UriHandler() {
|
||||||
|
@Override
|
||||||
|
public boolean supports(Uri uri, String responseType) {
|
||||||
|
String scheme = uri.getScheme();
|
||||||
|
boolean isRemote = scheme.equals("http") || scheme.equals("https");
|
||||||
|
|
||||||
|
return (!isRemote && responseType.equals("blob"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WritableMap fetch(Uri uri) throws IOException {
|
||||||
|
byte[] data = getBytesFromUri(uri);
|
||||||
|
|
||||||
|
WritableMap blob = Arguments.createMap();
|
||||||
|
blob.putString("blobId", store(data));
|
||||||
|
blob.putInt("offset", 0);
|
||||||
|
blob.putInt("size", data.length);
|
||||||
|
blob.putString("type", getMimeTypeFromUri(uri));
|
||||||
|
|
||||||
|
// Needed for files
|
||||||
|
blob.putString("name", getNameFromUri(uri));
|
||||||
|
blob.putDouble("lastModified", getLastModifiedFromUri(uri));
|
||||||
|
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final NetworkingModule.RequestBodyHandler mNetworkingRequestBodyHandler =
|
||||||
|
new NetworkingModule.RequestBodyHandler() {
|
||||||
|
@Override
|
||||||
|
public boolean supports(ReadableMap data) {
|
||||||
|
return data.hasKey("blob");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequestBody toRequestBody(ReadableMap data, String contentType) {
|
||||||
|
String type = contentType;
|
||||||
|
if (data.hasKey("type") && !data.getString("type").isEmpty()) {
|
||||||
|
type = data.getString("type");
|
||||||
}
|
}
|
||||||
|
if (type == null) {
|
||||||
@Override
|
type = "application/octet-stream";
|
||||||
public void onMessage(ByteString bytes, WritableMap params) {
|
|
||||||
byte[] data = bytes.toByteArray();
|
|
||||||
|
|
||||||
WritableMap blob = Arguments.createMap();
|
|
||||||
|
|
||||||
blob.putString("blobId", store(data));
|
|
||||||
blob.putInt("offset", 0);
|
|
||||||
blob.putInt("size", data.length);
|
|
||||||
|
|
||||||
params.putMap("data", blob);
|
|
||||||
params.putString("type", "blob");
|
|
||||||
}
|
}
|
||||||
};
|
ReadableMap blob = data.getMap("blob");
|
||||||
|
String blobId = blob.getString("blobId");
|
||||||
|
byte[] bytes = resolve(
|
||||||
|
blobId,
|
||||||
|
blob.getInt("offset"),
|
||||||
|
blob.getInt("size"));
|
||||||
|
|
||||||
|
return RequestBody.create(MediaType.parse(type), bytes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final NetworkingModule.ResponseHandler mNetworkingResponseHandler =
|
||||||
|
new NetworkingModule.ResponseHandler() {
|
||||||
|
@Override
|
||||||
|
public boolean supports(String responseType) {
|
||||||
|
return responseType.equals("blob");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WritableMap toResponseData(ResponseBody body) throws IOException {
|
||||||
|
byte[] data = body.bytes();
|
||||||
|
WritableMap blob = Arguments.createMap();
|
||||||
|
blob.putString("blobId", store(data));
|
||||||
|
blob.putInt("offset", 0);
|
||||||
|
blob.putInt("size", data.length);
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public BlobModule(ReactApplicationContext reactContext) {
|
public BlobModule(ReactApplicationContext reactContext) {
|
||||||
super(reactContext);
|
super(reactContext);
|
||||||
|
@ -67,8 +158,7 @@ public class BlobModule extends ReactContextBaseJavaModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
public @Nullable Map<String, Object> getConstants() {
|
||||||
public Map getConstants() {
|
|
||||||
// The application can register BlobProvider as a ContentProvider so that blobs are resolvable.
|
// The application can register BlobProvider as a ContentProvider so that blobs are resolvable.
|
||||||
// If it does, it needs to tell us what authority was used via this string resource.
|
// If it does, it needs to tell us what authority was used via this string resource.
|
||||||
Resources resources = getReactApplicationContext().getResources();
|
Resources resources = getReactApplicationContext().getResources();
|
||||||
|
@ -78,8 +168,8 @@ public class BlobModule extends ReactContextBaseJavaModule {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return MapBuilder.of(
|
return MapBuilder.<String, Object>of(
|
||||||
"BLOB_URI_SCHEME", "content", "BLOB_URI_HOST", resources.getString(resourceId));
|
"BLOB_URI_SCHEME", "content", "BLOB_URI_HOST", resources.getString(resourceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public String store(byte[] data) {
|
public String store(byte[] data) {
|
||||||
|
@ -96,8 +186,7 @@ public class BlobModule extends ReactContextBaseJavaModule {
|
||||||
mBlobs.remove(blobId);
|
mBlobs.remove(blobId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
public @Nullable byte[] resolve(Uri uri) {
|
||||||
public byte[] resolve(Uri uri) {
|
|
||||||
String blobId = uri.getLastPathSegment();
|
String blobId = uri.getLastPathSegment();
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
int size = -1;
|
int size = -1;
|
||||||
|
@ -112,8 +201,7 @@ public class BlobModule extends ReactContextBaseJavaModule {
|
||||||
return resolve(blobId, offset, size);
|
return resolve(blobId, offset, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
public @Nullable byte[] resolve(String blobId, int offset, int size) {
|
||||||
public byte[] resolve(String blobId, int offset, int size) {
|
|
||||||
byte[] data = mBlobs.get(blobId);
|
byte[] data = mBlobs.get(blobId);
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -121,33 +209,101 @@ public class BlobModule extends ReactContextBaseJavaModule {
|
||||||
if (size == -1) {
|
if (size == -1) {
|
||||||
size = data.length - offset;
|
size = data.length - offset;
|
||||||
}
|
}
|
||||||
if (offset > 0) {
|
if (offset > 0 || size != data.length) {
|
||||||
data = Arrays.copyOfRange(data, offset, offset + size);
|
data = Arrays.copyOfRange(data, offset, offset + size);
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
public @Nullable byte[] resolve(ReadableMap blob) {
|
||||||
public byte[] resolve(ReadableMap blob) {
|
|
||||||
return resolve(blob.getString("blobId"), blob.getInt("offset"), blob.getInt("size"));
|
return resolve(blob.getString("blobId"), blob.getInt("offset"), blob.getInt("size"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte[] getBytesFromUri(Uri contentUri) throws IOException {
|
||||||
|
InputStream is = getReactApplicationContext().getContentResolver().openInputStream(contentUri);
|
||||||
|
|
||||||
|
if (is == null) {
|
||||||
|
throw new FileNotFoundException("File not found for " + contentUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
|
||||||
|
int bufferSize = 1024;
|
||||||
|
byte[] buffer = new byte[bufferSize];
|
||||||
|
int len;
|
||||||
|
while ((len = is.read(buffer)) != -1) {
|
||||||
|
byteBuffer.write(buffer, 0, len);
|
||||||
|
}
|
||||||
|
return byteBuffer.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getNameFromUri(Uri contentUri) {
|
||||||
|
if (contentUri.getScheme().equals("file")) {
|
||||||
|
return contentUri.getLastPathSegment();
|
||||||
|
}
|
||||||
|
String[] projection = {MediaStore.MediaColumns.DISPLAY_NAME};
|
||||||
|
Cursor metaCursor = getReactApplicationContext()
|
||||||
|
.getContentResolver()
|
||||||
|
.query(contentUri, projection, null, null, null);
|
||||||
|
if (metaCursor != null) {
|
||||||
|
try {
|
||||||
|
if (metaCursor.moveToFirst()) {
|
||||||
|
return metaCursor.getString(0);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
metaCursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return contentUri.getLastPathSegment();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getLastModifiedFromUri(Uri contentUri) {
|
||||||
|
if (contentUri.getScheme().equals("file")) {
|
||||||
|
return new File(contentUri.toString()).lastModified();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMimeTypeFromUri(Uri contentUri) {
|
||||||
|
String type = getReactApplicationContext().getContentResolver().getType(contentUri);
|
||||||
|
|
||||||
|
if (type == null) {
|
||||||
|
String ext = MimeTypeMap.getFileExtensionFromUrl(contentUri.getPath());
|
||||||
|
if (ext != null) {
|
||||||
|
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == null) {
|
||||||
|
type = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
private WebSocketModule getWebSocketModule() {
|
private WebSocketModule getWebSocketModule() {
|
||||||
return getReactApplicationContext().getNativeModule(WebSocketModule.class);
|
return getReactApplicationContext().getNativeModule(WebSocketModule.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void enableBlobSupport(final int id) {
|
public void addNetworkingHandler() {
|
||||||
getWebSocketModule().setContentHandler(id, mContentHandler);
|
NetworkingModule networkingModule = getReactApplicationContext().getNativeModule(NetworkingModule.class);
|
||||||
|
networkingModule.addUriHandler(mNetworkingUriHandler);
|
||||||
|
networkingModule.addRequestBodyHandler(mNetworkingRequestBodyHandler);
|
||||||
|
networkingModule.addResponseHandler(mNetworkingResponseHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void disableBlobSupport(final int id) {
|
public void addWebSocketHandler(final int id) {
|
||||||
|
getWebSocketModule().setContentHandler(id, mWebSocketContentHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void removeWebSocketHandler(final int id) {
|
||||||
getWebSocketModule().setContentHandler(id, null);
|
getWebSocketModule().setContentHandler(id, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void sendBlob(ReadableMap blob, int id) {
|
public void sendOverSocket(ReadableMap blob, int id) {
|
||||||
byte[] data = resolve(blob.getString("blobId"), blob.getInt("offset"), blob.getInt("size"));
|
byte[] data = resolve(blob.getString("blobId"), blob.getInt("offset"), blob.getInt("size"));
|
||||||
|
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
|
@ -160,15 +316,27 @@ public class BlobModule extends ReactContextBaseJavaModule {
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void createFromParts(ReadableArray parts, String blobId) {
|
public void createFromParts(ReadableArray parts, String blobId) {
|
||||||
int totalBlobSize = 0;
|
int totalBlobSize = 0;
|
||||||
ArrayList<ReadableMap> partList = new ArrayList<>(parts.size());
|
ArrayList<byte[]> partList = new ArrayList<>(parts.size());
|
||||||
for (int i = 0; i < parts.size(); i++) {
|
for (int i = 0; i < parts.size(); i++) {
|
||||||
ReadableMap part = parts.getMap(i);
|
ReadableMap part = parts.getMap(i);
|
||||||
totalBlobSize += part.getInt("size");
|
switch (part.getString("type")) {
|
||||||
partList.add(i, part);
|
case "blob":
|
||||||
|
ReadableMap blob = part.getMap("data");
|
||||||
|
totalBlobSize += blob.getInt("size");
|
||||||
|
partList.add(i, resolve(blob));
|
||||||
|
break;
|
||||||
|
case "string":
|
||||||
|
byte[] bytes = part.getString("data").getBytes(Charset.forName("UTF-8"));
|
||||||
|
totalBlobSize += bytes.length;
|
||||||
|
partList.add(i, bytes);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid type for blob: " + part.getString("type"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(totalBlobSize);
|
ByteBuffer buffer = ByteBuffer.allocate(totalBlobSize);
|
||||||
for (ReadableMap part : partList) {
|
for (byte[] bytes : partList) {
|
||||||
buffer.put(resolve(part));
|
buffer.put(bytes);
|
||||||
}
|
}
|
||||||
store(buffer.array(), blobId);
|
store(buffer.array(), blobId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.facebook.react.modules.blob;
|
||||||
|
|
||||||
|
import android.util.Base64;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.Promise;
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||||
|
import com.facebook.react.bridge.ReactMethod;
|
||||||
|
import com.facebook.react.bridge.ReadableMap;
|
||||||
|
import com.facebook.react.module.annotations.ReactModule;
|
||||||
|
|
||||||
|
|
||||||
|
@ReactModule(name = FileReaderModule.NAME)
|
||||||
|
public class FileReaderModule extends ReactContextBaseJavaModule {
|
||||||
|
|
||||||
|
protected static final String NAME = "FileReaderModule";
|
||||||
|
private static final String ERROR_INVALID_BLOB = "ERROR_INVALID_BLOB";
|
||||||
|
|
||||||
|
public FileReaderModule(ReactApplicationContext reactContext) {
|
||||||
|
super(reactContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BlobModule getBlobModule() {
|
||||||
|
return getReactApplicationContext().getNativeModule(BlobModule.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void readAsText(ReadableMap blob, String encoding, Promise promise) {
|
||||||
|
|
||||||
|
byte[] bytes = getBlobModule().resolve(
|
||||||
|
blob.getString("blobId"),
|
||||||
|
blob.getInt("offset"),
|
||||||
|
blob.getInt("size"));
|
||||||
|
|
||||||
|
if (bytes == null) {
|
||||||
|
promise.reject(ERROR_INVALID_BLOB, "The specified blob is invalid");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
promise.resolve(new String(bytes, encoding));
|
||||||
|
} catch (Exception e) {
|
||||||
|
promise.reject(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void readAsDataURL(ReadableMap blob, Promise promise) {
|
||||||
|
byte[] bytes = getBlobModule().resolve(
|
||||||
|
blob.getString("blobId"),
|
||||||
|
blob.getInt("offset"),
|
||||||
|
blob.getInt("size"));
|
||||||
|
|
||||||
|
if (bytes == null) {
|
||||||
|
promise.reject(ERROR_INVALID_BLOB, "The specified blob is invalid");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("data:");
|
||||||
|
|
||||||
|
if (blob.hasKey("type") && !blob.getString("type").isEmpty()) {
|
||||||
|
sb.append(blob.getString("type"));
|
||||||
|
} else {
|
||||||
|
sb.append("application/octet-stream");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append(";base64,");
|
||||||
|
sb.append(Base64.encodeToString(bytes, Base64.NO_WRAP));
|
||||||
|
|
||||||
|
promise.resolve(sb.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
promise.reject(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,20 +6,9 @@
|
||||||
* LICENSE file in the root directory of this source tree. An additional grant
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
* of patent rights can be found in the PATENTS file in the same directory.
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.facebook.react.modules.network;
|
package com.facebook.react.modules.network;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import android.net.Uri;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.Reader;
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
|
|
||||||
import com.facebook.react.bridge.Arguments;
|
import com.facebook.react.bridge.Arguments;
|
||||||
|
@ -35,6 +24,17 @@ import com.facebook.react.common.network.OkHttpCallUtil;
|
||||||
import com.facebook.react.module.annotations.ReactModule;
|
import com.facebook.react.module.annotations.ReactModule;
|
||||||
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
|
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import okhttp3.Call;
|
import okhttp3.Call;
|
||||||
import okhttp3.Callback;
|
import okhttp3.Callback;
|
||||||
import okhttp3.CookieJar;
|
import okhttp3.CookieJar;
|
||||||
|
@ -56,6 +56,52 @@ import okio.ByteString;
|
||||||
@ReactModule(name = NetworkingModule.NAME)
|
@ReactModule(name = NetworkingModule.NAME)
|
||||||
public final class NetworkingModule extends ReactContextBaseJavaModule {
|
public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to implement a custom fetching process for specific URIs. It is the handler's job
|
||||||
|
* to fetch the URI and return the JS body payload.
|
||||||
|
*/
|
||||||
|
public interface UriHandler {
|
||||||
|
/**
|
||||||
|
* Returns if the handler should be used for an URI.
|
||||||
|
*/
|
||||||
|
boolean supports(Uri uri, String responseType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the URI and return the JS body payload.
|
||||||
|
*/
|
||||||
|
WritableMap fetch(Uri uri) throws IOException;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows adding custom handling to build the {@link RequestBody} from the JS body payload.
|
||||||
|
*/
|
||||||
|
public interface RequestBodyHandler {
|
||||||
|
/**
|
||||||
|
* Returns if the handler should be used for a JS body payload.
|
||||||
|
*/
|
||||||
|
boolean supports(ReadableMap map);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link RequestBody} for the JS body payload.
|
||||||
|
*/
|
||||||
|
RequestBody toRequestBody(ReadableMap map, String contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows adding custom handling to build the JS body payload from the {@link ResponseBody}.
|
||||||
|
*/
|
||||||
|
public interface ResponseHandler {
|
||||||
|
/**
|
||||||
|
* Returns if the handler should be used for a response type.
|
||||||
|
*/
|
||||||
|
boolean supports(String responseType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the JS body payload for the {@link ResponseBody}.
|
||||||
|
*/
|
||||||
|
WritableMap toResponseData(ResponseBody body) throws IOException;
|
||||||
|
}
|
||||||
|
|
||||||
protected static final String NAME = "Networking";
|
protected static final String NAME = "Networking";
|
||||||
|
|
||||||
private static final String CONTENT_ENCODING_HEADER_NAME = "content-encoding";
|
private static final String CONTENT_ENCODING_HEADER_NAME = "content-encoding";
|
||||||
|
@ -73,6 +119,9 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||||
private final @Nullable String mDefaultUserAgent;
|
private final @Nullable String mDefaultUserAgent;
|
||||||
private final CookieJarContainer mCookieJarContainer;
|
private final CookieJarContainer mCookieJarContainer;
|
||||||
private final Set<Integer> mRequestIds;
|
private final Set<Integer> mRequestIds;
|
||||||
|
private final List<RequestBodyHandler> mRequestBodyHandlers = new ArrayList<>();
|
||||||
|
private final List<UriHandler> mUriHandlers = new ArrayList<>();
|
||||||
|
private final List<ResponseHandler> mResponseHandlers = new ArrayList<>();
|
||||||
private boolean mShuttingDown;
|
private boolean mShuttingDown;
|
||||||
|
|
||||||
/* package */ NetworkingModule(
|
/* package */ NetworkingModule(
|
||||||
|
@ -154,6 +203,34 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||||
|
|
||||||
mCookieHandler.destroy();
|
mCookieHandler.destroy();
|
||||||
mCookieJarContainer.removeCookieJar();
|
mCookieJarContainer.removeCookieJar();
|
||||||
|
|
||||||
|
mRequestBodyHandlers.clear();
|
||||||
|
mResponseHandlers.clear();
|
||||||
|
mUriHandlers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addUriHandler(UriHandler handler) {
|
||||||
|
mUriHandlers.add(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addRequestBodyHandler(RequestBodyHandler handler) {
|
||||||
|
mRequestBodyHandlers.add(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addResponseHandler(ResponseHandler handler) {
|
||||||
|
mResponseHandlers.add(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeUriHandler(UriHandler handler) {
|
||||||
|
mUriHandlers.remove(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeRequestBodyHandler(RequestBodyHandler handler) {
|
||||||
|
mRequestBodyHandlers.remove(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeResponseHandler(ResponseHandler handler) {
|
||||||
|
mResponseHandlers.remove(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
|
@ -170,13 +247,31 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||||
final boolean useIncrementalUpdates,
|
final boolean useIncrementalUpdates,
|
||||||
int timeout,
|
int timeout,
|
||||||
boolean withCredentials) {
|
boolean withCredentials) {
|
||||||
|
final RCTDeviceEventEmitter eventEmitter = getEventEmitter();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Uri uri = Uri.parse(url);
|
||||||
|
|
||||||
|
// Check if a handler is registered
|
||||||
|
for (UriHandler handler : mUriHandlers) {
|
||||||
|
if (handler.supports(uri, responseType)) {
|
||||||
|
WritableMap res = handler.fetch(uri);
|
||||||
|
ResponseUtil.onDataReceived(eventEmitter, requestId, res);
|
||||||
|
ResponseUtil.onRequestSuccess(eventEmitter, requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
ResponseUtil.onRequestError(eventEmitter, requestId, e.getMessage(), e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Request.Builder requestBuilder = new Request.Builder().url(url);
|
Request.Builder requestBuilder = new Request.Builder().url(url);
|
||||||
|
|
||||||
if (requestId != 0) {
|
if (requestId != 0) {
|
||||||
requestBuilder.tag(requestId);
|
requestBuilder.tag(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
final RCTDeviceEventEmitter eventEmitter = getEventEmitter();
|
|
||||||
OkHttpClient.Builder clientBuilder = mClient.newBuilder();
|
OkHttpClient.Builder clientBuilder = mClient.newBuilder();
|
||||||
|
|
||||||
if (!withCredentials) {
|
if (!withCredentials) {
|
||||||
|
@ -237,8 +332,22 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||||
String contentEncoding = requestHeaders.get(CONTENT_ENCODING_HEADER_NAME);
|
String contentEncoding = requestHeaders.get(CONTENT_ENCODING_HEADER_NAME);
|
||||||
requestBuilder.headers(requestHeaders);
|
requestBuilder.headers(requestHeaders);
|
||||||
|
|
||||||
|
// Check if a handler is registered
|
||||||
|
RequestBodyHandler handler = null;
|
||||||
|
if (data != null) {
|
||||||
|
for (RequestBodyHandler curHandler : mRequestBodyHandlers) {
|
||||||
|
if (curHandler.supports(data)) {
|
||||||
|
handler = curHandler;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
requestBuilder.method(method, RequestBodyUtil.getEmptyBody(method));
|
requestBuilder.method(method, RequestBodyUtil.getEmptyBody(method));
|
||||||
|
} else if (handler != null) {
|
||||||
|
RequestBody requestBody = handler.toRequestBody(data, contentType);
|
||||||
|
requestBuilder.method(method, requestBody);
|
||||||
} else if (data.hasKey(REQUEST_BODY_KEY_STRING)) {
|
} else if (data.hasKey(REQUEST_BODY_KEY_STRING)) {
|
||||||
if (contentType == null) {
|
if (contentType == null) {
|
||||||
ResponseUtil.onRequestError(
|
ResponseUtil.onRequestError(
|
||||||
|
@ -360,6 +469,16 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
|
||||||
|
|
||||||
ResponseBody responseBody = response.body();
|
ResponseBody responseBody = response.body();
|
||||||
try {
|
try {
|
||||||
|
// Check if a handler is registered
|
||||||
|
for (ResponseHandler handler : mResponseHandlers) {
|
||||||
|
if (handler.supports(responseType)) {
|
||||||
|
WritableMap res = handler.toResponseData(responseBody);
|
||||||
|
ResponseUtil.onDataReceived(eventEmitter, requestId, res);
|
||||||
|
ResponseUtil.onRequestSuccess(eventEmitter, requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If JS wants progress updates during the download, and it requested a text response,
|
// If JS wants progress updates during the download, and it requested a text response,
|
||||||
// periodically send response data updates to JS.
|
// periodically send response data updates to JS.
|
||||||
if (useIncrementalUpdates && responseType.equals("text")) {
|
if (useIncrementalUpdates && responseType.equals("text")) {
|
||||||
|
|
|
@ -9,14 +9,14 @@
|
||||||
|
|
||||||
package com.facebook.react.modules.network;
|
package com.facebook.react.modules.network;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.SocketTimeoutException;
|
|
||||||
|
|
||||||
import com.facebook.react.bridge.Arguments;
|
import com.facebook.react.bridge.Arguments;
|
||||||
import com.facebook.react.bridge.WritableArray;
|
import com.facebook.react.bridge.WritableArray;
|
||||||
import com.facebook.react.bridge.WritableMap;
|
import com.facebook.react.bridge.WritableMap;
|
||||||
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
|
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.SocketTimeoutException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Util methods to send network responses to JS.
|
* Util methods to send network responses to JS.
|
||||||
*/
|
*/
|
||||||
|
@ -72,6 +72,17 @@ public class ResponseUtil {
|
||||||
eventEmitter.emit("didReceiveNetworkData", args);
|
eventEmitter.emit("didReceiveNetworkData", args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void onDataReceived(
|
||||||
|
RCTDeviceEventEmitter eventEmitter,
|
||||||
|
int requestId,
|
||||||
|
WritableMap data) {
|
||||||
|
WritableArray args = Arguments.createArray();
|
||||||
|
args.pushInt(requestId);
|
||||||
|
args.pushMap(data);
|
||||||
|
|
||||||
|
eventEmitter.emit("didReceiveNetworkData", args);
|
||||||
|
}
|
||||||
|
|
||||||
public static void onRequestError(
|
public static void onRequestError(
|
||||||
RCTDeviceEventEmitter eventEmitter,
|
RCTDeviceEventEmitter eventEmitter,
|
||||||
int requestId,
|
int requestId,
|
||||||
|
|
|
@ -30,6 +30,7 @@ import com.facebook.react.module.model.ReactModuleInfoProvider;
|
||||||
import com.facebook.react.modules.accessibilityinfo.AccessibilityInfoModule;
|
import com.facebook.react.modules.accessibilityinfo.AccessibilityInfoModule;
|
||||||
import com.facebook.react.modules.appstate.AppStateModule;
|
import com.facebook.react.modules.appstate.AppStateModule;
|
||||||
import com.facebook.react.modules.blob.BlobModule;
|
import com.facebook.react.modules.blob.BlobModule;
|
||||||
|
import com.facebook.react.modules.blob.FileReaderModule;
|
||||||
import com.facebook.react.modules.camera.CameraRollManager;
|
import com.facebook.react.modules.camera.CameraRollManager;
|
||||||
import com.facebook.react.modules.camera.ImageEditingManager;
|
import com.facebook.react.modules.camera.ImageEditingManager;
|
||||||
import com.facebook.react.modules.camera.ImageStoreManager;
|
import com.facebook.react.modules.camera.ImageStoreManager;
|
||||||
|
@ -125,6 +126,14 @@ public class MainReactPackage extends LazyReactPackage {
|
||||||
return new BlobModule(context);
|
return new BlobModule(context);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
ModuleSpec.nativeModuleSpec(
|
||||||
|
FileReaderModule.class,
|
||||||
|
new Provider<NativeModule>() {
|
||||||
|
@Override
|
||||||
|
public NativeModule get() {
|
||||||
|
return new FileReaderModule(context);
|
||||||
|
}
|
||||||
|
}),
|
||||||
ModuleSpec.nativeModuleSpec(
|
ModuleSpec.nativeModuleSpec(
|
||||||
AsyncStorageModule.class,
|
AsyncStorageModule.class,
|
||||||
new Provider<NativeModule>() {
|
new Provider<NativeModule>() {
|
||||||
|
|
|
@ -26,6 +26,7 @@ rn_robolectric_test(
|
||||||
react_native_target("java/com/facebook/react/common/network:network"),
|
react_native_target("java/com/facebook/react/common/network:network"),
|
||||||
react_native_target("java/com/facebook/react/devsupport:interfaces"),
|
react_native_target("java/com/facebook/react/devsupport:interfaces"),
|
||||||
react_native_target("java/com/facebook/react/jstasks:jstasks"),
|
react_native_target("java/com/facebook/react/jstasks:jstasks"),
|
||||||
|
react_native_target("java/com/facebook/react/modules/blob:blob"),
|
||||||
react_native_target("java/com/facebook/react/modules/camera:camera"),
|
react_native_target("java/com/facebook/react/modules/camera:camera"),
|
||||||
react_native_target("java/com/facebook/react/modules/clipboard:clipboard"),
|
react_native_target("java/com/facebook/react/modules/clipboard:clipboard"),
|
||||||
react_native_target("java/com/facebook/react/modules/common:common"),
|
react_native_target("java/com/facebook/react/modules/common:common"),
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.facebook.react.modules.blob;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.Arguments;
|
||||||
|
import com.facebook.react.bridge.JavaOnlyArray;
|
||||||
|
import com.facebook.react.bridge.JavaOnlyMap;
|
||||||
|
import com.facebook.react.bridge.ReactTestHelper;
|
||||||
|
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
|
import org.mockito.stubbing.Answer;
|
||||||
|
import org.powermock.api.mockito.PowerMockito;
|
||||||
|
import org.powermock.core.classloader.annotations.PowerMockIgnore;
|
||||||
|
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||||
|
import org.powermock.modules.junit4.rule.PowerMockRule;
|
||||||
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
import org.robolectric.annotation.Config;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
|
||||||
|
@PrepareForTest({Arguments.class})
|
||||||
|
@RunWith(RobolectricTestRunner.class)
|
||||||
|
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
|
||||||
|
@Config(manifest = Config.NONE)
|
||||||
|
public class BlobModuleTest {
|
||||||
|
|
||||||
|
private byte[] mBytes;
|
||||||
|
private String mBlobId;
|
||||||
|
private BlobModule mBlobModule;
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public PowerMockRule rule = new PowerMockRule();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void prepareModules() throws Exception {
|
||||||
|
PowerMockito.mockStatic(Arguments.class);
|
||||||
|
Mockito.when(Arguments.createMap()).thenAnswer(new Answer<Object>() {
|
||||||
|
@Override
|
||||||
|
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||||
|
return new JavaOnlyMap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mBytes = new byte[120];
|
||||||
|
new Random().nextBytes(mBytes);
|
||||||
|
|
||||||
|
mBlobModule = new BlobModule(ReactTestHelper.createCatalystContextForTest());
|
||||||
|
mBlobId = mBlobModule.store(mBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void cleanUp() {
|
||||||
|
mBlobModule.remove(mBlobId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testResolve() {
|
||||||
|
assertArrayEquals(mBytes, mBlobModule.resolve(mBlobId, 0, mBytes.length));
|
||||||
|
byte[] expectedRange = Arrays.copyOfRange(mBytes, 30, mBytes.length);
|
||||||
|
assertArrayEquals(expectedRange, mBlobModule.resolve(mBlobId, 30, mBytes.length - 30));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testResolveUri() {
|
||||||
|
Uri uri = new Uri.Builder()
|
||||||
|
.appendPath(mBlobId)
|
||||||
|
.appendQueryParameter("offset", "0")
|
||||||
|
.appendQueryParameter("size", String.valueOf(mBytes.length))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertArrayEquals(mBytes, mBlobModule.resolve(uri));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testResolveMap() {
|
||||||
|
JavaOnlyMap blob = new JavaOnlyMap();
|
||||||
|
blob.putString("blobId", mBlobId);
|
||||||
|
blob.putInt("offset", 0);
|
||||||
|
blob.putInt("size", mBytes.length);
|
||||||
|
|
||||||
|
assertArrayEquals(mBytes, mBlobModule.resolve(blob));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemove() {
|
||||||
|
assertNotNull(mBlobModule.resolve(mBlobId, 0, mBytes.length));
|
||||||
|
|
||||||
|
mBlobModule.remove(mBlobId);
|
||||||
|
|
||||||
|
assertNull(mBlobModule.resolve(mBlobId, 0, mBytes.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateFromParts() {
|
||||||
|
String id = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
JavaOnlyMap blobData = new JavaOnlyMap();
|
||||||
|
blobData.putString("blobId", mBlobId);
|
||||||
|
blobData.putInt("offset", 0);
|
||||||
|
blobData.putInt("size", mBytes.length);
|
||||||
|
JavaOnlyMap blob = new JavaOnlyMap();
|
||||||
|
blob.putMap("data", blobData);
|
||||||
|
blob.putString("type", "blob");
|
||||||
|
|
||||||
|
String stringData = "i \u2665 dogs";
|
||||||
|
byte[] stringBytes = stringData.getBytes(Charset.forName("UTF-8"));
|
||||||
|
JavaOnlyMap string = new JavaOnlyMap();
|
||||||
|
string.putString("data", stringData);
|
||||||
|
string.putString("type", "string");
|
||||||
|
|
||||||
|
JavaOnlyArray parts = new JavaOnlyArray();
|
||||||
|
parts.pushMap(blob);
|
||||||
|
parts.pushMap(string);
|
||||||
|
|
||||||
|
mBlobModule.createFromParts(parts, id);
|
||||||
|
|
||||||
|
int resultSize = mBytes.length + stringBytes.length;
|
||||||
|
|
||||||
|
byte[] result = mBlobModule.resolve(id, 0, resultSize);
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(resultSize);
|
||||||
|
buffer.put(mBytes);
|
||||||
|
buffer.put(stringBytes);
|
||||||
|
|
||||||
|
assertArrayEquals(result, buffer.array());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRelease() {
|
||||||
|
assertNotNull(mBlobModule.resolve(mBlobId, 0, mBytes.length));
|
||||||
|
|
||||||
|
mBlobModule.release(mBlobId);
|
||||||
|
|
||||||
|
assertNull(mBlobModule.resolve(mBlobId, 0, mBytes.length));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue