1// Copyright 2015-present 650 Industries. All rights reserved. 2 3#import "EXCachedResource.h" 4#import "EXEnvironment.h" 5#import "EXFileDownloader.h" 6#import "EXKernelUtil.h" 7#import "EXUtil.h" 8#import "EXVersions.h" 9 10#import <React/RCTUtils.h> 11 12NS_ASSUME_NONNULL_BEGIN 13 14@implementation EXLoadingProgress 15 16@end 17 18@interface EXCachedResource () 19 20@property (nonatomic, strong) NSString *resourceName; 21@property (nonatomic, strong) NSString *resourceType; 22@property (nonatomic, strong) NSString *cachePath; 23 24@end 25 26@implementation EXCachedResource 27 28static dispatch_queue_t _reapingQueue; 29 30- (instancetype)initWithResourceName:(NSString *)resourceName resourceType:(NSString *)resourceType remoteUrl:(nonnull NSURL *)url cachePath:(NSString * _Nullable)cachePath 31{ 32 if (self = [super init]) { 33 _shouldVersionCache = YES; 34 _resourceName = [EXUtil escapedResourceName:resourceName]; 35 _resourceType = resourceType; 36 _remoteUrl = url; 37 _cachePath = (cachePath) ? cachePath : [self _defaultCachePath]; 38 } 39 return self; 40} 41 42- (void)loadResourceWithBehavior:(EXCachedResourceBehavior)behavior 43 progressBlock:(__nullable EXCachedResourceProgressBlock)progressBlock 44 successBlock:(EXCachedResourceSuccessBlock)success 45 errorBlock:(EXCachedResourceErrorBlock)error 46{ 47 switch (behavior) { 48 case EXCachedResourceNoCache: { 49 NSLog(@"EXCachedResource: Not using cache for %@", _resourceName); 50 [self _loadRemoteResourceWithSuccess:success error:error ignoringCache:YES]; 51 break; 52 } 53 case EXCachedResourceWriteToCache: { 54 [self _loadRemoteAndWriteToCacheWithSuccess:success error:error]; 55 break; 56 } 57 case EXCachedResourceUseCacheImmediately: { 58 [self _loadCacheImmediatelyAndDownload:YES withSuccess:success error:error]; 59 break; 60 } 61 case EXCachedResourceFallBackToNetwork: { 62 [self _loadCacheAndDownloadConditionallyWithSuccess:success error:error]; 63 break; 64 } 65 case EXCachedResourceFallBackToCache: default: { 66 [self _loadRemoteAndFallBackToCacheWithSuccess:success error:error]; 67 break; 68 } 69 case EXCachedResourceOnlyCache: { 70 [self _loadCacheImmediatelyAndDownload:NO withSuccess:success error:error]; 71 break; 72 } 73 } 74} 75 76/** 77 * If @ignoreCache is true, make sure NSURLSession busts any existing cache. 78 */ 79- (void)_loadRemoteResourceWithSuccess:(EXCachedResourceSuccessBlock)successBlock 80 error:(EXCachedResourceErrorBlock)errorBlock 81 ignoringCache:(BOOL)ignoreCache 82{ 83 EXFileDownloader *downloader = [[EXFileDownloader alloc] init]; 84 if (_requestTimeoutInterval) { 85 downloader.timeoutInterval = _requestTimeoutInterval; 86 } 87 if (_abiVersion) { 88 downloader.abiVersion = _abiVersion; 89 } 90 if (_releaseChannel){ 91 downloader.releaseChannel = _releaseChannel; 92 } 93 if (_urlCache || ignoreCache) { 94 NSURLSessionConfiguration *customConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; 95 if (_urlCache) { 96 customConfiguration.URLCache = _urlCache; 97 } 98 if (ignoreCache) { 99 customConfiguration.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; 100 } 101 downloader.urlSessionConfiguration = customConfiguration; 102 } 103 104 [downloader downloadFileFromURL:_remoteUrl successBlock:^(NSData *data, NSURLResponse *response) { 105 NSError *err = [self _validateResponseData:data response:response]; 106 if (err) { 107 errorBlock(err); 108 } else { 109 successBlock(data); 110 } 111 } errorBlock:^(NSError *error, NSURLResponse *response) { 112 NSError *err = [self _validateErrorData:error response:response]; 113 errorBlock(err); 114 }]; 115} 116 117- (void)_loadCacheImmediatelyAndDownload:(BOOL)shouldAttemptDownload 118 withSuccess:(EXCachedResourceSuccessBlock)successBlock 119 error:(EXCachedResourceErrorBlock)errorBlock 120{ 121 BOOL hasLocalBundle = NO; 122 NSString *resourceCachePath = [self resourceCachePath]; 123 NSString *resourceLocalPath = [self _resourceLocalPathPreferringCache]; 124 125 // check cache 126 if (resourceLocalPath) { 127 NSData *data = [NSData dataWithContentsOfFile:resourceLocalPath]; 128 if (data && data.length) { 129 hasLocalBundle = YES; 130 NSLog(@"EXCachedResource: Using cached resource at %@...", resourceLocalPath); 131 successBlock(data); 132 } 133 } 134 135 if (shouldAttemptDownload) { 136 EXCachedResourceSuccessBlock onSuccess = ^(NSData *data) { 137 if (!hasLocalBundle) { 138 // no local bundle found, so call back with the newly downloaded resource 139 successBlock(data); 140 } 141 142 // write to cache for next time 143 NSLog(@"EXCachedResource: Caching resource to %@...", resourceCachePath); 144 [data writeToFile:resourceCachePath atomically:YES]; 145 }; 146 EXCachedResourceErrorBlock onError = ^(NSError *error) { 147 if (!hasLocalBundle) { 148 // no local bundle found, and download failed, so call back with the bad news 149 errorBlock(error); 150 } 151 }; 152 153 [self _loadRemoteResourceWithSuccess:onSuccess error:onError ignoringCache:NO]; 154 } else { 155 // download not allowed, and we found no cached data, so fail 156 if (!hasLocalBundle) { 157 errorBlock(RCTErrorWithMessage([NSString stringWithFormat:@"No cache exists for this resource: %@.%@", _resourceName, _resourceType])); 158 } 159 } 160} 161 162- (void)_loadRemoteAndWriteToCacheWithSuccess:(EXCachedResourceSuccessBlock)successBlock 163 error:(EXCachedResourceErrorBlock)errorBlock 164{ 165 NSString *resourceCachePath = [self resourceCachePath]; 166 167 [self _loadRemoteResourceWithSuccess:^(NSData * _Nonnull data) { 168 // write to cache for next time 169 NSLog(@"EXCachedResource: Caching resource to %@...", resourceCachePath); 170 [data writeToFile:resourceCachePath atomically:YES]; 171 successBlock(data); 172 } error:errorBlock ignoringCache:YES]; 173} 174 175- (void)_loadRemoteAndFallBackToCacheWithSuccess:(EXCachedResourceSuccessBlock)successBlock 176 error:(EXCachedResourceErrorBlock)errorBlock 177{ 178 NSString *resourceCachePath = [self resourceCachePath]; 179 NSString *resourceLocalPath = [self _resourceLocalPathPreferringCache]; 180 181 [self _loadRemoteResourceWithSuccess:^(NSData * _Nonnull data) { 182 // write to cache for next time 183 NSLog(@"EXCachedResource: Caching resource to %@...", resourceCachePath); 184 [data writeToFile:resourceCachePath atomically:YES]; 185 successBlock(data); 186 } error:^(NSError * _Nonnull error) { 187 // failed, check cache instead 188 BOOL hasLocalBundle = NO; 189 if (resourceLocalPath) { 190 NSData *data = [NSData dataWithContentsOfFile:resourceLocalPath]; 191 if (data && data.length) { 192 hasLocalBundle = YES; 193 NSLog(@"EXCachedResource: Using cached resource at %@...", resourceLocalPath); 194 successBlock(data); 195 } 196 } 197 if (!hasLocalBundle) { 198 errorBlock(error); 199 } 200 } ignoringCache:NO]; 201} 202 203- (void)_loadCacheAndDownloadConditionallyWithSuccess:(EXCachedResourceSuccessBlock)successBlock 204 error:(EXCachedResourceErrorBlock)errorBlock 205{ 206 [self _loadCacheImmediatelyAndDownload:NO withSuccess:successBlock error:^(NSError * _Nonnull error) { 207 [self _loadRemoteAndWriteToCacheWithSuccess:successBlock error:errorBlock]; 208 }]; 209} 210 211- (NSString *)_resourceCacheFilenameUsingLegacy:(BOOL)useLegacy 212{ 213 NSString *base; 214 215 // this is versioned because it can persist between updates of native code 216 if (_shouldVersionCache) { 217 NSString *prefix = _abiVersion && _abiVersion.length > 0 ? _abiVersion : [EXVersions sharedInstance].temporarySdkVersion; 218 base = [NSString stringWithFormat:@"%@-%@", prefix ?: @"", _resourceName]; 219 } else { 220 base = _resourceName; 221 } 222 223 if (useLegacy) { 224 return base; 225 } else { 226 return [NSString stringWithFormat:@"%@-%lu", base, (unsigned long)[_remoteUrl hash]]; 227 } 228} 229 230- (NSString *)resourceCachePath 231{ 232 NSString *versionedResourceFilename = [NSString stringWithFormat:@"%@.%@", [self _resourceCacheFilenameUsingLegacy:NO], _resourceType]; 233 return [_cachePath stringByAppendingPathComponent:versionedResourceFilename]; 234} 235 236- (NSString *)resourceBundlePath 237{ 238 return [[NSBundle mainBundle] pathForResource:_resourceName ofType:_resourceType]; 239} 240 241- (NSString *)_resourceLocalPathPreferringCache 242{ 243 if ([self isUsingEmbeddedResource]) { 244 return [self resourceBundlePath]; 245 } 246 return [self resourceCachePath]; 247} 248 249- (BOOL)isUsingEmbeddedResource 250{ 251 // by default, only use the embedded resource if no cached copy exists 252 // but this behavior can be overridden by subclasses 253 NSString *localPath = [self resourceCachePath]; 254 return ![[NSFileManager defaultManager] fileExistsAtPath:localPath isDirectory:nil]; 255} 256 257- (BOOL)removeCache 258{ 259 NSString *localPath = [self resourceCachePath]; 260 if ([[NSFileManager defaultManager] fileExistsAtPath:localPath isDirectory:nil]) { 261 NSError *error; 262 [[NSFileManager defaultManager] removeItemAtPath:localPath error:&error]; 263 return (error == nil); 264 } 265 return NO; 266} 267 268- (NSError *)_validateResponseData:(NSData *)data response:(NSURLResponse *)response 269{ 270 // always valid 271 return nil; 272} 273 274- (NSError *)_validateErrorData:(NSError *)error response:(NSURLResponse *)response 275{ 276 NSError *networkError = [NSError errorWithDomain:EXNetworkErrorDomain code:error.code userInfo:error.userInfo]; 277 return networkError; 278} 279 280- (NSString *)_defaultCachePath 281{ 282 return [[self class] cachePathWithName:@"EXCachedResource"]; 283} 284 285+ (NSString *)cachePathWithName:(NSString *)cacheName 286{ 287 NSString *cachesDirectory = [EXEnvironment sharedEnvironment].isDetached 288 ? NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES).firstObject 289 : NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; 290 291 NSString *sourceDirectory = [cachesDirectory stringByAppendingPathComponent:cacheName]; 292 NSString *sourceDirectoryVersioned = [EXEnvironment sharedEnvironment].isDetached 293 ? [sourceDirectory stringByAppendingPathComponent:[EXVersions sharedInstance].temporarySdkVersion] 294 : sourceDirectory; 295 296 BOOL cacheDirectoryExists = [[NSFileManager defaultManager] fileExistsAtPath:sourceDirectoryVersioned isDirectory:nil]; 297 if (!cacheDirectoryExists) { 298 NSError *error; 299 BOOL created = [[NSFileManager defaultManager] createDirectoryAtPath:sourceDirectoryVersioned 300 withIntermediateDirectories:YES 301 attributes:nil 302 error:&error]; 303 if (created) { 304 cacheDirectoryExists = YES; 305 } else { 306 DDLogError(@"Could not create source cache directory: %@", error.localizedDescription); 307 } 308 } 309 310 if (cacheDirectoryExists && [EXEnvironment sharedEnvironment].isDetached) { 311 NSURL *cacheDirectoryUrl = [NSURL fileURLWithPath:sourceDirectoryVersioned]; 312 NSError *error; 313 if (![cacheDirectoryUrl setResourceValue:@(YES) forKey:NSURLIsExcludedFromBackupKey error:&error]) { 314 DDLogError(@"Could not exclude source cache directory from backup: %@", error.localizedDescription); 315 } 316 } 317 318 if ([EXEnvironment sharedEnvironment].isDetached) { 319 static dispatch_once_t onceToken; 320 dispatch_once(&onceToken, ^{ 321 _reapingQueue = dispatch_queue_create("expo.cached-resource.reaping", DISPATCH_QUEUE_SERIAL); 322 }); 323 324 dispatch_async(_reapingQueue, ^{ 325 NSError *error; 326 NSArray<NSString *>* subfolders = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:sourceDirectory error:&error]; 327 if (error) { 328 DDLogError(@"Could not read old SDK version cache directories: %@", error.localizedDescription); 329 } else { 330 for (NSString *subfolder in subfolders) { 331 if (![subfolder isEqualToString:[EXVersions sharedInstance].temporarySdkVersion]) { 332 NSString *path = [sourceDirectory stringByAppendingPathComponent:subfolder]; 333 NSError *error; 334 if (![[NSFileManager defaultManager] removeItemAtPath:path error:&error]) { 335 DDLogError(@"Failed to reap old SDK version cache directories: %@", error.localizedDescription); 336 } 337 } 338 } 339 } 340 }); 341 } 342 343 return (cacheDirectoryExists) ? sourceDirectoryVersioned : nil; 344} 345 346@end 347 348NS_ASSUME_NONNULL_END 349