1// Copyright 2016-present 650 Industries. All rights reserved. 2 3#import <ExpoModulesCore/EXModuleRegistry.h> 4 5#import <ExpoFileSystem/EXFileSystem.h> 6 7#import <CommonCrypto/CommonDigest.h> 8#import <MobileCoreServices/MobileCoreServices.h> 9 10#import <ExpoFileSystem/EXFileSystemLocalFileHandler.h> 11#import <ExpoFileSystem/EXFileSystemAssetLibraryHandler.h> 12 13#import <ExpoModulesCore/EXFileSystemInterface.h> 14#import <ExpoModulesCore/EXFilePermissionModuleInterface.h> 15 16#import <ExpoModulesCore/EXEventEmitterService.h> 17 18#import <ExpoFileSystem/EXTaskHandlersManager.h> 19#import <ExpoFileSystem/EXSessionTaskDispatcher.h> 20#import <ExpoFileSystem/EXSessionDownloadTaskDelegate.h> 21#import <ExpoFileSystem/EXSessionResumableDownloadTaskDelegate.h> 22#import <ExpoFileSystem/EXSessionUploadTaskDelegate.h> 23#import <ExpoFileSystem/EXSessionCancelableUploadTaskDelegate.h> 24 25NSString * const EXDownloadProgressEventName = @"expo-file-system.downloadProgress"; 26NSString * const EXUploadProgressEventName = @"expo-file-system.uploadProgress"; 27 28typedef NS_ENUM(NSInteger, EXFileSystemSessionType) { 29 EXFileSystemBackgroundSession = 0, 30 EXFileSystemForegroundSession = 1, 31}; 32 33typedef NS_ENUM(NSInteger, EXFileSystemUploadType) { 34 EXFileSystemInvalidType = -1, 35 EXFileSystemBinaryContent = 0, 36 EXFileSystemMultipart = 1, 37}; 38 39@interface EXFileSystem () 40 41@property (nonatomic, strong) NSURLSession *backgroundSession; 42@property (nonatomic, strong) NSURLSession *foregroundSession; 43@property (nonatomic, strong) EXSessionTaskDispatcher *sessionTaskDispatcher; 44@property (nonatomic, strong) EXTaskHandlersManager *taskHandlersManager; 45@property (nonatomic, weak) EXModuleRegistry *moduleRegistry; 46@property (nonatomic, weak) id<EXEventEmitterService> eventEmitter; 47@property (nonatomic, strong) NSString *documentDirectory; 48@property (nonatomic, strong) NSString *cachesDirectory; 49@property (nonatomic, strong) NSString *bundleDirectory; 50 51@end 52 53@implementation EXFileSystem 54 55EX_REGISTER_MODULE(); 56 57+ (const NSString *)exportedModuleName 58{ 59 return @"ExponentFileSystem"; 60} 61 62+ (const NSArray<Protocol *> *)exportedInterfaces 63{ 64 return @[@protocol(EXFileSystemInterface)]; 65} 66 67- (instancetype)initWithDocumentDirectory:(NSString *)documentDirectory cachesDirectory:(NSString *)cachesDirectory bundleDirectory:(NSString *)bundleDirectory 68{ 69 if (self = [super init]) { 70 _documentDirectory = documentDirectory; 71 _cachesDirectory = cachesDirectory; 72 _bundleDirectory = bundleDirectory; 73 74 _taskHandlersManager = [EXTaskHandlersManager new]; 75 76 [EXFileSystem ensureDirExistsWithPath:_documentDirectory]; 77 [EXFileSystem ensureDirExistsWithPath:_cachesDirectory]; 78 } 79 return self; 80} 81 82- (instancetype)init 83{ 84 NSArray<NSString *> *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 85 NSString *documentDirectory = [documentPaths objectAtIndex:0]; 86 87 NSArray<NSString *> *cachesPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); 88 NSString *cacheDirectory = [cachesPaths objectAtIndex:0]; 89 90 return [self initWithDocumentDirectory:documentDirectory 91 cachesDirectory:cacheDirectory 92 bundleDirectory:[NSBundle mainBundle].bundlePath]; 93} 94 95- (void)setModuleRegistry:(EXModuleRegistry *)moduleRegistry 96{ 97 _moduleRegistry = moduleRegistry; 98 _eventEmitter = [_moduleRegistry getModuleImplementingProtocol:@protocol(EXEventEmitterService)]; 99 100 _sessionTaskDispatcher = [[EXSessionTaskDispatcher alloc] initWithSessionHandler:[moduleRegistry getSingletonModuleForName:@"SessionHandler"]]; 101 _backgroundSession = [self _createSession:EXFileSystemBackgroundSession delegate:_sessionTaskDispatcher]; 102 _foregroundSession = [self _createSession:EXFileSystemForegroundSession delegate:_sessionTaskDispatcher]; 103} 104 105- (NSDictionary *)constantsToExport 106{ 107 return @{ 108 @"documentDirectory": _documentDirectory ? [NSURL fileURLWithPath:_documentDirectory].absoluteString : [NSNull null], 109 @"cacheDirectory": _cachesDirectory ? [NSURL fileURLWithPath:_cachesDirectory].absoluteString : [NSNull null], 110 @"bundleDirectory": _bundleDirectory ? [NSURL fileURLWithPath:_bundleDirectory].absoluteString : [NSNull null] 111 }; 112} 113 114- (NSArray<NSString *> *)supportedEvents 115{ 116 return @[EXDownloadProgressEventName, EXUploadProgressEventName]; 117} 118 119- (void)startObserving { 120 121} 122 123 124- (void)stopObserving { 125 126} 127 128- (void)dealloc 129{ 130 [_sessionTaskDispatcher deactivate]; 131 [_backgroundSession invalidateAndCancel]; 132 [_foregroundSession invalidateAndCancel]; 133} 134 135- (NSDictionary *)encodingMap 136{ 137 /* 138 TODO:Bacon: match node.js fs 139 https://github.com/nodejs/node/blob/master/lib/buffer.js 140 ascii 141 base64 142 binary 143 hex 144 ucs2/ucs-2 145 utf16le/utf-16le 146 utf8/utf-8 147 latin1 (ISO8859-1, only in node 6.4.0+) 148 */ 149 return @{ 150 @"ascii": @(NSASCIIStringEncoding), 151 @"nextstep": @(NSNEXTSTEPStringEncoding), 152 @"japaneseeuc": @(NSJapaneseEUCStringEncoding), 153 @"utf8": @(NSUTF8StringEncoding), 154 @"isolatin1": @(NSISOLatin1StringEncoding), 155 @"symbol": @(NSSymbolStringEncoding), 156 @"nonlossyascii": @(NSNonLossyASCIIStringEncoding), 157 @"shiftjis": @(NSShiftJISStringEncoding), 158 @"isolatin2": @(NSISOLatin2StringEncoding), 159 @"unicode": @(NSUnicodeStringEncoding), 160 @"windowscp1251": @(NSWindowsCP1251StringEncoding), 161 @"windowscp1252": @(NSWindowsCP1252StringEncoding), 162 @"windowscp1253": @(NSWindowsCP1253StringEncoding), 163 @"windowscp1254": @(NSWindowsCP1254StringEncoding), 164 @"windowscp1250": @(NSWindowsCP1250StringEncoding), 165 @"iso2022jp": @(NSISO2022JPStringEncoding), 166 @"macosroman": @(NSMacOSRomanStringEncoding), 167 @"utf16": @(NSUTF16StringEncoding), 168 @"utf16bigendian": @(NSUTF16BigEndianStringEncoding), 169 @"utf16littleendian": @(NSUTF16LittleEndianStringEncoding), 170 @"utf32": @(NSUTF32StringEncoding), 171 @"utf32bigendian": @(NSUTF32BigEndianStringEncoding), 172 @"utf32littleendian": @(NSUTF32LittleEndianStringEncoding), 173 }; 174} 175 176EX_EXPORT_METHOD_AS(getInfoAsync, 177 getInfoAsyncWithURI:(NSString *)uriString 178 withOptions:(NSDictionary *)options 179 resolver:(EXPromiseResolveBlock)resolve 180 rejecter:(EXPromiseRejectBlock)reject) 181{ 182 NSURL *uri = [self percentEncodedURLFromURIString:uriString]; 183 // no scheme provided in uri, handle as a local path and add 'file://' scheme 184 if (!uri.scheme) { 185 uri = [NSURL fileURLWithPath:uriString isDirectory:false]; 186 } 187 if (!([self permissionsForURI:uri] & EXFileSystemPermissionRead)) { 188 reject(@"ERR_FILESYSTEM_NO_PERMISSIONS", 189 [NSString stringWithFormat:@"File '%@' isn't readable.", uri], 190 nil); 191 return; 192 } 193 194 if ([uri.scheme isEqualToString:@"file"]) { 195 [EXFileSystemLocalFileHandler getInfoForFile:uri withOptions:options resolver:resolve rejecter:reject]; 196 } else if ([uri.scheme isEqualToString:@"assets-library"] || [uri.scheme isEqualToString:@"ph"]) { 197 [EXFileSystemAssetLibraryHandler getInfoForFile:uri withOptions:options resolver:resolve rejecter:reject]; 198 } else { 199 reject(@"ERR_FILESYSTEM_INVALID_URI", 200 [NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri], 201 nil); 202 } 203} 204 205EX_EXPORT_METHOD_AS(readAsStringAsync, 206 readAsStringAsyncWithURI:(NSString *)uriString 207 withOptions:(NSDictionary *)options 208 resolver:(EXPromiseResolveBlock)resolve 209 rejecter:(EXPromiseRejectBlock)reject) 210{ 211 NSURL *uri = [self percentEncodedURLFromURIString:uriString]; 212 // no scheme provided in uri, handle as a local path and add 'file://' scheme 213 if (!uri.scheme) { 214 uri = [NSURL fileURLWithPath:uriString isDirectory:false]; 215 } 216 if (!([self permissionsForURI:uri] & EXFileSystemPermissionRead)) { 217 reject(@"ERR_FILESYSTEM_NO_PERMISSIONS", 218 [NSString stringWithFormat:@"File '%@' isn't readable.", uri], 219 nil); 220 return; 221 } 222 223 if ([uri.scheme isEqualToString:@"file"]) { 224 NSString *encodingType = @"utf8"; 225 if (options[@"encoding"] && [options[@"encoding"] isKindOfClass:[NSString class]]) { 226 encodingType = [options[@"encoding"] lowercaseString]; 227 } 228 if ([encodingType isEqualToString:@"base64"]) { 229 NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:uri.path]; 230 if (file == nil) { 231 reject(@"ERR_FILESYSTEM_CANNOT_READ_FILE", 232 [NSString stringWithFormat:@"File '%@' could not be read.", uri.path], 233 nil); 234 return; 235 } 236 // position and length are used as a cursor/paging system. 237 if ([options[@"position"] isKindOfClass:[NSNumber class]]) { 238 [file seekToFileOffset:[options[@"position"] intValue]]; 239 } 240 241 NSData *data; 242 if ([options[@"length"] isKindOfClass:[NSNumber class]]) { 243 data = [file readDataOfLength:[options[@"length"] intValue]]; 244 } else { 245 data = [file readDataToEndOfFile]; 246 } 247 resolve([data base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]); 248 } else { 249 NSUInteger encoding = NSUTF8StringEncoding; 250 id possibleEncoding = [[self encodingMap] valueForKey:encodingType]; 251 if (possibleEncoding != nil) { 252 encoding = [possibleEncoding integerValue]; 253 } 254 NSError *error; 255 NSString *string = [NSString stringWithContentsOfFile:uri.path encoding:encoding error:&error]; 256 if (string) { 257 resolve(string); 258 } else { 259 reject(@"ERR_FILESYSTEM_CANNOT_READ_FILE", 260 [NSString stringWithFormat:@"File '%@' could not be read.", uri], 261 error); 262 } 263 } 264 } else { 265 reject(@"ERR_FILESYSTEM_INVALID_URI", 266 [NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri], 267 nil); 268 } 269} 270 271EX_EXPORT_METHOD_AS(writeAsStringAsync, 272 writeAsStringAsyncWithURI:(NSString *)uriString 273 withString:(NSString *)string 274 withOptions:(NSDictionary *)options 275 resolver:(EXPromiseResolveBlock)resolve 276 rejecter:(EXPromiseRejectBlock)reject) 277{ 278 NSURL *uri = [self percentEncodedURLFromURIString:uriString]; 279 if (!([self permissionsForURI:uri] & EXFileSystemPermissionWrite)) { 280 reject(@"ERR_FILESYSTEM_NO_PERMISSIONS", 281 [NSString stringWithFormat:@"File '%@' isn't writable.", uri], 282 nil); 283 return; 284 } 285 286 if ([uri.scheme isEqualToString:@"file"]) { 287 NSString *encodingType = @"utf8"; 288 if ([options[@"encoding"] isKindOfClass:[NSString class]]) { 289 encodingType = [options[@"encoding"] lowercaseString]; 290 } 291 if ([encodingType isEqualToString:@"base64"]) { 292 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 293 NSData *imageData = [[NSData alloc] initWithBase64EncodedString:string options:NSDataBase64DecodingIgnoreUnknownCharacters]; 294 if (imageData) { 295 // TODO:Bacon: Should we surface `attributes`? 296 if ([[NSFileManager defaultManager] createFileAtPath:uri.path contents:imageData attributes:nil]) { 297 resolve([NSNull null]); 298 } else { 299 return reject(@"ERR_FILESYSTEM_UNKNOWN_FILE", 300 [NSString stringWithFormat:@"No such file or directory '%@'", uri.path], 301 nil); 302 } 303 } else { 304 reject(@"ERR_FILESYSTEM_INVALID_FORMAT", 305 @"Failed to parse base64 string.", 306 nil); 307 } 308 }); 309 } else { 310 NSUInteger encoding = NSUTF8StringEncoding; 311 id possibleEncoding = [[self encodingMap] valueForKey:encodingType]; 312 if (possibleEncoding != nil) { 313 encoding = [possibleEncoding integerValue]; 314 } 315 316 NSError *error; 317 if ([string writeToFile:uri.path atomically:YES encoding:encoding error:&error]) { 318 resolve([NSNull null]); 319 } else { 320 reject(@"ERR_FILESYSTEM_CANNOT_WRITE_TO_FILE", 321 [NSString stringWithFormat:@"File '%@' could not be written.", uri], 322 error); 323 } 324 } 325 } else { 326 reject(@"ERR_FILESYSTEM_INVALID_URI", 327 [NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri], 328 nil); 329 } 330} 331 332EX_EXPORT_METHOD_AS(deleteAsync, 333 deleteAsyncWithURI:(NSString *)uriString 334 withOptions:(NSDictionary *)options 335 resolver:(EXPromiseResolveBlock)resolve 336 rejecter:(EXPromiseRejectBlock)reject) 337{ 338 NSURL *uri = [self percentEncodedURLFromURIString:uriString]; 339 if (!([self permissionsForURI:[uri URLByAppendingPathComponent:@".."]] & EXFileSystemPermissionWrite)) { 340 reject(@"ERR_FILESYSTEM_NO_PERMISSIONS", 341 [NSString stringWithFormat:@"Location '%@' isn't deletable.", uri], 342 nil); 343 return; 344 } 345 346 if ([uri.scheme isEqualToString:@"file"]) { 347 NSString *path = uri.path; 348 if ([self _checkIfFileExists:path]) { 349 NSError *error; 350 if ([[NSFileManager defaultManager] removeItemAtPath:path error:&error]) { 351 resolve([NSNull null]); 352 } else { 353 reject(@"ERR_FILESYSTEM_CANNOT_DELETE_FILE", 354 [NSString stringWithFormat:@"File '%@' could not be deleted.", uri], 355 error); 356 } 357 } else { 358 if (options[@"idempotent"]) { 359 resolve([NSNull null]); 360 } else { 361 reject(@"ERR_FILESYSTEM_CANNOT_FIND_FILE", 362 [NSString stringWithFormat:@"File '%@' could not be deleted because it could not be found.", uri], 363 nil); 364 } 365 } 366 } else { 367 reject(@"ERR_FILESYSTEM_INVALID_URI", 368 [NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri], 369 nil); 370 } 371} 372 373EX_EXPORT_METHOD_AS(moveAsync, 374 moveAsyncWithOptions:(NSDictionary *)options 375 resolver:(EXPromiseResolveBlock)resolve 376 rejecter:(EXPromiseRejectBlock)reject) 377{ 378 NSURL *from = [NSURL URLWithString:options[@"from"]]; 379 if (!from) { 380 reject(@"ERR_FILESYSTEM_MISSING_PARAMETER", @"Need a `from` location.", nil); 381 return; 382 } 383 if (!([self permissionsForURI:[from URLByAppendingPathComponent:@".."]] & EXFileSystemPermissionWrite)) { 384 reject(@"ERR_FILESYSTEM_NO_PERMISSIONS", 385 [NSString stringWithFormat:@"Location '%@' isn't movable.", from], 386 nil); 387 return; 388 } 389 NSURL *to = [NSURL URLWithString:options[@"to"]]; 390 if (!to) { 391 reject(@"ERR_FILESYSTEM_MISSING_PARAMETER", @"Need a `to` location.", nil); 392 return; 393 } 394 if (!([self permissionsForURI:to] & EXFileSystemPermissionWrite)) { 395 reject(@"ERR_FILESYSTEM_NO_PERMISSIONS", 396 [NSString stringWithFormat:@"File '%@' isn't writable.", to], 397 nil); 398 return; 399 } 400 401 // NOTE: The destination-delete and the move should happen atomically, but we hope for the best for now 402 if ([from.scheme isEqualToString:@"file"]) { 403 NSString *fromPath = [from.path stringByStandardizingPath]; 404 NSString *toPath = [to.path stringByStandardizingPath]; 405 NSError *error; 406 if ([self _checkIfFileExists:toPath]) { 407 if (![[NSFileManager defaultManager] removeItemAtPath:toPath error:&error]) { 408 reject(@"ERR_FILESYSTEM_CANNOT_MOVE_FILE", 409 [NSString stringWithFormat:@"File '%@' could not be moved to '%@' because a file already exists at " 410 "the destination and could not be deleted.", from, to], 411 error); 412 return; 413 } 414 } 415 if ([[NSFileManager defaultManager] moveItemAtPath:fromPath toPath:toPath error:&error]) { 416 resolve([NSNull null]); 417 } else { 418 reject(@"ERR_FILESYSTEM_CANNOT_MOVE_FILE", 419 [NSString stringWithFormat:@"File '%@' could not be moved to '%@'.", from, to], 420 error); 421 } 422 } else { 423 reject(@"ERR_FILESYSTEM_INVALID_URI", 424 [NSString stringWithFormat:@"Unsupported URI scheme for '%@'", from], 425 nil); 426 } 427} 428 429EX_EXPORT_METHOD_AS(copyAsync, 430 copyAsyncWithOptions:(NSDictionary *)options 431 resolver:(EXPromiseResolveBlock)resolve 432 rejecter:(EXPromiseRejectBlock)reject) 433{ 434 NSURL *from = [NSURL URLWithString:options[@"from"]]; 435 if (!from) { 436 reject(@"ERR_FILESYSTEM_MISSING_PARAMETER", @"Need a `from` location.", nil); 437 return; 438 } 439 if (!([self permissionsForURI:from] & EXFileSystemPermissionRead)) { 440 reject(@"ERR_FILESYSTEM_NO_PERMISSIONS", 441 [NSString stringWithFormat:@"File '%@' isn't readable.", from], 442 nil); 443 return; 444 } 445 NSURL *to = [NSURL URLWithString:options[@"to"]]; 446 if (!to) { 447 reject(@"ERR_FILESYSTEM_MISSING_PARAMETER", @"Need a `to` location.", nil); 448 return; 449 } 450 if (!([self permissionsForURI:to] & EXFileSystemPermissionWrite)) { 451 reject(@"ERR_FILESYSTEM_NO_PERMISSIONS", 452 [NSString stringWithFormat:@"File '%@' isn't writable.", to], 453 nil); 454 return; 455 } 456 457 if ([from.scheme isEqualToString:@"file"]) { 458 [EXFileSystemLocalFileHandler copyFrom:from to:to resolver:resolve rejecter:reject]; 459 } else if ([from.scheme isEqualToString:@"assets-library"] || [from.scheme isEqualToString:@"ph"]) { 460 [EXFileSystemAssetLibraryHandler copyFrom:from to:to resolver:resolve rejecter:reject]; 461 } else { 462 reject(@"ERR_FILESYSTEM_INVALID_URI", 463 [NSString stringWithFormat:@"Unsupported URI scheme for '%@'", from], 464 nil); 465 } 466} 467 468EX_EXPORT_METHOD_AS(makeDirectoryAsync, 469 makeDirectoryAsyncWithURI:(NSString *)uriString 470 withOptions:(NSDictionary *)options 471 resolver:(EXPromiseResolveBlock)resolve 472 rejecter:(EXPromiseRejectBlock)reject) 473{ 474 475 NSURL *uri = [self percentEncodedURLFromURIString:uriString]; 476 if (!([self permissionsForURI:uri] & EXFileSystemPermissionWrite)) { 477 reject(@"ERR_FILESYSTEM_NO_PERMISSIONS", 478 [NSString stringWithFormat:@"Directory '%@' could not be created because the location isn't writable.", uri], 479 nil); 480 return; 481 } 482 483 if ([uri.scheme isEqualToString:@"file"]) { 484 NSError *error; 485 if ([[NSFileManager defaultManager] createDirectoryAtPath:uri.path 486 withIntermediateDirectories:[options[@"intermediates"] boolValue] 487 attributes:nil 488 error:&error]) { 489 resolve([NSNull null]); 490 } else { 491 reject(@"ERR_FILESYSTEM_CANNOT_CREATE_DIRECTORY", 492 [NSString stringWithFormat:@"Directory '%@' could not be created.", uri], 493 error); 494 } 495 } else { 496 reject(@"ERR_FILESYSTEM_INVALID_URI", 497 [NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri], 498 nil); 499 } 500} 501 502EX_EXPORT_METHOD_AS(readDirectoryAsync, 503 readDirectoryAsyncWithURI:(NSString *)uriString 504 resolver:(EXPromiseResolveBlock)resolve 505 rejecter:(EXPromiseRejectBlock)reject) 506{ 507 NSURL *uri = [self percentEncodedURLFromURIString:uriString]; 508 if (!([self permissionsForURI:uri] & EXFileSystemPermissionRead)) { 509 reject(@"ERR_FILESYSTEM_NO_PERMISSIONS", 510 [NSString stringWithFormat:@"Location '%@' isn't readable.", uri], 511 nil); 512 return; 513 } 514 515 if ([uri.scheme isEqualToString:@"file"]) { 516 NSError *error; 517 NSArray<NSString *> *children = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:uri.path error:&error]; 518 if (children) { 519 resolve(children); 520 } else { 521 reject(@"ERR_FILESYSTEM_CANNOT_READ_DIRECTORY", 522 [NSString stringWithFormat:@"Directory '%@' could not be read.", uri], 523 error); 524 } 525 } else { 526 reject(@"ERR_FILESYSTEM_INVALID_URI", 527 [NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri], 528 nil); 529 } 530} 531 532EX_EXPORT_METHOD_AS(downloadAsync, 533 downloadAsyncWithUrl:(NSString *)urlString 534 localURI:(NSString *)localUriString 535 options:(NSDictionary *)options 536 resolver:(EXPromiseResolveBlock)resolve 537 rejecter:(EXPromiseRejectBlock)reject) 538{ 539 NSURL *url = [NSURL URLWithString:urlString]; 540 NSURL *localUri = [self percentEncodedURLFromURIString:localUriString]; 541 if (!([self checkIfFileDirExists:localUri.path])) { 542 reject(@"ERR_FILESYSTEM_WRONG_DESTINATION", 543 [NSString stringWithFormat:@"Directory for '%@' doesn't exist. Please make sure directory '%@' exists before calling downloadAsync.", localUriString, [localUri.path stringByDeletingLastPathComponent]], 544 nil); 545 return; 546 } 547 if (!([self permissionsForURI:localUri] & EXFileSystemPermissionWrite)) { 548 reject(@"ERR_FILESYSTEM_NO_PERMISSIONS", 549 [NSString stringWithFormat:@"File '%@' isn't writable.", localUri], 550 nil); 551 return; 552 } 553 if (![self _checkHeadersDictionary:options[@"headers"]]) { 554 reject(@"ERR_FILESYSTEM_INVALID_HEADERS", 555 @"Invalid headers dictionary. Keys and values should be strings.", 556 nil); 557 return; 558 } 559 560 NSURLSession *session = [self _sessionForType:[options[@"sessionType"] intValue]]; 561 if (!session) { 562 reject(@"ERR_FILESYSTEM_INVALID_SESSION_TYPE", 563 [NSString stringWithFormat:@"Invalid session type: '%@'", options[@"sessionType"]], 564 nil); 565 return; 566 } 567 568 NSURLRequest *request = [self _createRequest:url headers:options[@"headers"]]; 569 NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:request]; 570 EXSessionTaskDelegate *taskDelegate = [[EXSessionDownloadTaskDelegate alloc] initWithResolve:resolve 571 reject:reject 572 localUrl:localUri 573 shouldCalculateMd5:[options[@"md5"] boolValue]]; 574 [_sessionTaskDispatcher registerTaskDelegate:taskDelegate forTask:task]; 575 [task resume]; 576} 577 578EX_EXPORT_METHOD_AS(uploadAsync, 579 uploadAsync:(NSString *)urlString 580 localURI:(NSString *)fileUriString 581 options:(NSDictionary *)options 582 resolver:(EXPromiseResolveBlock)resolve 583 rejecter:(EXPromiseRejectBlock)reject) 584{ 585 NSURLSessionUploadTask *task = [self createUploadTask:urlString localURI:fileUriString options:options rejecter:reject]; 586 if (!task) { 587 return; 588 } 589 590 EXSessionTaskDelegate *taskDelegate = [[EXSessionUploadTaskDelegate alloc] initWithResolve:resolve reject:reject]; 591 [_sessionTaskDispatcher registerTaskDelegate:taskDelegate forTask:task]; 592 [task resume]; 593} 594 595EX_EXPORT_METHOD_AS(uploadTaskStartAsync, 596 uploadTaskStartAsync:(NSString *)urlString 597 localURI:(NSString *)fileUriString 598 uuid:(NSString *)uuid 599 options:(NSDictionary *)options 600 resolver:(EXPromiseResolveBlock)resolve 601 rejecter:(EXPromiseRejectBlock)reject) 602{ 603 NSURLSessionUploadTask *task = [self createUploadTask:urlString localURI:fileUriString options:options rejecter:reject]; 604 if (!task) { 605 return; 606 } 607 608 EX_WEAKIFY(self); 609 EXUploadDelegateOnSendCallback onSend = ^(NSURLSessionUploadTask *task, int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) { 610 EX_ENSURE_STRONGIFY(self); 611 [self sendEventWithName:EXUploadProgressEventName 612 body:@{ 613 @"uuid": uuid, 614 @"data": @{ 615 @"totalBytesSent": @(totalBytesSent), 616 @"totalBytesExpectedToSend": @(totalBytesExpectedToSend), 617 }, 618 }]; 619 }; 620 621 EXSessionTaskDelegate *taskDelegate = [[EXSessionCancelableUploadTaskDelegate alloc] initWithResolve:resolve 622 reject:reject 623 onSendCallback:onSend 624 resumableManager:_taskHandlersManager 625 uuid:uuid]; 626 627 [_sessionTaskDispatcher registerTaskDelegate:taskDelegate forTask:task]; 628 [_taskHandlersManager registerTask:task uuid:uuid]; 629 [task resume]; 630} 631 632- (NSURLSessionUploadTask * _Nullable)createUploadTask:(NSString *)urlString 633 localURI:(NSString *)fileUriString 634 options:(NSDictionary *)options 635 rejecter:(EXPromiseRejectBlock)reject 636{ 637 NSURL *fileUri = [self percentEncodedURLFromURIString:fileUriString]; 638 NSString *httpMethod = options[@"httpMethod"]; 639 EXFileSystemUploadType type = [self _getUploadTypeFrom:options[@"uploadType"]]; 640 if (![fileUri.scheme isEqualToString:@"file"]) { 641 reject(@"ERR_FILESYSTEM_NO_PERMISSIONS", 642 [NSString stringWithFormat:@"Cannot upload file '%@'. Only 'file://' URI are supported.", fileUri], 643 nil); 644 return nil; 645 } 646 if (!([self _checkIfFileExists:fileUri.path])) { 647 reject(@"ERR_FILE_NOT_EXISTS", 648 [NSString stringWithFormat:@"File '%@' does not exist.", fileUri], 649 nil); 650 return nil; 651 } 652 if (![self _checkHeadersDictionary:options[@"headers"]]) { 653 reject(@"ERR_FILESYSTEM_INVALID_HEADERS_DICTIONARY", 654 @"Invalid headers dictionary. Keys and values should be strings.", 655 nil); 656 return nil; 657 } 658 if (!httpMethod) { 659 reject(@"ERR_FILESYSTEM_MISSING_HTTP_METHOD", @"Missing HTTP method.", nil); 660 return nil; 661 } 662 663 NSMutableURLRequest *request = [self _createRequest:[NSURL URLWithString:urlString] headers:options[@"headers"]]; 664 [request setHTTPMethod:httpMethod]; 665 NSURLSession *session = [self _sessionForType:[options[@"sessionType"] intValue]]; 666 if (!session) { 667 reject(@"ERR_FILESYSTEM_INVALID_SESSION_TYPE", 668 [NSString stringWithFormat:@"Invalid session type: '%@'", options[@"sessionType"]], 669 nil); 670 return nil; 671 } 672 673 NSURLSessionUploadTask *task; 674 if (type == EXFileSystemBinaryContent) { 675 task = [session uploadTaskWithRequest:request fromFile:fileUri]; 676 } else if (type == EXFileSystemMultipart) { 677 NSString *boundaryString = [[NSUUID UUID] UUIDString]; 678 [request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundaryString] forHTTPHeaderField:@"Content-Type"]; 679 NSData *httpBody = [self _createBodyWithBoundary:boundaryString 680 fileUri:fileUri 681 parameters:options[@"parameters"] 682 fieldName:options[@"fieldName"] 683 mimeType:options[@"mimeType"]]; 684 [request setHTTPBody:httpBody]; 685 task = [session uploadTaskWithStreamedRequest:request]; 686 } else { 687 reject(@"ERR_FILESYSTEM_INVALID_UPLOAD_TYPE", 688 [NSString stringWithFormat:@"Invalid upload type: '%@'.", options[@"uploadType"]], 689 nil); 690 } 691 return task; 692} 693 694EX_EXPORT_METHOD_AS(downloadResumableStartAsync, 695 downloadResumableStartAsyncWithUrl:(NSString *)urlString 696 fileURI:(NSString *)fileUri 697 uuid:(NSString *)uuid 698 options:(NSDictionary *)options 699 resumeData:(NSString *)data 700 resolver:(EXPromiseResolveBlock)resolve 701 rejecter:(EXPromiseRejectBlock)reject) 702{ 703 NSURL *url = [NSURL URLWithString:urlString]; 704 NSURL *localUrl = [self percentEncodedURLFromURIString:fileUri]; 705 if (!([self checkIfFileDirExists:localUrl.path])) { 706 reject(@"ERR_FILESYSTEM_WRONG_DESTINATION", 707 [NSString stringWithFormat:@"Directory for '%@' doesn't exist. Please make sure directory '%@' exists before calling downloadAsync.", fileUri, [localUrl.path stringByDeletingLastPathComponent]], 708 nil); 709 return; 710 } 711 if (![localUrl.scheme isEqualToString:@"file"]) { 712 reject(@"ERR_FILESYSTEM_NO_PERMISSIONS", 713 [NSString stringWithFormat:@"Cannot download to '%@': only 'file://' URI destinations are supported.", fileUri], 714 nil); 715 return; 716 } 717 718 NSString *path = localUrl.path; 719 if (!([self _permissionsForPath:path] & EXFileSystemPermissionWrite)) { 720 reject(@"ERR_FILESYSTEM_NO_PERMISSIONS", 721 [NSString stringWithFormat:@"File '%@' isn't writable.", fileUri], 722 nil); 723 return; 724 } 725 726 if (![self _checkHeadersDictionary:options[@"headers"]]) { 727 reject(@"ERR_FILESYSTEM_INVALID_HEADERS_DICTIONARY", 728 @"Invalid headers dictionary. Keys and values should be strings.", 729 nil); 730 return; 731 } 732 733 NSData *resumeData = data ? [[NSData alloc] initWithBase64EncodedString:data options:0] : nil; 734 [self _downloadResumableCreateSessionWithUrl:url 735 fileUrl:localUrl 736 uuid:uuid 737 optins:options 738 resumeData:resumeData 739 resolve:resolve 740 reject:reject]; 741} 742 743EX_EXPORT_METHOD_AS(downloadResumablePauseAsync, 744 downloadResumablePauseAsyncWithUUID:(NSString *)uuid 745 resolver:(EXPromiseResolveBlock)resolve 746 rejecter:(EXPromiseRejectBlock)reject) 747{ 748 NSURLSessionDownloadTask *task = [_taskHandlersManager downloadTaskForId:uuid]; 749 if (!task) { 750 reject(@"ERR_FILESYSTEM_CANNOT_FIND_TASK", 751 [NSString stringWithFormat:@"There is no download object with UUID: %@", uuid], 752 nil); 753 return; 754 } 755 756 EX_WEAKIFY(self); 757 [task cancelByProducingResumeData:^(NSData * _Nullable resumeData) { 758 EX_ENSURE_STRONGIFY(self); 759 resolve(@{ @"resumeData": EXNullIfNil([resumeData base64EncodedStringWithOptions:0]) }); 760 }]; 761} 762 763EX_EXPORT_METHOD_AS(networkTaskCancelAsync, 764 networkTaskCancelAsyncWithUUID:(NSString *)uuid 765 resolver:(EXPromiseResolveBlock)resolve 766 rejecter:(EXPromiseRejectBlock)reject) 767{ 768 NSURLSessionTask *task = [_taskHandlersManager taskForId:uuid]; 769 if (task) { 770 [task cancel]; 771 } 772 resolve([NSNull null]); 773} 774 775EX_EXPORT_METHOD_AS(getFreeDiskStorageAsync, getFreeDiskStorageAsyncWithResolver:(EXPromiseResolveBlock)resolve rejecter:(EXPromiseRejectBlock)reject) 776{ 777 NSError *error = nil; 778 NSNumber *freeDiskStorage = [self freeDiskStorageWithError:&error]; 779 780 if(!freeDiskStorage || error) { 781 reject(@"ERR_FILESYSTEM_CANNOT_DETERMINE_DISK_CAPACITY", @"Unable to determine free disk storage capacity", error); 782 } else { 783 resolve(freeDiskStorage); 784 } 785} 786 787EX_EXPORT_METHOD_AS(getTotalDiskCapacityAsync, getTotalDiskCapacityAsyncWithResolver:(EXPromiseResolveBlock)resolve rejecter:(EXPromiseRejectBlock)reject) 788{ 789 NSError *error = nil; 790 NSNumber *diskCapacity = [self totalDiskCapacityWithError:&error]; 791 792 if (!diskCapacity || error) { 793 reject(@"ERR_FILESYSTEM_CANNOT_DETERMINE_DISK_CAPACITY", @"Unable to determine total disk capacity", error); 794 } else { 795 resolve(diskCapacity); 796 } 797} 798 799#pragma mark - Internal methods 800 801- (EXFileSystemUploadType)_getUploadTypeFrom:(NSNumber * _Nullable)type 802{ 803 switch ([type intValue]) { 804 case EXFileSystemBinaryContent: 805 case EXFileSystemMultipart: 806 return [type intValue]; 807 } 808 809 return EXFileSystemInvalidType; 810} 811 812// Borrowed from http://stackoverflow.com/questions/2439020/wheres-the-iphone-mime-type-database 813- (NSString *)_guessMIMETypeFromPath:(NSString *)path 814{ 815 CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[path pathExtension], NULL); 816 CFStringRef MIMEType = UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType); 817 if (UTI) { 818 CFRelease(UTI); 819 } 820 if (!MIMEType) { 821 return @"application/octet-stream"; 822 } 823 return (__bridge NSString *)(MIMEType); 824} 825 826- (NSData *)_createBodyWithBoundary:(NSString *)boundary 827 fileUri:(NSURL *)fileUri 828 parameters:(NSDictionary * _Nullable)parameters 829 fieldName:(NSString * _Nullable)fieldName 830 mimeType:(NSString * _Nullable)mimetype 831{ 832 833 NSMutableData *body = [NSMutableData data]; 834 NSData *data = [NSData dataWithContentsOfURL:fileUri]; 835 NSString *filename = [[fileUri path] lastPathComponent]; 836 837 if (!mimetype) { 838 mimetype = [self _guessMIMETypeFromPath:[fileUri path]]; 839 } 840 841 [parameters enumerateKeysAndObjectsUsingBlock:^(NSString *parameterKey, NSString *parameterValue, BOOL *stop) { 842 [body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; 843 [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", parameterKey] dataUsingEncoding:NSUTF8StringEncoding]]; 844 [body appendData:[[NSString stringWithFormat:@"%@\r\n", parameterValue] dataUsingEncoding:NSUTF8StringEncoding]]; 845 }]; 846 847 [body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; 848 [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", fieldName ?: filename, filename] dataUsingEncoding:NSUTF8StringEncoding]]; 849 [body appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", mimetype] dataUsingEncoding:NSUTF8StringEncoding]]; 850 [body appendData:data]; 851 [body appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; 852 [body appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]]; 853 854 return body; 855} 856 857- (NSMutableURLRequest *)_createRequest:(NSURL *)url headers:(NSDictionary * _Nullable)headers 858{ 859 NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; 860 if (headers != nil) { 861 for (NSString *headerKey in headers) { 862 [request setValue:[headers valueForKey:headerKey] forHTTPHeaderField:headerKey]; 863 } 864 } 865 866 return request; 867} 868 869- (NSURLSession *)_sessionForType:(EXFileSystemSessionType)type 870{ 871 switch (type) { 872 case EXFileSystemBackgroundSession: 873 return _backgroundSession; 874 case EXFileSystemForegroundSession: 875 return _foregroundSession; 876 } 877 return nil; 878} 879 880- (BOOL)_checkHeadersDictionary:(NSDictionary * _Nullable)headers 881{ 882 for (id key in [headers allKeys]) { 883 if (![key isKindOfClass:[NSString class]] || ![headers[key] isKindOfClass:[NSString class]]) { 884 return false; 885 } 886 } 887 888 return true; 889} 890 891- (NSURLSession *)_createSession:(EXFileSystemSessionType)type delegate:(id<NSURLSessionDelegate>)delegate 892{ 893 NSURLSessionConfiguration *sessionConfiguration; 894 if (type == EXFileSystemForegroundSession) { 895 sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; 896 } else { 897 sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:[[NSUUID UUID] UUIDString]]; 898 } 899 sessionConfiguration.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; 900 sessionConfiguration.URLCache = nil; 901 return [NSURLSession sessionWithConfiguration:sessionConfiguration 902 delegate:delegate 903 delegateQueue:nil]; 904} 905 906- (BOOL)_checkIfFileExists:(NSString *)path 907{ 908 return [[NSFileManager defaultManager] fileExistsAtPath:path]; 909} 910 911- (void)_downloadResumableCreateSessionWithUrl:(NSURL *)url 912 fileUrl:(NSURL *)fileUrl 913 uuid:(NSString *)uuid 914 optins:(NSDictionary *)options 915 resumeData:(NSData * _Nullable)resumeData 916 resolve:(EXPromiseResolveBlock)resolve 917 reject:(EXPromiseRejectBlock)reject 918{ 919 EX_WEAKIFY(self); 920 EXDownloadDelegateOnWriteCallback onWrite = ^(NSURLSessionDownloadTask *task, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite) { 921 EX_ENSURE_STRONGIFY(self); 922 [self sendEventWithName:EXDownloadProgressEventName 923 body:@{ 924 @"uuid": uuid, 925 @"data": @{ 926 @"totalBytesWritten": @(totalBytesWritten), 927 @"totalBytesExpectedToWrite": @(totalBytesExpectedToWrite), 928 }, 929 }]; 930 }; 931 932 NSURLSessionDownloadTask *downloadTask; 933 NSURLSession *session = [self _sessionForType:[options[@"sessionType"] intValue]]; 934 if (!session) { 935 reject(@"ERR_FILESYSTEM_INVALID_SESSION_TYPE", 936 [NSString stringWithFormat:@"Invalid session type: '%@'", options[@"sessionType"]], 937 nil); 938 return; 939 } 940 941 if (resumeData) { 942 downloadTask = [session downloadTaskWithResumeData:resumeData]; 943 } else { 944 NSURLRequest *request = [self _createRequest:url headers:options[@"headers"]]; 945 downloadTask = [session downloadTaskWithRequest:request]; 946 } 947 EXSessionTaskDelegate *taskDelegate = [[EXSessionResumableDownloadTaskDelegate alloc] initWithResolve:resolve 948 reject:reject 949 localUrl:fileUrl 950 shouldCalculateMd5:[options[@"md5"] boolValue] 951 onWriteCallback:onWrite 952 resumableManager:_taskHandlersManager 953 uuid:uuid]; 954 [_sessionTaskDispatcher registerTaskDelegate:taskDelegate forTask:downloadTask]; 955 [_taskHandlersManager registerTask:downloadTask uuid:uuid]; 956 [downloadTask resume]; 957} 958 959- (EXFileSystemPermissionFlags)_permissionsForPath:(NSString *)path 960{ 961 return [[_moduleRegistry getModuleImplementingProtocol:@protocol(EXFilePermissionModuleInterface)] getPathPermissions:(NSString *)path]; 962} 963 964- (void)sendEventWithName:(NSString *)eventName body:(id)body 965{ 966 if (_eventEmitter != nil) { 967 [_eventEmitter sendEventWithName:eventName body:body]; 968 } 969} 970 971- (NSDictionary<NSURLResourceKey, id> *)documentFileResourcesForKeys:(NSArray<NSURLResourceKey> *)keys 972 error:(out NSError * __autoreleasing *)error 973{ 974 if (!keys.count) { 975 return @{}; 976 } 977 978 NSURL *documentDirectoryUrl = [NSURL fileURLWithPath:_documentDirectory]; 979 NSDictionary *results = [documentDirectoryUrl resourceValuesForKeys:keys 980 error:error]; 981 982 if (!results) { 983 return @{}; 984 } 985 986 return results; 987} 988 989#pragma mark - Public utils 990 991- (EXFileSystemPermissionFlags)permissionsForURI:(NSURL *)uri 992{ 993 NSArray *validSchemas = @[ 994 @"assets-library", 995 @"http", 996 @"https", 997 @"ph", 998 ]; 999 if ([validSchemas containsObject:uri.scheme]) { 1000 return EXFileSystemPermissionRead; 1001 } 1002 if ([uri.scheme isEqualToString:@"file"]) { 1003 return [self _permissionsForPath:uri.path]; 1004 } 1005 return EXFileSystemPermissionNone; 1006} 1007 1008- (BOOL)checkIfFileDirExists:(NSString *)path 1009{ 1010 NSString *dir = [path stringByDeletingLastPathComponent]; 1011 return [self _checkIfFileExists:dir]; 1012} 1013 1014/** 1015 Given an URI string, returns a percent-encoded URL as an NSURL object. 1016 Only encodes characters that are outside the set of allowed characters defined by RFC3986. 1017*/ 1018- (nullable NSURL *)percentEncodedURLFromURIString:(nonnull NSString *)uri 1019{ 1020 NSMutableCharacterSet *allowedCharacterSet = [NSMutableCharacterSet alphanumericCharacterSet]; 1021 [allowedCharacterSet addCharactersInString:@"-._~:/?#[]@!$&'()*+,;="]; 1022 NSString *encodedUriString = [uri stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet]; 1023 1024 return [NSURL URLWithString:[encodedUriString stringByReplacingOccurrencesOfString:@"%25" withString:@"%"]]; 1025} 1026 1027#pragma mark - Class methods 1028 1029- (BOOL)ensureDirExistsWithPath:(NSString *)path 1030{ 1031 return [EXFileSystem ensureDirExistsWithPath:path]; 1032} 1033 1034+ (BOOL)ensureDirExistsWithPath:(NSString *)path 1035{ 1036 BOOL isDir = NO; 1037 NSError *error; 1038 BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]; 1039 if (!(exists && isDir)) { 1040 [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error]; 1041 if (error) { 1042 return NO; 1043 } 1044 } 1045 return YES; 1046} 1047 1048- (NSString *)generatePathInDirectory:(NSString *)directory withExtension:(NSString *)extension 1049{ 1050 return [EXFileSystem generatePathInDirectory:directory withExtension:extension]; 1051} 1052 1053+ (NSString *)generatePathInDirectory:(NSString *)directory withExtension:(NSString *)extension 1054{ 1055 NSString *fileName = [[[NSUUID UUID] UUIDString] stringByAppendingString:extension]; 1056 [EXFileSystem ensureDirExistsWithPath:directory]; 1057 return [directory stringByAppendingPathComponent:fileName]; 1058} 1059 1060// '<ARCType> *__autoreleasing*' problem solution: https://stackoverflow.com/a/8862061/4337317 1061- (NSNumber *)totalDiskCapacityWithError:(out NSError * __autoreleasing *)error 1062{ 1063 NSDictionary *results = [self documentFileResourcesForKeys:@[NSURLVolumeTotalCapacityKey] 1064 error:error]; 1065 1066 return results[NSURLVolumeTotalCapacityKey]; 1067} 1068 1069// '<ARCType> *__autoreleasing*' problem solution: https://stackoverflow.com/a/8862061/4337317 1070- (NSNumber *)freeDiskStorageWithError:(out NSError * __autoreleasing *)error 1071{ 1072#if TARGET_OS_TV 1073 NSURLResourceKey key = NSURLVolumeAvailableCapacityKey; 1074#else 1075 NSURLResourceKey key = NSURLVolumeAvailableCapacityForImportantUsageKey; 1076#endif 1077 1078 NSDictionary *results = [self documentFileResourcesForKeys:@[key] 1079 error:error]; 1080 1081 return results[key]; 1082} 1083 1084@end 1085