-
Notifications
You must be signed in to change notification settings - Fork 335
/
VydiaRNFileUploader.m
346 lines (301 loc) · 14 KB
/
VydiaRNFileUploader.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
#import <Foundation/Foundation.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTBridgeModule.h>
#import <Photos/Photos.h>
@interface VydiaRNFileUploader : RCTEventEmitter <RCTBridgeModule, NSURLSessionTaskDelegate>
{
NSMutableDictionary *_responsesData;
}
@end
@implementation VydiaRNFileUploader
RCT_EXPORT_MODULE();
@synthesize bridge = _bridge;
static int uploadId = 0;
static RCTEventEmitter* staticEventEmitter = nil;
static NSString *BACKGROUND_SESSION_ID = @"ReactNativeBackgroundUpload";
NSURLSession *_urlSession = nil;
+ (BOOL)requiresMainQueueSetup {
return NO;
}
-(id) init {
self = [super init];
if (self) {
staticEventEmitter = self;
_responsesData = [NSMutableDictionary dictionary];
}
return self;
}
- (void)_sendEventWithName:(NSString *)eventName body:(id)body {
if (staticEventEmitter == nil)
return;
[staticEventEmitter sendEventWithName:eventName body:body];
}
- (NSArray<NSString *> *)supportedEvents {
return @[
@"RNFileUploader-progress",
@"RNFileUploader-error",
@"RNFileUploader-cancelled",
@"RNFileUploader-completed"
];
}
/*
Gets file information for the path specified. Example valid path is: file:///var/mobile/Containers/Data/Application/3C8A0EFB-A316-45C0-A30A-761BF8CCF2F8/tmp/trim.A5F76017-14E9-4890-907E-36A045AF9436.MOV
Returns an object such as: {mimeType: "video/quicktime", size: 2569900, exists: true, name: "trim.AF9A9225-FC37-416B-A25B-4EDB8275A625.MOV", extension: "MOV"}
*/
RCT_EXPORT_METHOD(getFileInfo:(NSString *)path resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
@try {
NSURL *fileUri = [NSURL URLWithString: path];
NSString *pathWithoutProtocol = [fileUri path];
NSString *name = [fileUri lastPathComponent];
NSString *extension = [name pathExtension];
bool exists = [[NSFileManager defaultManager] fileExistsAtPath:pathWithoutProtocol];
NSMutableDictionary *params = [NSMutableDictionary dictionaryWithObjectsAndKeys: name, @"name", nil];
[params setObject:extension forKey:@"extension"];
[params setObject:[NSNumber numberWithBool:exists] forKey:@"exists"];
if (exists)
{
[params setObject:[self guessMIMETypeFromFileName:name] forKey:@"mimeType"];
NSError* error;
NSDictionary<NSFileAttributeKey, id> *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:pathWithoutProtocol error:&error];
if (error == nil)
{
unsigned long long fileSize = [attributes fileSize];
[params setObject:[NSNumber numberWithLong:fileSize] forKey:@"size"];
}
}
resolve(params);
}
@catch (NSException *exception) {
reject(@"RN Uploader", exception.name, nil);
}
}
/*
Borrowed from http://stackoverflow.com/questions/2439020/wheres-the-iphone-mime-type-database
*/
- (NSString *)guessMIMETypeFromFileName: (NSString *)fileName {
CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[fileName pathExtension], NULL);
CFStringRef MIMEType = UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType);
CFRelease(UTI);
if (!MIMEType) {
return @"application/octet-stream";
}
return (__bridge NSString *)(MIMEType);
}
/*
Utility method to copy a PHAsset file into a local temp file, which can then be uploaded.
*/
- (void)copyAssetToFile: (NSString *)assetUrl completionHandler: (void(^)(NSString *__nullable tempFileUrl, NSError *__nullable error))completionHandler {
NSURL *url = [NSURL URLWithString:assetUrl];
PHAsset *asset = [PHAsset fetchAssetsWithALAssetURLs:@[url] options:nil].lastObject;
if (!asset) {
NSMutableDictionary* details = [NSMutableDictionary dictionary];
[details setValue:@"Asset could not be fetched. Are you missing permissions?" forKey:NSLocalizedDescriptionKey];
completionHandler(nil, [NSError errorWithDomain:@"RNUploader" code:5 userInfo:details]);
return;
}
PHAssetResource *assetResource = [[PHAssetResource assetResourcesForAsset:asset] firstObject];
NSString *pathToWrite = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]];
NSURL *pathUrl = [NSURL fileURLWithPath:pathToWrite];
NSString *fileURI = pathUrl.absoluteString;
PHAssetResourceRequestOptions *options = [PHAssetResourceRequestOptions new];
options.networkAccessAllowed = YES;
[[PHAssetResourceManager defaultManager] writeDataForAssetResource:assetResource toFile:pathUrl options:options completionHandler:^(NSError * _Nullable e) {
if (e == nil) {
completionHandler(fileURI, nil);
}
else {
completionHandler(nil, e);
}
}];
}
/*
* Starts a file upload.
* Options are passed in as the first argument as a js hash:
* {
* url: string. url to post to.
* path: string. path to the file on the device
* headers: hash of name/value header pairs
* }
*
* Returns a promise with the string ID of the upload.
*/
RCT_EXPORT_METHOD(startUpload:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
{
int thisUploadId;
@synchronized(self.class)
{
thisUploadId = uploadId++;
}
NSString *uploadUrl = options[@"url"];
__block NSString *fileURI = options[@"path"];
NSString *method = options[@"method"] ?: @"POST";
NSString *uploadType = options[@"type"] ?: @"raw";
NSString *fieldName = options[@"field"];
NSString *customUploadId = options[@"customUploadId"];
NSString *appGroup = options[@"appGroup"];
NSDictionary *headers = options[@"headers"];
NSDictionary *parameters = options[@"parameters"];
@try {
NSURL *requestUrl = [NSURL URLWithString: uploadUrl];
if (requestUrl == nil) {
return reject(@"RN Uploader", @"URL not compliant with RFC 2396", nil);
}
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestUrl];
[request setHTTPMethod: method];
[headers enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull val, BOOL * _Nonnull stop) {
if ([val respondsToSelector:@selector(stringValue)]) {
val = [val stringValue];
}
if ([val isKindOfClass:[NSString class]]) {
[request setValue:val forHTTPHeaderField:key];
}
}];
// asset library files have to be copied over to a temp file. they can't be uploaded directly
if ([fileURI hasPrefix:@"assets-library"]) {
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
[self copyAssetToFile:fileURI completionHandler:^(NSString * _Nullable tempFileUrl, NSError * _Nullable error) {
if (error) {
dispatch_group_leave(group);
reject(@"RN Uploader", @"Asset could not be copied to temp file.", nil);
return;
}
fileURI = tempFileUrl;
dispatch_group_leave(group);
}];
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
}
NSURLSessionDataTask *uploadTask;
if ([uploadType isEqualToString:@"multipart"]) {
NSString *uuidStr = [[NSUUID UUID] UUIDString];
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", uuidStr] forHTTPHeaderField:@"Content-Type"];
NSData *httpBody = [self createBodyWithBoundary:uuidStr path:fileURI parameters: parameters fieldName:fieldName];
[request setHTTPBody: httpBody];
uploadTask = [[self urlSession: appGroup] uploadTaskWithStreamedRequest:request];
} else {
if (parameters.count > 0) {
reject(@"RN Uploader", @"Parameters supported only in multipart type", nil);
return;
}
uploadTask = [[self urlSession: appGroup] uploadTaskWithRequest:request fromFile:[NSURL URLWithString: fileURI]];
}
uploadTask.taskDescription = customUploadId ? customUploadId : [NSString stringWithFormat:@"%i", thisUploadId];
[uploadTask resume];
resolve(uploadTask.taskDescription);
}
@catch (NSException *exception) {
reject(@"RN Uploader", exception.name, nil);
}
}
/*
* Cancels file upload
* Accepts upload ID as a first argument, this upload will be cancelled
* Event "cancelled" will be fired when upload is cancelled.
*/
RCT_EXPORT_METHOD(cancelUpload: (NSString *)cancelUploadId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
[_urlSession getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
for (NSURLSessionTask *uploadTask in uploadTasks) {
if ([uploadTask.taskDescription isEqualToString:cancelUploadId]){
// == checks if references are equal, while isEqualToString checks the string value
[uploadTask cancel];
}
}
}];
resolve([NSNumber numberWithBool:YES]);
}
- (NSData *)createBodyWithBoundary:(NSString *)boundary
path:(NSString *)path
parameters:(NSDictionary *)parameters
fieldName:(NSString *)fieldName {
NSMutableData *httpBody = [NSMutableData data];
// resolve path
NSURL *fileUri = [NSURL URLWithString: path];
NSString *pathWithoutProtocol = [fileUri path];
NSData *data = [[NSFileManager defaultManager] contentsAtPath:pathWithoutProtocol];
NSString *filename = [path lastPathComponent];
NSString *mimetype = [self guessMIMETypeFromFileName:path];
[parameters enumerateKeysAndObjectsUsingBlock:^(NSString *parameterKey, NSString *parameterValue, BOOL *stop) {
[httpBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[httpBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", parameterKey] dataUsingEncoding:NSUTF8StringEncoding]];
[httpBody appendData:[[NSString stringWithFormat:@"%@\r\n", parameterValue] dataUsingEncoding:NSUTF8StringEncoding]];
}];
[httpBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
[httpBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", fieldName, filename] dataUsingEncoding:NSUTF8StringEncoding]];
[httpBody appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", mimetype] dataUsingEncoding:NSUTF8StringEncoding]];
[httpBody appendData:data];
[httpBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
[httpBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
return httpBody;
}
- (NSURLSession *)urlSession: (NSString *) groupId {
if (_urlSession == nil) {
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:BACKGROUND_SESSION_ID];
if (groupId != nil && ![groupId isEqualToString:@""]) {
sessionConfiguration.sharedContainerIdentifier = groupId;
}
_urlSession = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];
}
return _urlSession;
}
#pragma NSURLSessionTaskDelegate
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
NSMutableDictionary *data = [NSMutableDictionary dictionaryWithObjectsAndKeys:task.taskDescription, @"id", nil];
NSURLSessionDataTask *uploadTask = (NSURLSessionDataTask *)task;
NSHTTPURLResponse *response = (NSHTTPURLResponse *)uploadTask.response;
if (response != nil)
{
[data setObject:[NSNumber numberWithInteger:response.statusCode] forKey:@"responseCode"];
}
//Add data that was collected earlier by the didReceiveData method
NSMutableData *responseData = _responsesData[@(task.taskIdentifier)];
if (responseData) {
[_responsesData removeObjectForKey:@(task.taskIdentifier)];
NSString *response = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
[data setObject:response forKey:@"responseBody"];
} else {
[data setObject:[NSNull null] forKey:@"responseBody"];
}
if (error == nil)
{
[self _sendEventWithName:@"RNFileUploader-completed" body:data];
}
else
{
[data setObject:error.localizedDescription forKey:@"error"];
if (error.code == NSURLErrorCancelled) {
[self _sendEventWithName:@"RNFileUploader-cancelled" body:data];
} else {
[self _sendEventWithName:@"RNFileUploader-error" body:data];
}
}
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
float progress = -1;
if (totalBytesExpectedToSend > 0) //see documentation. For unknown size it's -1 (NSURLSessionTransferSizeUnknown)
{
progress = 100.0 * (float)totalBytesSent / (float)totalBytesExpectedToSend;
}
[self _sendEventWithName:@"RNFileUploader-progress" body:@{ @"id": task.taskDescription, @"progress": [NSNumber numberWithFloat:progress] }];
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
if (!data.length) {
return;
}
//Hold returned data so it can be picked up by the didCompleteWithError method later
NSMutableData *responseData = _responsesData[@(dataTask.taskIdentifier)];
if (!responseData) {
responseData = [NSMutableData dataWithData:data];
_responsesData[@(dataTask.taskIdentifier)] = responseData;
} else {
[responseData appendData:data];
}
}
@end