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