2018-01-26 17:06:14 +00:00
|
|
|
/**
|
|
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
|
|
*
|
2018-02-17 02:24:55 +00:00
|
|
|
* This source code is licensed under the MIT license found in the
|
|
|
|
* LICENSE file in the root directory of this source tree.
|
2018-01-26 17:06:14 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
#import "RCTBlobManager.h"
|
|
|
|
|
|
|
|
#import <mutex>
|
|
|
|
|
|
|
|
#import <React/RCTConvert.h>
|
|
|
|
#import <React/RCTNetworking.h>
|
2018-02-09 03:25:42 +00:00
|
|
|
#import <React/RCTUtils.h>
|
2018-01-26 17:06:14 +00:00
|
|
|
#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
|
|
|
|
{
|
2018-03-27 17:34:09 +00:00
|
|
|
// An empty body will have nil for data, in this case we need to return
|
|
|
|
// an empty blob as per the XMLHttpRequest spec.
|
|
|
|
data = data ?: [NSData new];
|
2018-01-26 17:06:14 +00:00
|
|
|
return @{
|
|
|
|
@"blobId": [self store:data],
|
|
|
|
@"offset": @0,
|
|
|
|
@"size": @(data.length),
|
2018-02-09 03:25:42 +00:00
|
|
|
@"name": RCTNullIfNil([response suggestedFilename]),
|
|
|
|
@"type": RCTNullIfNil([response MIMEType]),
|
2018-01-26 17:06:14 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
#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
|