Files
react-native-code-push/iOS/CodePushPackage.m
2016-02-18 15:56:17 -08:00

518 lines
21 KiB
Objective-C

#import "CodePush.h"
#import "SSZipArchive.h"
@implementation CodePushPackage
NSString * const CodePushErrorDomain = @"CodePushError";
const int CodePushErrorCode = -1;
NSString * const DiffManifestFileName = @"hotcodepush.json";
NSString * const DownloadFileName = @"download.zip";
NSString * const RelativeBundlePathKey = @"bundlePath";
NSString * const StatusFile = @"codepush.json";
NSString * const UpdateBundleFileName = @"app.jsbundle";
NSString * const UnzippedFolderName = @"unzipped";
+ (NSString *)getCodePushPath
{
NSString* codePushPath = [[CodePush getApplicationSupportDirectory] stringByAppendingPathComponent:@"CodePush"];
if ([CodePush isUsingTestConfiguration]) {
codePushPath = [codePushPath stringByAppendingPathComponent:@"TestPackages"];
}
return codePushPath;
}
+ (NSString *)getDownloadFilePath
{
return [[self getCodePushPath] stringByAppendingPathComponent:DownloadFileName];
}
+ (NSString *)getUnzippedFolderPath
{
return [[self getCodePushPath] stringByAppendingPathComponent:UnzippedFolderName];
}
+ (NSString *)getStatusFilePath
{
return [[self getCodePushPath] stringByAppendingPathComponent:StatusFile];
}
+ (NSMutableDictionary *)getCurrentPackageInfo:(NSError **)error
{
NSString *statusFilePath = [self getStatusFilePath];
if (![[NSFileManager defaultManager] fileExistsAtPath:statusFilePath]) {
return [NSMutableDictionary dictionary];
}
NSString *content = [NSString stringWithContentsOfFile:statusFilePath
encoding:NSUTF8StringEncoding
error:error];
if (*error) {
return NULL;
}
NSData *data = [content dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary* json = [NSJSONSerialization JSONObjectWithData:data
options:kNilOptions
error:error];
if (*error) {
return NULL;
}
return [json mutableCopy];
}
+ (void)updateCurrentPackageInfo:(NSDictionary *)packageInfo
error:(NSError **)error
{
NSData *packageInfoData = [NSJSONSerialization dataWithJSONObject:packageInfo
options:0
error:error];
NSString *packageInfoString = [[NSString alloc] initWithData:packageInfoData
encoding:NSUTF8StringEncoding];
[packageInfoString writeToFile:[self getStatusFilePath]
atomically:YES
encoding:NSUTF8StringEncoding
error:error];
}
+ (NSString *)getCurrentPackageFolderPath:(NSError **)error
{
NSDictionary *info = [self getCurrentPackageInfo:error];
if (*error) {
return NULL;
}
NSString *packageHash = info[@"currentPackage"];
if (!packageHash) {
return NULL;
}
return [self getPackageFolderPath:packageHash];
}
+ (NSString *)getCurrentPackageBundlePath:(NSError **)error
{
NSString *packageFolder = [self getCurrentPackageFolderPath:error];
if(*error) {
return NULL;
}
NSDictionary *currentPackage = [self getCurrentPackage:error];
if(*error) {
return NULL;
}
NSString *relativeBundlePath = [currentPackage objectForKey:RelativeBundlePathKey];
if (relativeBundlePath) {
return [packageFolder stringByAppendingPathComponent:relativeBundlePath];
} else {
return [packageFolder stringByAppendingPathComponent:UpdateBundleFileName];
}
}
+ (NSString *)getCurrentPackageHash:(NSError **)error
{
NSDictionary *info = [self getCurrentPackageInfo:error];
if (*error) {
return NULL;
}
return info[@"currentPackage"];
}
+ (NSString *)getPreviousPackageHash:(NSError **)error
{
NSDictionary *info = [self getCurrentPackageInfo:error];
if (*error) {
return NULL;
}
return info[@"previousPackage"];
}
+ (NSDictionary *)getCurrentPackage:(NSError **)error
{
NSString *folderPath = [CodePushPackage getCurrentPackageFolderPath:error];
if (!*error) {
if (!folderPath) {
return nil;
}
NSString *packagePath = [folderPath stringByAppendingPathComponent:@"app.json"];
NSString *content = [NSString stringWithContentsOfFile:packagePath
encoding:NSUTF8StringEncoding
error:error];
if (!*error) {
NSData *data = [content dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary* jsonDict = [NSJSONSerialization JSONObjectWithData:data
options:kNilOptions
error:error];
return jsonDict;
}
}
return nil;
}
+ (NSDictionary *)getPackage:(NSString *)packageHash
error:(NSError **)error
{
NSString *folderPath = [self getPackageFolderPath:packageHash];
if (!folderPath) {
return [NSDictionary dictionary];
}
NSString *packageFilePath = [folderPath stringByAppendingPathComponent:@"app.json"];
NSString *content = [NSString stringWithContentsOfFile:packageFilePath
encoding:NSUTF8StringEncoding
error:error];
if (!*error) {
NSData *data = [content dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary* jsonDict = [NSJSONSerialization JSONObjectWithData:data
options:kNilOptions
error:error];
return jsonDict;
}
return NULL;
}
+ (NSString *)getPackageFolderPath:(NSString *)packageHash
{
return [[self getCodePushPath] stringByAppendingPathComponent:packageHash];
}
+ (BOOL)isCodePushError:(NSError *)err
{
return err != nil && [CodePushErrorDomain isEqualToString:err.domain];
}
+ (void)downloadPackage:(NSDictionary *)updatePackage
progressCallback:(void (^)(long long, long long))progressCallback
doneCallback:(void (^)())doneCallback
failCallback:(void (^)(NSError *err))failCallback
{
NSString *newPackageHash = updatePackage[@"packageHash"];
NSString *newPackageFolderPath = [self getPackageFolderPath:newPackageHash];
NSString *newPackageMetadataPath = [newPackageFolderPath stringByAppendingPathComponent:@"app.json"];
NSError *error;
if ([[NSFileManager defaultManager] fileExistsAtPath:newPackageFolderPath]) {
// This removes any stale data in newPackageFolderPath that could have been left
// uncleared due to a crash or error during the download or install process.
[[NSFileManager defaultManager] removeItemAtPath:newPackageFolderPath
error:&error];
} else if (![[NSFileManager defaultManager] fileExistsAtPath:[self getCodePushPath]]) {
[[NSFileManager defaultManager] createDirectoryAtPath:[self getCodePushPath]
withIntermediateDirectories:YES
attributes:nil
error:&error];
}
if (error) {
return failCallback(error);
}
NSString *downloadFilePath = [self getDownloadFilePath];
NSString *bundleFilePath = [newPackageFolderPath stringByAppendingPathComponent:UpdateBundleFileName];
CodePushDownloadHandler *downloadHandler = [[CodePushDownloadHandler alloc]
init:downloadFilePath
progressCallback:progressCallback
doneCallback:^(BOOL isZip) {
NSError *error = nil;
NSString * unzippedFolderPath = [CodePushPackage getUnzippedFolderPath];
NSMutableDictionary * mutableUpdatePackage = [updatePackage mutableCopy];
if (isZip) {
if ([[NSFileManager defaultManager] fileExistsAtPath:unzippedFolderPath]) {
// This removes any unzipped download data that could have been left
// uncleared due to a crash or error during the download process.
[[NSFileManager defaultManager] removeItemAtPath:unzippedFolderPath
error:&error];
if (error) {
failCallback(error);
return;
}
}
NSError *nonFailingError = nil;
[SSZipArchive unzipFileAtPath:downloadFilePath
toDestination:unzippedFolderPath];
[[NSFileManager defaultManager] removeItemAtPath:downloadFilePath
error:&nonFailingError];
if (nonFailingError) {
NSLog(@"Error deleting downloaded file: %@", nonFailingError);
nonFailingError = nil;
}
NSString *diffManifestFilePath = [unzippedFolderPath stringByAppendingPathComponent:DiffManifestFileName];
if ([[NSFileManager defaultManager] fileExistsAtPath:diffManifestFilePath]) {
// Copy the current package to the new package.
NSString *currentPackageFolderPath = [self getCurrentPackageFolderPath:&error];
if (error) {
failCallback(error);
return;
}
[[NSFileManager defaultManager] copyItemAtPath:currentPackageFolderPath
toPath:newPackageFolderPath
error:&error];
if (error) {
failCallback(error);
return;
}
// Delete files mentioned in the manifest.
NSString *manifestContent = [NSString stringWithContentsOfFile:diffManifestFilePath
encoding:NSUTF8StringEncoding
error:&error];
if (error) {
failCallback(error);
return;
}
NSData *data = [manifestContent dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *manifestJSON = [NSJSONSerialization JSONObjectWithData:data
options:kNilOptions
error:&error];
NSArray *deletedFiles = manifestJSON[@"deletedFiles"];
for (NSString *deletedFileName in deletedFiles) {
[[NSFileManager defaultManager] removeItemAtPath:[newPackageFolderPath stringByAppendingPathComponent:deletedFileName]
error:&error];
if (error) {
failCallback(error);
return;
}
}
}
if ([[NSFileManager defaultManager] fileExistsAtPath:diffManifestFilePath]) {
[[NSFileManager defaultManager] removeItemAtPath:diffManifestFilePath
error:&error];
if (error) {
failCallback(error);
return;
}
}
[CodePushUtils copyEntriesInFolder:unzippedFolderPath
destFolder:newPackageFolderPath
error:&error];
if (error) {
failCallback(error);
return;
}
[[NSFileManager defaultManager] removeItemAtPath:unzippedFolderPath
error:&nonFailingError];
if (nonFailingError) {
NSLog(@"Error deleting downloaded file: %@", nonFailingError);
nonFailingError = nil;
}
NSString *relativeBundlePath = [CodePushUtils findMainBundleInFolder:newPackageFolderPath
error:&error];
if (error) {
failCallback(error);
return;
}
if (relativeBundlePath) {
NSString *absoluteBundlePath = [newPackageFolderPath stringByAppendingPathComponent:relativeBundlePath];
NSDictionary *bundleFileAttributes = [[[NSFileManager defaultManager] attributesOfItemAtPath:absoluteBundlePath error:&error] mutableCopy];
if (error) {
failCallback(error);
return;
}
[bundleFileAttributes setValue:[NSDate date] forKey:NSFileModificationDate];
[[NSFileManager defaultManager] setAttributes:bundleFileAttributes
ofItemAtPath:absoluteBundlePath
error:&error];
if (error) {
failCallback(error);
return;
}
[mutableUpdatePackage setValue:relativeBundlePath forKey:RelativeBundlePathKey];
} else {
error = [[NSError alloc] initWithDomain:CodePushErrorDomain
code:CodePushErrorCode
userInfo:@{
NSLocalizedDescriptionKey:
NSLocalizedString(@"Update is invalid - no files with extension .jsbundle or .bundle were found in the update package.", nil)
}];
failCallback(error);
return;
}
if ([[NSFileManager defaultManager] fileExistsAtPath:newPackageMetadataPath]) {
[[NSFileManager defaultManager] removeItemAtPath:newPackageMetadataPath
error:&error];
if (error) {
failCallback(error);
return;
}
}
if (![CodePushUtils verifyHashForZipUpdate:newPackageFolderPath
expectedHash:newPackageHash
error:&error]) {
if (error) {
failCallback(error);
return;
}
error = [[NSError alloc] initWithDomain:CodePushErrorDomain
code:CodePushErrorCode
userInfo:@{
NSLocalizedDescriptionKey:
NSLocalizedString(@"The update contents failed the data integrity check.", nil)
}];
failCallback(error);
return;
}
} else {
[[NSFileManager defaultManager] createDirectoryAtPath:newPackageFolderPath
withIntermediateDirectories:YES
attributes:nil
error:&error];
[[NSFileManager defaultManager] moveItemAtPath:downloadFilePath
toPath:bundleFilePath
error:&error];
if (error) {
failCallback(error);
return;
}
}
NSData *updateSerializedData = [NSJSONSerialization dataWithJSONObject:mutableUpdatePackage
options:0
error:&error];
NSString *packageJsonString = [[NSString alloc] initWithData:updateSerializedData
encoding:NSUTF8StringEncoding];
[packageJsonString writeToFile:newPackageMetadataPath
atomically:YES
encoding:NSUTF8StringEncoding
error:&error];
if (error) {
failCallback(error);
} else {
doneCallback();
}
}
failCallback:failCallback];
[downloadHandler download:updatePackage[@"downloadUrl"]];
}
+ (void)installPackage:(NSDictionary *)updatePackage
removePendingUpdate:(BOOL)removePendingUpdate
error:(NSError **)error
{
NSString *packageHash = updatePackage[@"packageHash"];
NSMutableDictionary *info = [self getCurrentPackageInfo:error];
if (*error) {
return;
}
if (removePendingUpdate) {
NSString *currentPackageFolderPath = [self getCurrentPackageFolderPath:error];
if (!*error && currentPackageFolderPath) {
// Error in deleting pending package will not cause the entire operation to fail.
NSError *deleteError;
[[NSFileManager defaultManager] removeItemAtPath:currentPackageFolderPath
error:&deleteError];
if (deleteError) {
NSLog(@"Error deleting pending package: %@", deleteError);
}
}
} else {
NSString *previousPackageHash = [self getPreviousPackageHash:error];
if (!*error && previousPackageHash && ![previousPackageHash isEqualToString:packageHash]) {
NSString *previousPackageFolderPath = [self getPackageFolderPath:previousPackageHash];
// Error in deleting old package will not cause the entire operation to fail.
NSError *deleteError;
[[NSFileManager defaultManager] removeItemAtPath:previousPackageFolderPath
error:&deleteError];
if (deleteError) {
NSLog(@"Error deleting old package: %@", deleteError);
}
}
[info setValue:info[@"currentPackage"] forKey:@"previousPackage"];
}
[info setValue:packageHash forKey:@"currentPackage"];
[self updateCurrentPackageInfo:info
error:error];
}
+ (void)rollbackPackage
{
NSError *error;
NSMutableDictionary *info = [self getCurrentPackageInfo:&error];
if (error) {
return;
}
NSString *currentPackageFolderPath = [self getCurrentPackageFolderPath:&error];
if (error) {
return;
}
NSError *deleteError;
[[NSFileManager defaultManager] removeItemAtPath:currentPackageFolderPath
error:&deleteError];
if (deleteError) {
NSLog(@"Error deleting current package contents at %@", currentPackageFolderPath);
}
[info setValue:info[@"previousPackage"] forKey:@"currentPackage"];
[info removeObjectForKey:@"previousPackage"];
[self updateCurrentPackageInfo:info error:&error];
}
+ (void)downloadAndReplaceCurrentBundle:(NSString *)remoteBundleUrl
{
NSURL *urlRequest = [NSURL URLWithString:remoteBundleUrl];
NSError *error = nil;
NSString *downloadedBundle = [NSString stringWithContentsOfURL:urlRequest
encoding:NSUTF8StringEncoding
error:&error];
if (error) {
NSLog(@"Error downloading from URL %@", remoteBundleUrl);
} else {
NSString *currentPackageBundlePath = [self getCurrentPackageBundlePath:&error];
[downloadedBundle writeToFile:currentPackageBundlePath
atomically:YES
encoding:NSUTF8StringEncoding
error:&error];
}
}
+ (void)clearUpdates
{
[[NSFileManager defaultManager] removeItemAtPath:[self getCodePushPath] error:nil];
[[NSFileManager defaultManager] removeItemAtPath:[self getStatusFilePath] error:nil];
}
@end