mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-17 23:04:40 +08:00
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: ! 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:
committed by
Facebook Github Bot
parent
3fc33bb54f
commit
be56a3efee
@@ -8,19 +8,12 @@
|
||||
*
|
||||
* @providesModule Blob
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const invariant = require('fbjs/lib/invariant');
|
||||
/* $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';
|
||||
import type {BlobData, BlobOptions} from 'BlobTypes';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
class Blob {
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
_data: ?BlobData;
|
||||
|
||||
/**
|
||||
* Constructor for JS consumers.
|
||||
* Currently we only support creating Blobs from other Blobs.
|
||||
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob
|
||||
*/
|
||||
constructor(parts: Array<Blob>, options: any) {
|
||||
const blobId = uuid();
|
||||
let size = 0;
|
||||
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,
|
||||
});
|
||||
constructor(parts: Array<Blob | string> = [], options?: BlobOptions) {
|
||||
const BlobManager = require('BlobManager');
|
||||
this.data = BlobManager.createFromParts(parts, options).data;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -112,9 +70,22 @@ class 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
|
||||
*/
|
||||
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 {
|
||||
let offset = this.offset;
|
||||
let size = this.size;
|
||||
const BlobManager = require('BlobManager');
|
||||
let {offset, size} = this.data;
|
||||
|
||||
if (typeof start === 'number') {
|
||||
if (start > size) {
|
||||
start = size;
|
||||
@@ -129,8 +100,8 @@ class Blob {
|
||||
size = end - start;
|
||||
}
|
||||
}
|
||||
return Blob.create({
|
||||
blobId: this.blobId,
|
||||
return BlobManager.createFromOptions({
|
||||
blobId: this.data.blobId,
|
||||
offset,
|
||||
size,
|
||||
});
|
||||
@@ -149,7 +120,24 @@ class Blob {
|
||||
* `new Blob([blob, ...])` actually copies the data in memory.
|
||||
*/
|
||||
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 || '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
146
Libraries/Blob/BlobManager.js
Normal file
146
Libraries/Blob/BlobManager.js
Normal file
@@ -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;
|
||||
41
Libraries/Blob/BlobRegistry.js
Normal file
41
Libraries/Blob/BlobRegistry.js
Normal file
@@ -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
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
export type BlobProps = {
|
||||
export type BlobData = {
|
||||
blobId: string,
|
||||
offset: number,
|
||||
size: number,
|
||||
name?: string,
|
||||
type?: string,
|
||||
lastModified?: number,
|
||||
};
|
||||
|
||||
export type FileProps = BlobProps & {
|
||||
name: string,
|
||||
export type BlobOptions = {
|
||||
type: string,
|
||||
lastModified: number,
|
||||
};
|
||||
|
||||
58
Libraries/Blob/File.js
Normal file
58
Libraries/Blob/File.js
Normal file
@@ -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;
|
||||
156
Libraries/Blob/FileReader.js
Normal file
156
Libraries/Blob/FileReader.js
Normal 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 = {
|
||||
|
||||
/* 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 */; };
|
||||
AD0871161E215EC9007D136D /* 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 */; };
|
||||
AD9A43C31DFC7126008DC588 /* RCTBlobManager.m in Sources */ = {isa = PBXBuildFile; fileRef = AD9A43C21DFC7126008DC588 /* RCTBlobManager.m */; };
|
||||
ADD01A711E09404A00F6D226 /* RCTBlobManager.m in Sources */ = {isa = PBXBuildFile; fileRef = AD9A43C21DFC7126008DC588 /* RCTBlobManager.m */; };
|
||||
AD9A43C31DFC7126008DC588 /* RCTBlobManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = AD9A43C21DFC7126008DC588 /* RCTBlobManager.mm */; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
@@ -22,6 +28,7 @@
|
||||
dstPath = include/RCTBlob;
|
||||
dstSubfolderSpec = 16;
|
||||
files = (
|
||||
19BA88FE1F84391700741C5A /* RCTFileReaderModule.h in Copy Headers */,
|
||||
AD08711A1E2162C8007D136D /* RCTBlobManager.h in Copy Headers */,
|
||||
);
|
||||
name = "Copy Headers";
|
||||
@@ -33,6 +40,7 @@
|
||||
dstPath = include/RCTBlob;
|
||||
dstSubfolderSpec = 16;
|
||||
files = (
|
||||
19BA89001F84392F00741C5A /* RCTFileReaderModule.h in Copy Headers */,
|
||||
AD0871131E215B28007D136D /* RCTBlobManager.h in Copy Headers */,
|
||||
);
|
||||
name = "Copy Headers";
|
||||
@@ -42,17 +50,21 @@
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
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>"; };
|
||||
AD9A43C21DFC7126008DC588 /* RCTBlobManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBlobManager.m; sourceTree = "<group>"; };
|
||||
AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTBlobManager.h; 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; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
358F4ECE1D1E81A9004DF814 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ADDFBA6A1F33455F0064C998 /* RCTFileReaderModule.h */,
|
||||
ADDFBA6B1F33455F0064C998 /* RCTFileReaderModule.m */,
|
||||
AD9A43C11DFC7126008DC588 /* RCTBlobManager.h */,
|
||||
AD9A43C21DFC7126008DC588 /* RCTBlobManager.m */,
|
||||
AD9A43C21DFC7126008DC588 /* RCTBlobManager.mm */,
|
||||
358F4ED81D1E81A9004DF814 /* Products */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
@@ -77,6 +89,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
AD0871161E215EC9007D136D /* RCTBlobManager.h in Headers */,
|
||||
ADDFBA6C1F33455F0064C998 /* RCTFileReaderModule.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -84,6 +97,7 @@
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
19BA88FF1F84392900741C5A /* RCTFileReaderModule.h in Headers */,
|
||||
AD0871181E215ED1007D136D /* RCTBlobManager.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -132,7 +146,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 0730;
|
||||
ORGANIZATIONNAME = "Silk Labs";
|
||||
ORGANIZATIONNAME = Facebook;
|
||||
TargetAttributes = {
|
||||
358F4ED61D1E81A9004DF814 = {
|
||||
CreatedOnToolsVersion = 7.3;
|
||||
@@ -166,7 +180,8 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
AD9A43C31DFC7126008DC588 /* RCTBlobManager.m in Sources */,
|
||||
ADDFBA6D1F33455F0064C998 /* RCTFileReaderModule.m in Sources */,
|
||||
AD9A43C31DFC7126008DC588 /* RCTBlobManager.mm in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -174,7 +189,8 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
ADD01A711E09404A00F6D226 /* RCTBlobManager.m in Sources */,
|
||||
19BA89011F84393D00741C5A /* RCTFileReaderModule.m in Sources */,
|
||||
ADD01A711E09404A00F6D226 /* RCTBlobManager.mm in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -13,4 +13,18 @@
|
||||
|
||||
@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
|
||||
|
||||
@@ -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
|
||||
290
Libraries/Blob/RCTBlobManager.mm
Executable file
290
Libraries/Blob/RCTBlobManager.mm
Executable file
@@ -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
|
||||
14
Libraries/Blob/RCTFileReaderModule.h
Normal file
14
Libraries/Blob/RCTFileReaderModule.h
Normal file
@@ -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
|
||||
71
Libraries/Blob/RCTFileReaderModule.m
Normal file
71
Libraries/Blob/RCTFileReaderModule.m
Normal file
@@ -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 {
|
||||
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) {
|
||||
if (BLOB_URL_PREFIX === null) {
|
||||
throw new Error('Cannot create URL for blob!');
|
||||
}
|
||||
return `${BLOB_URL_PREFIX}${blob.blobId}?offset=${blob.offset}&size=${
|
||||
blob.size
|
||||
}`;
|
||||
return `${BLOB_URL_PREFIX}${blob.data.blobId}?offset=${
|
||||
blob.data.offset
|
||||
}&size=${blob.size}`;
|
||||
}
|
||||
|
||||
static revokeObjectURL(url: string) {
|
||||
|
||||
17
Libraries/Blob/__mocks__/BlobModule.js
Normal file
17
Libraries/Blob/__mocks__/BlobModule.js
Normal file
@@ -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;
|
||||
21
Libraries/Blob/__mocks__/FileReaderModule.js
Normal file
21
Libraries/Blob/__mocks__/FileReaderModule.js
Normal file
@@ -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;
|
||||
84
Libraries/Blob/__tests__/Blob-test.js
Normal file
84
Libraries/Blob/__tests__/Blob-test.js
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
27
Libraries/Blob/__tests__/BlobManager-test.js
Normal file
27
Libraries/Blob/__tests__/BlobManager-test.js
Normal file
@@ -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');
|
||||
});
|
||||
});
|
||||
46
Libraries/Blob/__tests__/File-test.js
Normal file
46
Libraries/Blob/__tests__/File-test.js
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
42
Libraries/Blob/__tests__/FileReader-test.js
Normal file
42
Libraries/Blob/__tests__/FileReader-test.js
Normal file
@@ -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('WebSocket', () => require('WebSocket'));
|
||||
polyfillGlobal('Blob', () => require('Blob'));
|
||||
polyfillGlobal('File', () => require('File'));
|
||||
polyfillGlobal('FileReader', () => require('FileReader'));
|
||||
polyfillGlobal('URL', () => require('URL'));
|
||||
|
||||
// Set up alert
|
||||
|
||||
@@ -10,6 +10,22 @@
|
||||
#import <React/RCTEventEmitter.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
|
||||
|
||||
/**
|
||||
@@ -24,6 +40,14 @@
|
||||
- (RCTNetworkTask *)networkTaskWithRequest:(NSURLRequest *)request
|
||||
completionBlock:(RCTURLRequestCompletionBlock)completionBlock;
|
||||
|
||||
- (void)addRequestHandler:(id<RCTNetworkingRequestHandler>)handler;
|
||||
|
||||
- (void)addResponseHandler:(id<RCTNetworkingResponseHandler>)handler;
|
||||
|
||||
- (void)removeRequestHandler:(id<RCTNetworkingRequestHandler>)handler;
|
||||
|
||||
- (void)removeResponseHandler:(id<RCTNetworkingResponseHandler>)handler;
|
||||
|
||||
@end
|
||||
|
||||
@interface RCTBridge (RCTNetworking)
|
||||
|
||||
@@ -18,6 +18,8 @@ const convertRequestBody = require('convertRequestBody');
|
||||
|
||||
import type {RequestBody} from 'convertRequestBody';
|
||||
|
||||
import type { NativeResponseType } from './XMLHttpRequest';
|
||||
|
||||
class RCTNetworking extends NativeEventEmitter {
|
||||
|
||||
isAvailable: boolean = true;
|
||||
@@ -32,7 +34,7 @@ class RCTNetworking extends NativeEventEmitter {
|
||||
url: string,
|
||||
headers: Object,
|
||||
data: RequestBody,
|
||||
responseType: 'text' | 'base64',
|
||||
responseType: NativeResponseType,
|
||||
incrementalUpdates: boolean,
|
||||
timeout: number,
|
||||
callback: (requestId: number) => any,
|
||||
|
||||
@@ -131,12 +131,20 @@ static NSString *RCTGenerateFormBoundary()
|
||||
NSMutableDictionary<NSNumber *, RCTNetworkTask *> *_tasksByRequestID;
|
||||
std::mutex _handlersLock;
|
||||
NSArray<id<RCTURLRequestHandler>> *_handlers;
|
||||
NSMutableArray<id<RCTNetworkingRequestHandler>> *_requestHandlers;
|
||||
NSMutableArray<id<RCTNetworkingResponseHandler>> *_responseHandlers;
|
||||
}
|
||||
|
||||
@synthesize methodQueue = _methodQueue;
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
- (void)invalidate
|
||||
{
|
||||
_requestHandlers = nil;
|
||||
_responseHandlers = nil;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)supportedEvents
|
||||
{
|
||||
return @[@"didCompleteNetworkResponse",
|
||||
@@ -297,6 +305,8 @@ RCT_EXPORT_MODULE()
|
||||
*
|
||||
* - {"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:
|
||||
*
|
||||
* - @"body" (NSData): the body of the request
|
||||
@@ -312,6 +322,15 @@ RCT_EXPORT_MODULE()
|
||||
if (!query) {
|
||||
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"]];
|
||||
if (body) {
|
||||
return callback(nil, @{@"body": body});
|
||||
@@ -417,6 +436,7 @@ RCT_EXPORT_MODULE()
|
||||
|
||||
- (void)sendData:(NSData *)data
|
||||
responseType:(NSString *)responseType
|
||||
response:(NSURLResponse *)response
|
||||
forTask:(RCTNetworkTask *)task
|
||||
{
|
||||
RCTAssertThread(_methodQueue, @"sendData: must be called on method queue");
|
||||
@@ -425,23 +445,31 @@ RCT_EXPORT_MODULE()
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *responseString;
|
||||
if ([responseType isEqualToString:@"text"]) {
|
||||
// No carry storage is required here because the entire data has been loaded.
|
||||
responseString = [RCTNetworking decodeTextData:data fromResponse:task.response withCarryData:nil];
|
||||
if (!responseString) {
|
||||
RCTLogWarn(@"Received data was not a string, or was not a recognised encoding.");
|
||||
return;
|
||||
id responseData = nil;
|
||||
for (id<RCTNetworkingResponseHandler> handler in _responseHandlers) {
|
||||
if ([handler canHandleNetworkingResponse:responseType]) {
|
||||
responseData = [handler handleNetworkingResponse:response data:data];
|
||||
break;
|
||||
}
|
||||
} else if ([responseType isEqualToString:@"base64"]) {
|
||||
responseString = [data base64EncodedStringWithOptions:0];
|
||||
} else {
|
||||
RCTLogWarn(@"Invalid responseType: %@", responseType);
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray<id> *responseJSON = @[task.requestID, responseString];
|
||||
[self sendEventWithName:@"didReceiveNetworkData" body:responseJSON];
|
||||
if (!responseData) {
|
||||
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
|
||||
@@ -523,7 +551,10 @@ RCT_EXPORT_MODULE()
|
||||
// Unless we were sending incremental (text) chunks to JS, all along, now
|
||||
// is the time to send the request body to JS.
|
||||
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,
|
||||
RCTNullIfNil(error.localizedDescription),
|
||||
@@ -553,6 +584,32 @@ RCT_EXPORT_MODULE()
|
||||
|
||||
#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
|
||||
{
|
||||
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
|
||||
* run Flow. */
|
||||
const warning = require('fbjs/lib/warning');
|
||||
const BlobManager = require('BlobManager');
|
||||
|
||||
type ResponseType = '' | 'arraybuffer' | 'blob' | 'document' | 'json' | 'text';
|
||||
type Response = ?Object | string;
|
||||
export type NativeResponseType = 'base64' | 'blob' | 'text';
|
||||
export type ResponseType = '' | 'arraybuffer' | 'blob' | 'document' | 'json' | 'text';
|
||||
export type Response = ?Object | string;
|
||||
|
||||
type XHRInterceptor = {
|
||||
requestSent(
|
||||
@@ -54,6 +56,11 @@ type XHRInterceptor = {
|
||||
): void,
|
||||
};
|
||||
|
||||
// The native blob module is optional so inject it here if available.
|
||||
if (BlobManager.isAvailable) {
|
||||
BlobManager.addNetworkingHandler();
|
||||
}
|
||||
|
||||
const UNSENT = 0;
|
||||
const OPENED = 1;
|
||||
const HEADERS_RECEIVED = 2;
|
||||
@@ -200,6 +207,10 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
||||
SUPPORTED_RESPONSE_TYPES[responseType] || responseType === 'document',
|
||||
`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;
|
||||
}
|
||||
|
||||
@@ -242,10 +253,11 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
||||
break;
|
||||
|
||||
case 'blob':
|
||||
this._cachedResponse = new global.Blob(
|
||||
[base64.toByteArray(this._response).buffer],
|
||||
{type: this.getResponseHeader('content-type') || ''}
|
||||
);
|
||||
if (typeof this._response === 'object' && this._response) {
|
||||
this._cachedResponse = BlobManager.createFromOptions(this._response);
|
||||
} else {
|
||||
throw new Error(`Invalid response for blob: ${this._response}`);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'json':
|
||||
@@ -493,10 +505,13 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
|
||||
(args) => this.__didCompleteResponse(...args)
|
||||
));
|
||||
|
||||
let nativeResponseType = 'text';
|
||||
if (this._responseType === 'arraybuffer' || this._responseType === 'blob') {
|
||||
let nativeResponseType: NativeResponseType = 'text';
|
||||
if (this._responseType === 'arraybuffer') {
|
||||
nativeResponseType = 'base64';
|
||||
}
|
||||
if (this._responseType === 'blob') {
|
||||
nativeResponseType = 'blob';
|
||||
}
|
||||
|
||||
invariant(this._method, 'Request method needs to be defined.');
|
||||
invariant(this._url, 'Request URL needs to be defined.');
|
||||
|
||||
@@ -8,25 +8,30 @@
|
||||
*
|
||||
* @providesModule convertRequestBody
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const binaryToBase64 = require('binaryToBase64');
|
||||
|
||||
const Blob = require('Blob');
|
||||
const FormData = require('FormData');
|
||||
|
||||
export type RequestBody =
|
||||
string
|
||||
| string
|
||||
| Blob
|
||||
| FormData
|
||||
| {uri: string}
|
||||
| ArrayBuffer
|
||||
| $ArrayBufferView
|
||||
;
|
||||
| $ArrayBufferView;
|
||||
|
||||
function convertRequestBody(body: RequestBody): Object {
|
||||
if (typeof body === 'string') {
|
||||
return {string: body};
|
||||
}
|
||||
if (body instanceof Blob) {
|
||||
return {blob: body.data};
|
||||
}
|
||||
if (body instanceof FormData) {
|
||||
return {formData: body.getParts()};
|
||||
}
|
||||
|
||||
@@ -13,8 +13,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol RCTWebSocketContentHandler <NSObject>
|
||||
|
||||
- (id)processMessage:(id __nullable)message forSocketID:(NSNumber *)socketID
|
||||
withType:(NSString *__nonnull __autoreleasing *__nonnull)type;
|
||||
- (id)processWebsocketMessage:(id __nullable)message
|
||||
forSocketID:(NSNumber *)socketID
|
||||
withType:(NSString *__nonnull __autoreleasing *__nonnull)type;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
@implementation RCTWebSocketModule
|
||||
{
|
||||
NSMutableDictionary<NSNumber *, RCTSRWebSocket *> *_sockets;
|
||||
NSMutableDictionary<NSNumber *, id> *_contentHandlers;
|
||||
NSMutableDictionary<NSNumber *, id<RCTWebSocketContentHandler>> *_contentHandlers;
|
||||
}
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
@@ -53,8 +53,9 @@ RCT_EXPORT_MODULE()
|
||||
@"websocketClosed"];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
- (void)invalidate
|
||||
{
|
||||
_contentHandlers = nil;
|
||||
for (RCTSRWebSocket *socket in _sockets.allValues) {
|
||||
socket.delegate = nil;
|
||||
[socket close];
|
||||
@@ -135,7 +136,7 @@ RCT_EXPORT_METHOD(close:(nonnull NSNumber *)socketID)
|
||||
NSNumber *socketID = [webSocket reactTag];
|
||||
id contentHandler = _contentHandlers[socketID];
|
||||
if (contentHandler) {
|
||||
message = [contentHandler processMessage:message forSocketID:socketID withType:&type];
|
||||
message = [contentHandler processWebsocketMessage:message forSocketID:socketID withType:&type];
|
||||
} else {
|
||||
if ([message isKindOfClass:[NSData class]]) {
|
||||
type = @"binary";
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
const Blob = require('Blob');
|
||||
const EventTarget = require('event-target-shim');
|
||||
const NativeEventEmitter = require('NativeEventEmitter');
|
||||
const BlobManager = require('BlobManager');
|
||||
const NativeModules = require('NativeModules');
|
||||
const Platform = require('Platform');
|
||||
const WebSocketEvent = require('WebSocketEvent');
|
||||
@@ -147,19 +148,20 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
|
||||
throw new Error('binaryType must be either \'blob\' or \'arraybuffer\'');
|
||||
}
|
||||
if (this._binaryType === 'blob' || binaryType === 'blob') {
|
||||
const BlobModule = NativeModules.BlobModule;
|
||||
invariant(BlobModule, 'Native module BlobModule is required for blob support');
|
||||
if (BlobModule) {
|
||||
if (binaryType === 'blob') {
|
||||
BlobModule.enableBlobSupport(this._socketId);
|
||||
} else {
|
||||
BlobModule.disableBlobSupport(this._socketId);
|
||||
}
|
||||
invariant(BlobManager.isAvailable, 'Native module BlobModule is required for blob support');
|
||||
if (binaryType === 'blob') {
|
||||
BlobManager.addWebSocketHandler(this._socketId);
|
||||
} else {
|
||||
BlobManager.removeWebSocketHandler(this._socketId);
|
||||
}
|
||||
}
|
||||
this._binaryType = binaryType;
|
||||
}
|
||||
|
||||
get binaryType(): ?BinaryType {
|
||||
return this._binaryType;
|
||||
}
|
||||
|
||||
close(code?: number, reason?: string): void {
|
||||
if (this.readyState === this.CLOSING ||
|
||||
this.readyState === this.CLOSED) {
|
||||
@@ -176,9 +178,8 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
|
||||
}
|
||||
|
||||
if (data instanceof Blob) {
|
||||
const BlobModule = NativeModules.BlobModule;
|
||||
invariant(BlobModule, 'Native module BlobModule is required for blob support');
|
||||
BlobModule.sendBlob(data, this._socketId);
|
||||
invariant(BlobManager.isAvailable, 'Native module BlobModule is required for blob support');
|
||||
BlobManager.sendOverSocket(data, this._socketId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -212,6 +213,10 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
|
||||
} else {
|
||||
WebSocketModule.close(this._socketId);
|
||||
}
|
||||
|
||||
if (BlobManager.isAvailable && this._binaryType === 'blob') {
|
||||
BlobManager.removeWebSocketHandler(this._socketId);
|
||||
}
|
||||
}
|
||||
|
||||
_unregisterEvents(): void {
|
||||
@@ -231,7 +236,7 @@ class WebSocket extends EventTarget(...WEBSOCKET_EVENTS) {
|
||||
data = base64.toByteArray(ev.data).buffer;
|
||||
break;
|
||||
case 'blob':
|
||||
data = Blob.create(ev.data);
|
||||
data = BlobManager.createFromOptions(ev.data);
|
||||
break;
|
||||
}
|
||||
this.dispatchEvent(new WebSocketEvent('message', { data }));
|
||||
|
||||
Reference in New Issue
Block a user