19088dd0eSBen Roth// Copyright 2015-present 650 Industries. All rights reserved. 29088dd0eSBen Roth 39088dd0eSBen Roth#import "EXCachedResource.h" 41506aaceSEric Samelson#import "EXEnvironment.h" 59088dd0eSBen Roth#import "EXFileDownloader.h" 69088dd0eSBen Roth#import "EXKernelUtil.h" 79088dd0eSBen Roth#import "EXUtil.h" 89088dd0eSBen Roth#import "EXVersions.h" 99088dd0eSBen Roth 109088dd0eSBen Roth#import <React/RCTUtils.h> 119088dd0eSBen Roth 129088dd0eSBen RothNS_ASSUME_NONNULL_BEGIN 139088dd0eSBen Roth 149088dd0eSBen Roth@implementation EXLoadingProgress 159088dd0eSBen Roth 169088dd0eSBen Roth@end 179088dd0eSBen Roth 189088dd0eSBen Roth@interface EXCachedResource () 199088dd0eSBen Roth 209088dd0eSBen Roth@property (nonatomic, strong) NSString *resourceName; 219088dd0eSBen Roth@property (nonatomic, strong) NSString *resourceType; 229088dd0eSBen Roth@property (nonatomic, strong) NSString *cachePath; 239088dd0eSBen Roth 249088dd0eSBen Roth@end 259088dd0eSBen Roth 269088dd0eSBen Roth@implementation EXCachedResource 279088dd0eSBen Roth 281506aaceSEric Samelsonstatic dispatch_queue_t _reapingQueue; 291506aaceSEric Samelson 309088dd0eSBen Roth- (instancetype)initWithResourceName:(NSString *)resourceName resourceType:(NSString *)resourceType remoteUrl:(nonnull NSURL *)url cachePath:(NSString * _Nullable)cachePath 319088dd0eSBen Roth{ 329088dd0eSBen Roth if (self = [super init]) { 339088dd0eSBen Roth _shouldVersionCache = YES; 349088dd0eSBen Roth _resourceName = [EXUtil escapedResourceName:resourceName]; 359088dd0eSBen Roth _resourceType = resourceType; 369088dd0eSBen Roth _remoteUrl = url; 379088dd0eSBen Roth _cachePath = (cachePath) ? cachePath : [self _defaultCachePath]; 389088dd0eSBen Roth } 399088dd0eSBen Roth return self; 409088dd0eSBen Roth} 419088dd0eSBen Roth 429088dd0eSBen Roth- (void)loadResourceWithBehavior:(EXCachedResourceBehavior)behavior 439088dd0eSBen Roth progressBlock:(__nullable EXCachedResourceProgressBlock)progressBlock 449088dd0eSBen Roth successBlock:(EXCachedResourceSuccessBlock)success 459088dd0eSBen Roth errorBlock:(EXCachedResourceErrorBlock)error 469088dd0eSBen Roth{ 479088dd0eSBen Roth switch (behavior) { 489088dd0eSBen Roth case EXCachedResourceNoCache: { 499088dd0eSBen Roth NSLog(@"EXCachedResource: Not using cache for %@", _resourceName); 509088dd0eSBen Roth [self _loadRemoteResourceWithSuccess:success error:error ignoringCache:YES]; 519088dd0eSBen Roth break; 529088dd0eSBen Roth } 539088dd0eSBen Roth case EXCachedResourceWriteToCache: { 549088dd0eSBen Roth [self _loadRemoteAndWriteToCacheWithSuccess:success error:error]; 559088dd0eSBen Roth break; 569088dd0eSBen Roth } 579088dd0eSBen Roth case EXCachedResourceUseCacheImmediately: { 589088dd0eSBen Roth [self _loadCacheImmediatelyAndDownload:YES withSuccess:success error:error]; 599088dd0eSBen Roth break; 609088dd0eSBen Roth } 619088dd0eSBen Roth case EXCachedResourceFallBackToNetwork: { 629088dd0eSBen Roth [self _loadCacheAndDownloadConditionallyWithSuccess:success error:error]; 639088dd0eSBen Roth break; 649088dd0eSBen Roth } 659088dd0eSBen Roth case EXCachedResourceFallBackToCache: default: { 669088dd0eSBen Roth [self _loadRemoteAndFallBackToCacheWithSuccess:success error:error]; 679088dd0eSBen Roth break; 689088dd0eSBen Roth } 699088dd0eSBen Roth case EXCachedResourceOnlyCache: { 709088dd0eSBen Roth [self _loadCacheImmediatelyAndDownload:NO withSuccess:success error:error]; 719088dd0eSBen Roth break; 729088dd0eSBen Roth } 739088dd0eSBen Roth } 749088dd0eSBen Roth} 759088dd0eSBen Roth 769088dd0eSBen Roth/** 779088dd0eSBen Roth * If @ignoreCache is true, make sure NSURLSession busts any existing cache. 789088dd0eSBen Roth */ 799088dd0eSBen Roth- (void)_loadRemoteResourceWithSuccess:(EXCachedResourceSuccessBlock)successBlock 809088dd0eSBen Roth error:(EXCachedResourceErrorBlock)errorBlock 819088dd0eSBen Roth ignoringCache:(BOOL)ignoreCache 829088dd0eSBen Roth{ 839088dd0eSBen Roth EXFileDownloader *downloader = [[EXFileDownloader alloc] init]; 849088dd0eSBen Roth if (_requestTimeoutInterval) { 859088dd0eSBen Roth downloader.timeoutInterval = _requestTimeoutInterval; 869088dd0eSBen Roth } 879088dd0eSBen Roth if (_abiVersion) { 889088dd0eSBen Roth downloader.abiVersion = _abiVersion; 899088dd0eSBen Roth } 909088dd0eSBen Roth if (_releaseChannel){ 919088dd0eSBen Roth downloader.releaseChannel = _releaseChannel; 929088dd0eSBen Roth } 939088dd0eSBen Roth if (_urlCache || ignoreCache) { 949088dd0eSBen Roth NSURLSessionConfiguration *customConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; 959088dd0eSBen Roth if (_urlCache) { 969088dd0eSBen Roth customConfiguration.URLCache = _urlCache; 979088dd0eSBen Roth } 989088dd0eSBen Roth if (ignoreCache) { 999088dd0eSBen Roth customConfiguration.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; 1009088dd0eSBen Roth } 1019088dd0eSBen Roth downloader.urlSessionConfiguration = customConfiguration; 1029088dd0eSBen Roth } 1039088dd0eSBen Roth 1049088dd0eSBen Roth [downloader downloadFileFromURL:_remoteUrl successBlock:^(NSData *data, NSURLResponse *response) { 1059088dd0eSBen Roth NSError *err = [self _validateResponseData:data response:response]; 1069088dd0eSBen Roth if (err) { 1079088dd0eSBen Roth errorBlock(err); 1089088dd0eSBen Roth } else { 1099088dd0eSBen Roth successBlock(data); 1109088dd0eSBen Roth } 1119088dd0eSBen Roth } errorBlock:^(NSError *error, NSURLResponse *response) { 1129088dd0eSBen Roth NSError *err = [self _validateErrorData:error response:response]; 1139088dd0eSBen Roth errorBlock(err); 1149088dd0eSBen Roth }]; 1159088dd0eSBen Roth} 1169088dd0eSBen Roth 1179088dd0eSBen Roth- (void)_loadCacheImmediatelyAndDownload:(BOOL)shouldAttemptDownload 1189088dd0eSBen Roth withSuccess:(EXCachedResourceSuccessBlock)successBlock 1199088dd0eSBen Roth error:(EXCachedResourceErrorBlock)errorBlock 1209088dd0eSBen Roth{ 1219088dd0eSBen Roth BOOL hasLocalBundle = NO; 1229088dd0eSBen Roth NSString *resourceCachePath = [self resourceCachePath]; 1239211cde4SEric Samelson NSString *resourceLocalPath = [self _resourceLocalPathPreferringCache]; 1249088dd0eSBen Roth 1259088dd0eSBen Roth // check cache 1269088dd0eSBen Roth if (resourceLocalPath) { 1279088dd0eSBen Roth NSData *data = [NSData dataWithContentsOfFile:resourceLocalPath]; 1289088dd0eSBen Roth if (data && data.length) { 1299088dd0eSBen Roth hasLocalBundle = YES; 1309088dd0eSBen Roth NSLog(@"EXCachedResource: Using cached resource at %@...", resourceLocalPath); 1319088dd0eSBen Roth successBlock(data); 1329088dd0eSBen Roth } 1339088dd0eSBen Roth } 1349088dd0eSBen Roth 1359088dd0eSBen Roth if (shouldAttemptDownload) { 1369088dd0eSBen Roth EXCachedResourceSuccessBlock onSuccess = ^(NSData *data) { 1379088dd0eSBen Roth if (!hasLocalBundle) { 1389088dd0eSBen Roth // no local bundle found, so call back with the newly downloaded resource 1399088dd0eSBen Roth successBlock(data); 1409088dd0eSBen Roth } 1419088dd0eSBen Roth 1429088dd0eSBen Roth // write to cache for next time 1439088dd0eSBen Roth NSLog(@"EXCachedResource: Caching resource to %@...", resourceCachePath); 1449088dd0eSBen Roth [data writeToFile:resourceCachePath atomically:YES]; 1459088dd0eSBen Roth }; 1469088dd0eSBen Roth EXCachedResourceErrorBlock onError = ^(NSError *error) { 1479088dd0eSBen Roth if (!hasLocalBundle) { 1489088dd0eSBen Roth // no local bundle found, and download failed, so call back with the bad news 1499088dd0eSBen Roth errorBlock(error); 1509088dd0eSBen Roth } 1519088dd0eSBen Roth }; 1529088dd0eSBen Roth 1539088dd0eSBen Roth [self _loadRemoteResourceWithSuccess:onSuccess error:onError ignoringCache:NO]; 1549088dd0eSBen Roth } else { 1559088dd0eSBen Roth // download not allowed, and we found no cached data, so fail 1569088dd0eSBen Roth if (!hasLocalBundle) { 1579088dd0eSBen Roth errorBlock(RCTErrorWithMessage([NSString stringWithFormat:@"No cache exists for this resource: %@.%@", _resourceName, _resourceType])); 1589088dd0eSBen Roth } 1599088dd0eSBen Roth } 1609088dd0eSBen Roth} 1619088dd0eSBen Roth 1629088dd0eSBen Roth- (void)_loadRemoteAndWriteToCacheWithSuccess:(EXCachedResourceSuccessBlock)successBlock 1639088dd0eSBen Roth error:(EXCachedResourceErrorBlock)errorBlock 1649088dd0eSBen Roth{ 1659088dd0eSBen Roth NSString *resourceCachePath = [self resourceCachePath]; 1669088dd0eSBen Roth 1679088dd0eSBen Roth [self _loadRemoteResourceWithSuccess:^(NSData * _Nonnull data) { 1689088dd0eSBen Roth // write to cache for next time 1699088dd0eSBen Roth NSLog(@"EXCachedResource: Caching resource to %@...", resourceCachePath); 1709088dd0eSBen Roth [data writeToFile:resourceCachePath atomically:YES]; 1719088dd0eSBen Roth successBlock(data); 1729088dd0eSBen Roth } error:errorBlock ignoringCache:YES]; 1739088dd0eSBen Roth} 1749088dd0eSBen Roth 1759088dd0eSBen Roth- (void)_loadRemoteAndFallBackToCacheWithSuccess:(EXCachedResourceSuccessBlock)successBlock 1769088dd0eSBen Roth error:(EXCachedResourceErrorBlock)errorBlock 1779088dd0eSBen Roth{ 1789088dd0eSBen Roth NSString *resourceCachePath = [self resourceCachePath]; 1799211cde4SEric Samelson NSString *resourceLocalPath = [self _resourceLocalPathPreferringCache]; 1809088dd0eSBen Roth 1819088dd0eSBen Roth [self _loadRemoteResourceWithSuccess:^(NSData * _Nonnull data) { 1829088dd0eSBen Roth // write to cache for next time 1839088dd0eSBen Roth NSLog(@"EXCachedResource: Caching resource to %@...", resourceCachePath); 1849088dd0eSBen Roth [data writeToFile:resourceCachePath atomically:YES]; 1859088dd0eSBen Roth successBlock(data); 1869088dd0eSBen Roth } error:^(NSError * _Nonnull error) { 1879088dd0eSBen Roth // failed, check cache instead 1889088dd0eSBen Roth BOOL hasLocalBundle = NO; 1899088dd0eSBen Roth if (resourceLocalPath) { 1909088dd0eSBen Roth NSData *data = [NSData dataWithContentsOfFile:resourceLocalPath]; 1919088dd0eSBen Roth if (data && data.length) { 1929088dd0eSBen Roth hasLocalBundle = YES; 1939088dd0eSBen Roth NSLog(@"EXCachedResource: Using cached resource at %@...", resourceLocalPath); 1949088dd0eSBen Roth successBlock(data); 1959088dd0eSBen Roth } 1969088dd0eSBen Roth } 1979088dd0eSBen Roth if (!hasLocalBundle) { 1989088dd0eSBen Roth errorBlock(error); 1999088dd0eSBen Roth } 2009088dd0eSBen Roth } ignoringCache:NO]; 2019088dd0eSBen Roth} 2029088dd0eSBen Roth 2039088dd0eSBen Roth- (void)_loadCacheAndDownloadConditionallyWithSuccess:(EXCachedResourceSuccessBlock)successBlock 2049088dd0eSBen Roth error:(EXCachedResourceErrorBlock)errorBlock 2059088dd0eSBen Roth{ 2069088dd0eSBen Roth [self _loadCacheImmediatelyAndDownload:NO withSuccess:successBlock error:^(NSError * _Nonnull error) { 2079088dd0eSBen Roth [self _loadRemoteAndWriteToCacheWithSuccess:successBlock error:errorBlock]; 2089088dd0eSBen Roth }]; 2099088dd0eSBen Roth} 2109088dd0eSBen Roth 2119211cde4SEric Samelson- (NSString *)_resourceCacheFilenameUsingLegacy:(BOOL)useLegacy 2129088dd0eSBen Roth{ 2139211cde4SEric Samelson NSString *base; 2149211cde4SEric Samelson 2159088dd0eSBen Roth // this is versioned because it can persist between updates of native code 2169088dd0eSBen Roth if (_shouldVersionCache) { 217*20417916STomasz Sapeta NSString *prefix = _abiVersion && _abiVersion.length > 0 ? _abiVersion : [EXVersions sharedInstance].temporarySdkVersion; 218*20417916STomasz Sapeta base = [NSString stringWithFormat:@"%@-%@", prefix ?: @"", _resourceName]; 2199088dd0eSBen Roth } else { 2209211cde4SEric Samelson base = _resourceName; 2219088dd0eSBen Roth } 2229211cde4SEric Samelson 2239211cde4SEric Samelson if (useLegacy) { 2249211cde4SEric Samelson return base; 2259211cde4SEric Samelson } else { 2269211cde4SEric Samelson return [NSString stringWithFormat:@"%@-%lu", base, (unsigned long)[_remoteUrl hash]]; 2279211cde4SEric Samelson } 2289211cde4SEric Samelson} 2299211cde4SEric Samelson 2309211cde4SEric Samelson- (NSString *)resourceCachePath 2319088dd0eSBen Roth{ 2329211cde4SEric Samelson NSString *versionedResourceFilename = [NSString stringWithFormat:@"%@.%@", [self _resourceCacheFilenameUsingLegacy:NO], _resourceType]; 2339211cde4SEric Samelson return [_cachePath stringByAppendingPathComponent:versionedResourceFilename]; 2349211cde4SEric Samelson} 2359211cde4SEric Samelson 2369211cde4SEric Samelson- (NSString *)resourceBundlePath 2379211cde4SEric Samelson{ 2389211cde4SEric Samelson return [[NSBundle mainBundle] pathForResource:_resourceName ofType:_resourceType]; 2399211cde4SEric Samelson} 2409211cde4SEric Samelson 2419211cde4SEric Samelson- (NSString *)_resourceLocalPathPreferringCache 2429211cde4SEric Samelson{ 2439211cde4SEric Samelson if ([self isUsingEmbeddedResource]) { 2449211cde4SEric Samelson return [self resourceBundlePath]; 2459211cde4SEric Samelson } 246febe1c51SEric Samelson return [self resourceCachePath]; 2479211cde4SEric Samelson} 2489088dd0eSBen Roth 2499211cde4SEric Samelson- (BOOL)isUsingEmbeddedResource 2509088dd0eSBen Roth{ 251febe1c51SEric Samelson // by default, only use the embedded resource if no cached copy exists 2529211cde4SEric Samelson // but this behavior can be overridden by subclasses 2539088dd0eSBen Roth NSString *localPath = [self resourceCachePath]; 254febe1c51SEric Samelson return ![[NSFileManager defaultManager] fileExistsAtPath:localPath isDirectory:nil]; 2559088dd0eSBen Roth} 2569088dd0eSBen Roth 2579088dd0eSBen Roth- (BOOL)removeCache 2589088dd0eSBen Roth{ 2599088dd0eSBen Roth NSString *localPath = [self resourceCachePath]; 2609088dd0eSBen Roth if ([[NSFileManager defaultManager] fileExistsAtPath:localPath isDirectory:nil]) { 2619088dd0eSBen Roth NSError *error; 2629088dd0eSBen Roth [[NSFileManager defaultManager] removeItemAtPath:localPath error:&error]; 2639088dd0eSBen Roth return (error == nil); 2649088dd0eSBen Roth } 2659088dd0eSBen Roth return NO; 2669088dd0eSBen Roth} 2679088dd0eSBen Roth 2689088dd0eSBen Roth- (NSError *)_validateResponseData:(NSData *)data response:(NSURLResponse *)response 2699088dd0eSBen Roth{ 2709088dd0eSBen Roth // always valid 2719088dd0eSBen Roth return nil; 2729088dd0eSBen Roth} 2739088dd0eSBen Roth 2749088dd0eSBen Roth- (NSError *)_validateErrorData:(NSError *)error response:(NSURLResponse *)response 2759088dd0eSBen Roth{ 2769088dd0eSBen Roth NSError *networkError = [NSError errorWithDomain:EXNetworkErrorDomain code:error.code userInfo:error.userInfo]; 2779088dd0eSBen Roth return networkError; 2789088dd0eSBen Roth} 2799088dd0eSBen Roth 2809088dd0eSBen Roth- (NSString *)_defaultCachePath 2819088dd0eSBen Roth{ 2821506aaceSEric Samelson return [[self class] cachePathWithName:@"EXCachedResource"]; 2831506aaceSEric Samelson} 2849088dd0eSBen Roth 2851506aaceSEric Samelson+ (NSString *)cachePathWithName:(NSString *)cacheName 2861506aaceSEric Samelson{ 2871506aaceSEric Samelson NSString *cachesDirectory = [EXEnvironment sharedEnvironment].isDetached 2881506aaceSEric Samelson ? NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES).firstObject 2891506aaceSEric Samelson : NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; 2901506aaceSEric Samelson 2911506aaceSEric Samelson NSString *sourceDirectory = [cachesDirectory stringByAppendingPathComponent:cacheName]; 2921506aaceSEric Samelson NSString *sourceDirectoryVersioned = [EXEnvironment sharedEnvironment].isDetached 2931506aaceSEric Samelson ? [sourceDirectory stringByAppendingPathComponent:[EXVersions sharedInstance].temporarySdkVersion] 2941506aaceSEric Samelson : sourceDirectory; 2951506aaceSEric Samelson 2961506aaceSEric Samelson BOOL cacheDirectoryExists = [[NSFileManager defaultManager] fileExistsAtPath:sourceDirectoryVersioned isDirectory:nil]; 2979088dd0eSBen Roth if (!cacheDirectoryExists) { 2989088dd0eSBen Roth NSError *error; 2991506aaceSEric Samelson BOOL created = [[NSFileManager defaultManager] createDirectoryAtPath:sourceDirectoryVersioned 3009088dd0eSBen Roth withIntermediateDirectories:YES 3019088dd0eSBen Roth attributes:nil 3029088dd0eSBen Roth error:&error]; 3039088dd0eSBen Roth if (created) { 3049088dd0eSBen Roth cacheDirectoryExists = YES; 3059088dd0eSBen Roth } else { 3069088dd0eSBen Roth DDLogError(@"Could not create source cache directory: %@", error.localizedDescription); 3079088dd0eSBen Roth } 3089088dd0eSBen Roth } 3099088dd0eSBen Roth 3101506aaceSEric Samelson if (cacheDirectoryExists && [EXEnvironment sharedEnvironment].isDetached) { 3111506aaceSEric Samelson NSURL *cacheDirectoryUrl = [NSURL fileURLWithPath:sourceDirectoryVersioned]; 3121506aaceSEric Samelson NSError *error; 3131506aaceSEric Samelson if (![cacheDirectoryUrl setResourceValue:@(YES) forKey:NSURLIsExcludedFromBackupKey error:&error]) { 3141506aaceSEric Samelson DDLogError(@"Could not exclude source cache directory from backup: %@", error.localizedDescription); 3151506aaceSEric Samelson } 3161506aaceSEric Samelson } 3171506aaceSEric Samelson 3181506aaceSEric Samelson if ([EXEnvironment sharedEnvironment].isDetached) { 3191506aaceSEric Samelson static dispatch_once_t onceToken; 3201506aaceSEric Samelson dispatch_once(&onceToken, ^{ 3211506aaceSEric Samelson _reapingQueue = dispatch_queue_create("expo.cached-resource.reaping", DISPATCH_QUEUE_SERIAL); 3221506aaceSEric Samelson }); 3231506aaceSEric Samelson 3241506aaceSEric Samelson dispatch_async(_reapingQueue, ^{ 3251506aaceSEric Samelson NSError *error; 3261506aaceSEric Samelson NSArray<NSString *>* subfolders = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:sourceDirectory error:&error]; 3271506aaceSEric Samelson if (error) { 3281506aaceSEric Samelson DDLogError(@"Could not read old SDK version cache directories: %@", error.localizedDescription); 3291506aaceSEric Samelson } else { 3301506aaceSEric Samelson for (NSString *subfolder in subfolders) { 3311506aaceSEric Samelson if (![subfolder isEqualToString:[EXVersions sharedInstance].temporarySdkVersion]) { 3321506aaceSEric Samelson NSString *path = [sourceDirectory stringByAppendingPathComponent:subfolder]; 3331506aaceSEric Samelson NSError *error; 3341506aaceSEric Samelson if (![[NSFileManager defaultManager] removeItemAtPath:path error:&error]) { 3351506aaceSEric Samelson DDLogError(@"Failed to reap old SDK version cache directories: %@", error.localizedDescription); 3361506aaceSEric Samelson } 3371506aaceSEric Samelson } 3381506aaceSEric Samelson } 3391506aaceSEric Samelson } 3401506aaceSEric Samelson }); 3411506aaceSEric Samelson } 3421506aaceSEric Samelson 3431506aaceSEric Samelson return (cacheDirectoryExists) ? sourceDirectoryVersioned : nil; 3449088dd0eSBen Roth} 3459088dd0eSBen Roth 3469088dd0eSBen Roth@end 3479088dd0eSBen Roth 3489088dd0eSBen RothNS_ASSUME_NONNULL_END 349