UNPKG

8.01 kBPlain TextView Raw
1/**
2 * Copyright (c) Facebook, Inc. and its affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8#import "RCTBlobManager.h"
9
10#import <mutex>
11
12#import <React/RCTConvert.h>
13#import <React/RCTNetworking.h>
14#import <React/RCTUtils.h>
15#import <React/RCTWebSocketModule.h>
16
17static NSString *const kBlobURIScheme = @"blob";
18
19@interface RCTBlobManager () <RCTNetworkingRequestHandler, RCTNetworkingResponseHandler, RCTWebSocketContentHandler>
20
21@end
22
23@implementation RCTBlobManager
24{
25 // Blobs should be thread safe since they are used from the websocket and networking module,
26 // make sure to use proper locking when accessing this.
27 NSMutableDictionary<NSString *, NSData *> *_blobs;
28 std::mutex _blobsMutex;
29
30 NSOperationQueue *_queue;
31}
32
33RCT_EXPORT_MODULE(BlobModule)
34
35@synthesize bridge = _bridge;
36
37- (void)setBridge:(RCTBridge *)bridge
38{
39 _bridge = bridge;
40
41 std::lock_guard<std::mutex> lock(_blobsMutex);
42 _blobs = [NSMutableDictionary new];
43}
44
45+ (BOOL)requiresMainQueueSetup
46{
47 return NO;
48}
49
50- (NSDictionary<NSString *, id> *)constantsToExport
51{
52 return [self getConstants];
53}
54
55- (NSDictionary<NSString *, id> *)getConstants
56{
57 return @{
58 @"BLOB_URI_SCHEME": kBlobURIScheme,
59 @"BLOB_URI_HOST": [NSNull null],
60 };
61}
62
63- (NSString *)store:(NSData *)data
64{
65 NSString *blobId = [NSUUID UUID].UUIDString;
66 [self store:data withId:blobId];
67 return blobId;
68}
69
70- (void)store:(NSData *)data withId:(NSString *)blobId
71{
72 std::lock_guard<std::mutex> lock(_blobsMutex);
73 _blobs[blobId] = data;
74}
75
76- (NSData *)resolve:(NSDictionary<NSString *, id> *)blob
77{
78 NSString *blobId = [RCTConvert NSString:blob[@"blobId"]];
79 NSNumber *offset = [RCTConvert NSNumber:blob[@"offset"]];
80 NSNumber *size = [RCTConvert NSNumber:blob[@"size"]];
81 return [self resolve:blobId
82 offset:offset ? [offset integerValue] : 0
83 size:size ? [size integerValue] : -1];
84}
85
86- (NSData *)resolve:(NSString *)blobId offset:(NSInteger)offset size:(NSInteger)size
87{
88 NSData *data;
89 {
90 std::lock_guard<std::mutex> lock(_blobsMutex);
91 data = _blobs[blobId];
92 }
93 if (!data) {
94 return nil;
95 }
96 if (offset != 0 || (size != -1 && size != data.length)) {
97 data = [data subdataWithRange:NSMakeRange(offset, size)];
98 }
99 return data;
100}
101
102- (NSData *)resolveURL:(NSURL *)url
103{
104 NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
105
106 NSString *blobId = components.path;
107 NSInteger offset = 0;
108 NSInteger size = -1;
109
110 if (components.queryItems) {
111 for (NSURLQueryItem *queryItem in components.queryItems) {
112 if ([queryItem.name isEqualToString:@"offset"]) {
113 offset = [queryItem.value integerValue];
114 }
115 if ([queryItem.name isEqualToString:@"size"]) {
116 size = [queryItem.value integerValue];
117 }
118 }
119 }
120
121 if (blobId) {
122 return [self resolve:blobId offset:offset size:size];
123 }
124 return nil;
125}
126
127- (void)remove:(NSString *)blobId
128{
129 std::lock_guard<std::mutex> lock(_blobsMutex);
130 [_blobs removeObjectForKey:blobId];
131}
132
133RCT_EXPORT_METHOD(addNetworkingHandler)
134{
135 dispatch_async(_bridge.networking.methodQueue, ^{
136 [self->_bridge.networking addRequestHandler:self];
137 [self->_bridge.networking addResponseHandler:self];
138 });
139}
140
141RCT_EXPORT_METHOD(addWebSocketHandler:(nonnull NSNumber *)socketID)
142{
143 dispatch_async(_bridge.webSocketModule.methodQueue, ^{
144 [self->_bridge.webSocketModule setContentHandler:self forSocketID:socketID];
145 });
146}
147
148RCT_EXPORT_METHOD(removeWebSocketHandler:(nonnull NSNumber *)socketID)
149{
150 dispatch_async(_bridge.webSocketModule.methodQueue, ^{
151 [self->_bridge.webSocketModule setContentHandler:nil forSocketID:socketID];
152 });
153}
154
155// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
156RCT_EXPORT_METHOD(sendOverSocket:(NSDictionary *)blob socketID:(nonnull NSNumber *)socketID)
157{
158 dispatch_async(_bridge.webSocketModule.methodQueue, ^{
159 [self->_bridge.webSocketModule sendData:[self resolve:blob] forSocketID:socketID];
160 });
161}
162
163RCT_EXPORT_METHOD(createFromParts:(NSArray<NSDictionary<NSString *, id> *> *)parts withId:(NSString *)blobId)
164{
165 NSMutableData *data = [NSMutableData new];
166 for (NSDictionary<NSString *, id> *part in parts) {
167 NSString *type = [RCTConvert NSString:part[@"type"]];
168
169 if ([type isEqualToString:@"blob"]) {
170 NSData *partData = [self resolve:part[@"data"]];
171 [data appendData:partData];
172 } else if ([type isEqualToString:@"string"]) {
173 NSData *partData = [[RCTConvert NSString:part[@"data"]] dataUsingEncoding:NSUTF8StringEncoding];
174 [data appendData:partData];
175 } else {
176 [NSException raise:@"Invalid type for blob" format:@"%@ is invalid", type];
177 }
178 }
179 [self store:data withId:blobId];
180}
181
182RCT_EXPORT_METHOD(release:(NSString *)blobId)
183{
184 [self remove:blobId];
185}
186
187#pragma mark - RCTURLRequestHandler methods
188
189- (BOOL)canHandleRequest:(NSURLRequest *)request
190{
191 return [request.URL.scheme caseInsensitiveCompare:kBlobURIScheme] == NSOrderedSame;
192}
193
194- (id)sendRequest:(NSURLRequest *)request withDelegate:(id<RCTURLRequestDelegate>)delegate
195{
196 // Lazy setup
197 if (!_queue) {
198 _queue = [NSOperationQueue new];
199 _queue.maxConcurrentOperationCount = 2;
200 }
201
202 __weak __typeof(self) weakSelf = self;
203 __weak __block NSBlockOperation *weakOp;
204 __block NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
205 __typeof(self) strongSelf = weakSelf;
206 if (!strongSelf) {
207 return;
208 }
209 NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL
210 MIMEType:nil
211 expectedContentLength:-1
212 textEncodingName:nil];
213
214 [delegate URLRequest:weakOp didReceiveResponse:response];
215
216 NSData *data = [strongSelf resolveURL:response.URL];
217 NSError *error;
218 if (data) {
219 [delegate URLRequest:weakOp didReceiveData:data];
220 } else {
221 error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorBadURL userInfo:nil];
222 }
223 [delegate URLRequest:weakOp didCompleteWithError:error];
224 }];
225
226 weakOp = op;
227 [_queue addOperation:op];
228 return op;
229}
230
231- (void)cancelRequest:(NSOperation *)op
232{
233 [op cancel];
234}
235
236#pragma mark - RCTNetworkingRequestHandler methods
237
238// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
239- (BOOL)canHandleNetworkingRequest:(NSDictionary *)data
240{
241 return data[@"blob"] != nil;
242}
243
244// @lint-ignore FBOBJCUNTYPEDCOLLECTION1
245- (NSDictionary *)handleNetworkingRequest:(NSDictionary *)data
246{
247 // @lint-ignore FBOBJCUNTYPEDCOLLECTION1
248 NSDictionary *blob = [RCTConvert NSDictionary:data[@"blob"]];
249
250 NSString *contentType = @"application/octet-stream";
251 NSString *blobType = [RCTConvert NSString:blob[@"type"]];
252 if (blobType != nil && blobType.length > 0) {
253 contentType = blob[@"type"];
254 }
255
256 return @{@"body": [self resolve:blob], @"contentType": contentType};
257}
258
259- (BOOL)canHandleNetworkingResponse:(NSString *)responseType
260{
261 return [responseType isEqualToString:@"blob"];
262}
263
264- (id)handleNetworkingResponse:(NSURLResponse *)response data:(NSData *)data
265{
266 // An empty body will have nil for data, in this case we need to return
267 // an empty blob as per the XMLHttpRequest spec.
268 data = data ?: [NSData new];
269 return @{
270 @"blobId": [self store:data],
271 @"offset": @0,
272 @"size": @(data.length),
273 @"name": RCTNullIfNil([response suggestedFilename]),
274 @"type": RCTNullIfNil([response MIMEType]),
275 };
276}
277
278#pragma mark - RCTWebSocketContentHandler methods
279
280- (id)processWebsocketMessage:(id)message
281 forSocketID:(NSNumber *)socketID
282 withType:(NSString *__autoreleasing _Nonnull *)type
283{
284 if (![message isKindOfClass:[NSData class]]) {
285 *type = @"text";
286 return message;
287 }
288
289 *type = @"blob";
290 return @{
291 @"blobId": [self store:message],
292 @"offset": @0,
293 @"size": @(((NSData *)message).length),
294 };
295}
296
297@end