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:
Satyajit Sahoo 2018-01-26 09:06:14 -08:00 committed by Facebook Github Bot
parent 3fc33bb54f
commit be56a3efee
40 changed files with 2060 additions and 386 deletions

View File

@ -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 || '';
}
}

View 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;

View 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,
};

View File

@ -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
View 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;

View 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;

View File

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

View File

@ -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

View File

@ -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
View 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

View 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

View 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

View File

@ -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) {

View 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;

View 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;

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

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

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

View 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=');
});
});

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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];

View File

@ -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.');

View File

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

View File

@ -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

View File

@ -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";

View File

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

View File

@ -53,6 +53,8 @@
192F69B91E82409A008692C7 /* RCTConvert_YGValueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 192F69B61E82409A008692C7 /* RCTConvert_YGValueTests.m */; };
192F69BA1E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 192F69B71E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m */; };
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 */; };
27B885561BED29AF00008352 /* RCTRootViewIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 27B885551BED29AF00008352 /* RCTRootViewIntegrationTests.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>"; };
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>"; };
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>"; };
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>"; };
@ -553,6 +556,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
19BA89031F8439A700741C5A /* libRCTBlob.a in Frameworks */,
192F69DA1E8240E2008692C7 /* libRCTAnimation.a in Frameworks */,
14D6D71E1B2222EF001FB087 /* libRCTActionSheet.a in Frameworks */,
14D6D7201B2222EF001FB087 /* libRCTGeolocation.a in Frameworks */,
@ -767,6 +771,7 @@
isa = PBXGroup;
children = (
192F69B51E82409A008692C7 /* RCTAnimationUtilsTests.m */,
19BA88D41F84344F00741C5A /* RCTBlobManagerTests.m */,
192F69B61E82409A008692C7 /* RCTConvert_YGValueTests.m */,
192F69B71E82409A008692C7 /* RCTNativeAnimatedNodesManagerTests.m */,
13B6C1A21C34225900D3FAF5 /* RCTURLUtilsTests.m */,
@ -884,6 +889,13 @@
name = Products;
sourceTree = "<group>";
};
19BA89021F8439A700741C5A /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
272E6B3A1BEA846C001FCF37 /* NativeExampleViews */ = {
isa = PBXGroup;
children = (
@ -1683,6 +1695,7 @@
8385CF041B87479200C6273E /* RCTImageLoaderHelpers.m in Sources */,
192F69B91E82409A008692C7 /* RCTConvert_YGValueTests.m in Sources */,
BC9C03401DC9F1D600B1C635 /* RCTDevMenuTests.m in Sources */,
19BA88D51F84344F00741C5A /* RCTBlobManagerTests.m in Sources */,
68FF44381CF6111500720EFD /* RCTBundleURLProviderTests.m in Sources */,
8385CEF51B873B5C00C6273E /* RCTImageLoaderTests.m in Sources */,
);

View File

@ -97,6 +97,7 @@
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 */; };
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 */; };
39AA31A41DC1DFDC000F7EBB /* RCTUnicodeDecodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 39AA31A31DC1DFDC000F7EBB /* RCTUnicodeDecodeTests.m */; };
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 */; };
8385CEF51B873B5C00C6273E /* RCTImageLoaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8385CEF41B873B5C00C6273E /* RCTImageLoaderTests.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 */; };
C654F14C1EB34D0C000B7A9A /* 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;
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 */ = {
isa = PBXContainerItemProxy;
containerPortal = D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */;
@ -470,6 +488,7 @@
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>"; };
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>"; };
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>"; };
@ -544,6 +563,7 @@
2DD323E61DA2DE3F000FE1B8 /* libRCTNetwork-tvOS.a in Frameworks */,
3D05746D1DE6008900184BB4 /* libRCTPushNotification-tvOS.a in Frameworks */,
2DD323E71DA2DE3F000FE1B8 /* libRCTSettings-tvOS.a in Frameworks */,
ADD01A631E093FA900F6D226 /* libRCTBlob-tvOS.a in Frameworks */,
2DD323E81DA2DE3F000FE1B8 /* libRCTText-tvOS.a in Frameworks */,
2DD323E91DA2DE3F000FE1B8 /* libRCTWebSocket-tvOS.a in Frameworks */,
);
@ -755,6 +775,21 @@
name = Products;
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 */ = {
isa = PBXGroup;
children = (
@ -892,6 +927,15 @@
name = Products;
sourceTree = "<group>";
};
ADBDB9E91DFEC24500ED6528 /* Products */ = {
isa = PBXGroup;
children = (
ADBDB9F11DFEC24500ED6528 /* libRCTBlob.a */,
ADD01A471E093FA100F6D226 /* libRCTBlob-tvOS.a */,
);
name = Products;
sourceTree = "<group>";
};
D85B82921AB6D5CE003F4FE2 /* Products */ = {
isa = PBXGroup;
children = (
@ -1082,6 +1126,10 @@
ProductGroup = 13E5019D1D07A502005F35D8 /* Products */;
ProjectRef = 13E5019C1D07A502005F35D8 /* RCTAnimation.xcodeproj */;
},
{
ProductGroup = ADBDB9E91DFEC24500ED6528 /* Products */;
ProjectRef = ADBDB9E81DFEC24500ED6528 /* RCTBlob.xcodeproj */;
},
{
ProductGroup = 138DEE031B9EDDDB007F4EA5 /* Products */;
ProjectRef = 138DEE021B9EDDDB007F4EA5 /* RCTCameraRoll.xcodeproj */;
@ -1320,6 +1368,13 @@
remoteRef = 2DD323D41DA2DD8B000FE1B8 /* PBXContainerItemProxy */;
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 */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@ -1376,6 +1431,20 @@
remoteRef = 834C36D11AF8DA610019C93C /* PBXContainerItemProxy */;
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 */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;

View File

@ -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

View File

@ -166,7 +166,7 @@ Pod::Spec.new do |s|
s.subspec "RCTBlob" do |ss|
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"
end

View File

@ -14,11 +14,13 @@ rn_android_library(
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/jsr-305:jsr-305"),
react_native_dep("third-party/java/okhttp:okhttp3"),
react_native_dep("third-party/java/okio:okio"),
react_native_target("java/com/facebook/react:react"),
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/module/annotations:annotations"),
react_native_target("java/com/facebook/react/modules/network:network"),
react_native_target("java/com/facebook/react/modules/websocket:websocket"),
],
)

View File

@ -7,9 +7,15 @@
*/
package com.facebook.react.modules.blob;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.support.annotation.Nullable;
import android.webkit.MimeTypeMap;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
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.common.MapBuilder;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.modules.network.NetworkingModule;
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.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import okio.ByteString;
@ReactModule(name = BlobModule.NAME)
@ -35,27 +53,100 @@ public class BlobModule extends ReactContextBaseJavaModule {
private final Map<String, byte[]> mBlobs = new HashMap<>();
protected final WebSocketModule.ContentHandler mContentHandler =
new WebSocketModule.ContentHandler() {
@Override
public void onMessage(String text, WritableMap params) {
params.putString("data", text);
private final WebSocketModule.ContentHandler mWebSocketContentHandler =
new WebSocketModule.ContentHandler() {
@Override
public void onMessage(String text, WritableMap params) {
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");
}
@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");
if (type == null) {
type = "application/octet-stream";
}
};
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) {
super(reactContext);
@ -67,8 +158,7 @@ public class BlobModule extends ReactContextBaseJavaModule {
}
@Override
@Nullable
public Map getConstants() {
public @Nullable Map<String, Object> getConstants() {
// 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.
Resources resources = getReactApplicationContext().getResources();
@ -78,8 +168,8 @@ public class BlobModule extends ReactContextBaseJavaModule {
return null;
}
return MapBuilder.of(
"BLOB_URI_SCHEME", "content", "BLOB_URI_HOST", resources.getString(resourceId));
return MapBuilder.<String, Object>of(
"BLOB_URI_SCHEME", "content", "BLOB_URI_HOST", resources.getString(resourceId));
}
public String store(byte[] data) {
@ -96,8 +186,7 @@ public class BlobModule extends ReactContextBaseJavaModule {
mBlobs.remove(blobId);
}
@Nullable
public byte[] resolve(Uri uri) {
public @Nullable byte[] resolve(Uri uri) {
String blobId = uri.getLastPathSegment();
int offset = 0;
int size = -1;
@ -112,8 +201,7 @@ public class BlobModule extends ReactContextBaseJavaModule {
return resolve(blobId, offset, size);
}
@Nullable
public byte[] resolve(String blobId, int offset, int size) {
public @Nullable byte[] resolve(String blobId, int offset, int size) {
byte[] data = mBlobs.get(blobId);
if (data == null) {
return null;
@ -121,33 +209,101 @@ public class BlobModule extends ReactContextBaseJavaModule {
if (size == -1) {
size = data.length - offset;
}
if (offset > 0) {
if (offset > 0 || size != data.length) {
data = Arrays.copyOfRange(data, offset, offset + size);
}
return data;
}
@Nullable
public byte[] resolve(ReadableMap blob) {
public @Nullable byte[] resolve(ReadableMap blob) {
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() {
return getReactApplicationContext().getNativeModule(WebSocketModule.class);
}
@ReactMethod
public void enableBlobSupport(final int id) {
getWebSocketModule().setContentHandler(id, mContentHandler);
public void addNetworkingHandler() {
NetworkingModule networkingModule = getReactApplicationContext().getNativeModule(NetworkingModule.class);
networkingModule.addUriHandler(mNetworkingUriHandler);
networkingModule.addRequestBodyHandler(mNetworkingRequestBodyHandler);
networkingModule.addResponseHandler(mNetworkingResponseHandler);
}
@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);
}
@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"));
if (data != null) {
@ -160,15 +316,27 @@ public class BlobModule extends ReactContextBaseJavaModule {
@ReactMethod
public void createFromParts(ReadableArray parts, String blobId) {
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++) {
ReadableMap part = parts.getMap(i);
totalBlobSize += part.getInt("size");
partList.add(i, part);
switch (part.getString("type")) {
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);
for (ReadableMap part : partList) {
buffer.put(resolve(part));
for (byte[] bytes : partList) {
buffer.put(bytes);
}
store(buffer.array(), blobId);
}

View File

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

View File

@ -6,20 +6,9 @@
* 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.network;
import javax.annotation.Nullable;
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.net.Uri;
import android.util.Base64;
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.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.Callback;
import okhttp3.CookieJar;
@ -56,6 +56,52 @@ import okio.ByteString;
@ReactModule(name = NetworkingModule.NAME)
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";
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 CookieJarContainer mCookieJarContainer;
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;
/* package */ NetworkingModule(
@ -154,6 +203,34 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
mCookieHandler.destroy();
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
@ -170,13 +247,31 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
final boolean useIncrementalUpdates,
int timeout,
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);
if (requestId != 0) {
requestBuilder.tag(requestId);
}
final RCTDeviceEventEmitter eventEmitter = getEventEmitter();
OkHttpClient.Builder clientBuilder = mClient.newBuilder();
if (!withCredentials) {
@ -237,8 +332,22 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
String contentEncoding = requestHeaders.get(CONTENT_ENCODING_HEADER_NAME);
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) {
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)) {
if (contentType == null) {
ResponseUtil.onRequestError(
@ -360,6 +469,16 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
ResponseBody responseBody = response.body();
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,
// periodically send response data updates to JS.
if (useIncrementalUpdates && responseType.equals("text")) {

View File

@ -9,14 +9,14 @@
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.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
import java.io.IOException;
import java.net.SocketTimeoutException;
/**
* Util methods to send network responses to JS.
*/
@ -72,6 +72,17 @@ public class ResponseUtil {
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(
RCTDeviceEventEmitter eventEmitter,
int requestId,

View File

@ -30,6 +30,7 @@ import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.modules.accessibilityinfo.AccessibilityInfoModule;
import com.facebook.react.modules.appstate.AppStateModule;
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.ImageEditingManager;
import com.facebook.react.modules.camera.ImageStoreManager;
@ -125,6 +126,14 @@ public class MainReactPackage extends LazyReactPackage {
return new BlobModule(context);
}
}),
ModuleSpec.nativeModuleSpec(
FileReaderModule.class,
new Provider<NativeModule>() {
@Override
public NativeModule get() {
return new FileReaderModule(context);
}
}),
ModuleSpec.nativeModuleSpec(
AsyncStorageModule.class,
new Provider<NativeModule>() {

View File

@ -26,6 +26,7 @@ rn_robolectric_test(
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/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/clipboard:clipboard"),
react_native_target("java/com/facebook/react/modules/common:common"),

View File

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