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 base = [NSString stringWithFormat:@"%@-%@", (_abiVersion) ?: [EXVersions sharedInstance].temporarySdkVersion, _resourceName]; 218 } else { 219 base = _resourceName; 220 } 221 222 if (useLegacy) { 223 return base; 224 } else { 225 return [NSString stringWithFormat:@"%@-%lu", base, (unsigned long)[_remoteUrl hash]]; 226 } 227} 228 229- (NSString *)resourceCachePath 230{ 231 NSString *versionedResourceFilename = [NSString stringWithFormat:@"%@.%@", [self _resourceCacheFilenameUsingLegacy:NO], _resourceType]; 232 return [_cachePath stringByAppendingPathComponent:versionedResourceFilename]; 233} 234 235- (NSString *)resourceBundlePath 236{ 237 return [[NSBundle mainBundle] pathForResource:_resourceName ofType:_resourceType]; 238} 239 240- (NSString *)_resourceLocalPathPreferringCache 241{ 242 if ([self isUsingEmbeddedResource]) { 243 return [self resourceBundlePath]; 244 } 245 return [self resourceCachePath]; 246} 247 248- (BOOL)isUsingEmbeddedResource 249{ 250 // by default, only use the embedded resource if no cached copy exists 251 // but this behavior can be overridden by subclasses 252 NSString *localPath = [self resourceCachePath]; 253 return ![[NSFileManager defaultManager] fileExistsAtPath:localPath isDirectory:nil]; 254} 255 256- (BOOL)removeCache 257{ 258 NSString *localPath = [self resourceCachePath]; 259 if ([[NSFileManager defaultManager] fileExistsAtPath:localPath isDirectory:nil]) { 260 NSError *error; 261 [[NSFileManager defaultManager] removeItemAtPath:localPath error:&error]; 262 return (error == nil); 263 } 264 return NO; 265} 266 267- (NSError *)_validateResponseData:(NSData *)data response:(NSURLResponse *)response 268{ 269 // always valid 270 return nil; 271} 272 273- (NSError *)_validateErrorData:(NSError *)error response:(NSURLResponse *)response 274{ 275 NSError *networkError = [NSError errorWithDomain:EXNetworkErrorDomain code:error.code userInfo:error.userInfo]; 276 return networkError; 277} 278 279- (NSString *)_defaultCachePath 280{ 281 return [[self class] cachePathWithName:@"EXCachedResource"]; 282} 283 284+ (NSString *)cachePathWithName:(NSString *)cacheName 285{ 286 NSString *cachesDirectory = [EXEnvironment sharedEnvironment].isDetached 287 ? NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES).firstObject 288 : NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; 289 290 NSString *sourceDirectory = [cachesDirectory stringByAppendingPathComponent:cacheName]; 291 NSString *sourceDirectoryVersioned = [EXEnvironment sharedEnvironment].isDetached 292 ? [sourceDirectory stringByAppendingPathComponent:[EXVersions sharedInstance].temporarySdkVersion] 293 : sourceDirectory; 294 295 BOOL cacheDirectoryExists = [[NSFileManager defaultManager] fileExistsAtPath:sourceDirectoryVersioned isDirectory:nil]; 296 if (!cacheDirectoryExists) { 297 NSError *error; 298 BOOL created = [[NSFileManager defaultManager] createDirectoryAtPath:sourceDirectoryVersioned 299 withIntermediateDirectories:YES 300 attributes:nil 301 error:&error]; 302 if (created) { 303 cacheDirectoryExists = YES; 304 } else { 305 DDLogError(@"Could not create source cache directory: %@", error.localizedDescription); 306 } 307 } 308 309 if (cacheDirectoryExists && [EXEnvironment sharedEnvironment].isDetached) { 310 NSURL *cacheDirectoryUrl = [NSURL fileURLWithPath:sourceDirectoryVersioned]; 311 NSError *error; 312 if (![cacheDirectoryUrl setResourceValue:@(YES) forKey:NSURLIsExcludedFromBackupKey error:&error]) { 313 DDLogError(@"Could not exclude source cache directory from backup: %@", error.localizedDescription); 314 } 315 } 316 317 if ([EXEnvironment sharedEnvironment].isDetached) { 318 static dispatch_once_t onceToken; 319 dispatch_once(&onceToken, ^{ 320 _reapingQueue = dispatch_queue_create("expo.cached-resource.reaping", DISPATCH_QUEUE_SERIAL); 321 }); 322 323 dispatch_async(_reapingQueue, ^{ 324 NSError *error; 325 NSArray<NSString *>* subfolders = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:sourceDirectory error:&error]; 326 if (error) { 327 DDLogError(@"Could not read old SDK version cache directories: %@", error.localizedDescription); 328 } else { 329 for (NSString *subfolder in subfolders) { 330 if (![subfolder isEqualToString:[EXVersions sharedInstance].temporarySdkVersion]) { 331 NSString *path = [sourceDirectory stringByAppendingPathComponent:subfolder]; 332 NSError *error; 333 if (![[NSFileManager defaultManager] removeItemAtPath:path error:&error]) { 334 DDLogError(@"Failed to reap old SDK version cache directories: %@", error.localizedDescription); 335 } 336 } 337 } 338 } 339 }); 340 } 341 342 return (cacheDirectoryExists) ? sourceDirectoryVersioned : nil; 343} 344 345@end 346 347NS_ASSUME_NONNULL_END 348