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- (NSString *)resourceCachePath 227{ 228 NSString *versionedResourceFilename = [NSString stringWithFormat:@"%@.%@", [self _resourceCacheFilenameUsingLegacy:NO], _resourceType]; 229 return [_cachePath stringByAppendingPathComponent:versionedResourceFilename]; 230} 231 232- (NSString *)resourceBundlePath 233{ 234 return [[NSBundle mainBundle] pathForResource:_resourceName ofType:_resourceType]; 235} 236 237- (NSString *)_resourceLocalPathPreferringCache 238{ 239 if ([self isUsingEmbeddedResource]) { 240 return [self resourceBundlePath]; 241 } 242 return [self resourceCachePath]; 243} 244 245- (BOOL)isUsingEmbeddedResource 246{ 247 // by default, only use the embedded resource if no cached copy exists 248 // but this behavior can be overridden by subclasses 249 NSString *localPath = [self resourceCachePath]; 250 return ![[NSFileManager defaultManager] fileExistsAtPath:localPath isDirectory:nil]; 251} 252 253- (BOOL)removeCache 254{ 255 NSString *localPath = [self resourceCachePath]; 256 if ([[NSFileManager defaultManager] fileExistsAtPath:localPath isDirectory:nil]) { 257 NSError *error; 258 [[NSFileManager defaultManager] removeItemAtPath:localPath error:&error]; 259 return (error == nil); 260 } 261 return NO; 262} 263 264- (NSError *)_validateResponseData:(NSData *)data response:(NSURLResponse *)response 265{ 266 // always valid 267 return nil; 268} 269 270- (NSError *)_validateErrorData:(NSError *)error response:(NSURLResponse *)response 271{ 272 NSError *networkError = [NSError errorWithDomain:EXNetworkErrorDomain code:error.code userInfo:error.userInfo]; 273 return networkError; 274} 275 276- (NSString *)_defaultCachePath 277{ 278 NSString *cachesDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; 279 NSString *sourceDirectory = [cachesDirectory stringByAppendingPathComponent:@"EXCachedResource"]; 280 281 BOOL cacheDirectoryExists = [[NSFileManager defaultManager] fileExistsAtPath:sourceDirectory isDirectory:nil]; 282 if (!cacheDirectoryExists) { 283 NSError *error; 284 BOOL created = [[NSFileManager defaultManager] createDirectoryAtPath:sourceDirectory 285 withIntermediateDirectories:YES 286 attributes:nil 287 error:&error]; 288 if (created) { 289 cacheDirectoryExists = YES; 290 } else { 291 DDLogError(@"Could not create source cache directory: %@", error.localizedDescription); 292 } 293 } 294 295 return (cacheDirectoryExists) ? sourceDirectory : nil; 296} 297 298@end 299 300NS_ASSUME_NONNULL_END 301