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