450 lines
17 KiB
Objective-C
450 lines
17 KiB
Objective-C
//
|
|
// RCTHotUpdate.m
|
|
// RCTHotUpdate
|
|
//
|
|
// Created by LvBingru on 2/19/16.
|
|
// Copyright © 2016 erica. All rights reserved.
|
|
//
|
|
|
|
#import "RCTHotUpdate.h"
|
|
#import "RCTHotUpdateDownloader.h"
|
|
#import "RCTEventDispatcher.h"
|
|
#import "RCTConvert.h"
|
|
#import "RCTHotUpdateManager.h"
|
|
#import "RCTLog.h"
|
|
|
|
//
|
|
static NSString *const keyUpdateInfo = @"REACTNATIVECN_HOTUPDATE_INFO_KEY";
|
|
static NSString *const paramPackageVersion = @"packageVersion";
|
|
static NSString *const paramLastVersion = @"lastVersion";
|
|
static NSString *const paramCurrentVersion = @"currentVersion";
|
|
static NSString *const paramIsFirstTime = @"isFirstTime";
|
|
static NSString *const paramIsFirstLoadOk = @"isFirstLoadOK";
|
|
static NSString *const keyFirstLoadMarked = @"REACTNATIVECN_HOTUPDATE_FIRSTLOADMARKED_KEY";
|
|
static NSString *const keyRolledBackMarked = @"REACTNATIVECN_HOTUPDATE_ROLLEDBACKMARKED_KEY";
|
|
static NSString *const KeyPackageUpdatedMarked = @"REACTNATIVECN_HOTUPDATE_ISPACKAGEUPDATEDMARKED_KEY";
|
|
|
|
// app info
|
|
static NSString * const AppVersionKey = @"appVersion";
|
|
static NSString * const BuildVersionKey = @"buildVersion";
|
|
|
|
// file def
|
|
static NSString * const BUNDLE_FILE_NAME = @"index.bundlejs";
|
|
static NSString * const SOURCE_PATCH_NAME = @"__diff.json";
|
|
static NSString * const BUNDLE_PATCH_NAME = @"index.bundlejs.patch";
|
|
|
|
// error def
|
|
static NSString * const ERROR_OPTIONS = @"options error";
|
|
static NSString * const ERROR_BSDIFF = @"bsdiff error";
|
|
static NSString * const ERROR_FILE_OPERATION = @"file operation error";
|
|
|
|
// event def
|
|
static NSString * const EVENT_PROGRESS_DOWNLOAD = @"RCTHotUpdateDownloadProgress";
|
|
static NSString * const EVENT_PROGRESS_UNZIP = @"RCTHotUpdateUnzipProgress";
|
|
static NSString * const PARAM_PROGRESS_HASHNAME = @"hashname";
|
|
static NSString * const PARAM_PROGRESS_RECEIVED = @"received";
|
|
static NSString * const PARAM_PROGRESS_TOTAL = @"total";
|
|
|
|
|
|
typedef NS_ENUM(NSInteger, HotUpdateType) {
|
|
HotUpdateTypeFullDownload = 1,
|
|
HotUpdateTypePatchFromPackage = 2,
|
|
HotUpdateTypePatchFromPpk = 3,
|
|
};
|
|
|
|
@implementation RCTHotUpdate {
|
|
RCTHotUpdateManager *_fileManager;
|
|
}
|
|
|
|
@synthesize bridge = _bridge;
|
|
@synthesize methodQueue = _methodQueue;
|
|
|
|
RCT_EXPORT_MODULE(RCTHotUpdate);
|
|
|
|
+ (NSURL *)bundleURL
|
|
{
|
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
|
|
NSDictionary *updateInfo = [defaults dictionaryForKey:keyUpdateInfo];
|
|
if (updateInfo) {
|
|
NSString *curPackageVersion = [RCTHotUpdate packageVersion];
|
|
NSString *packageVersion = [updateInfo objectForKey:paramPackageVersion];
|
|
|
|
BOOL needClearUpdateInfo = ![curPackageVersion isEqualToString:packageVersion];
|
|
if (needClearUpdateInfo) {
|
|
[defaults setObject:nil forKey:keyUpdateInfo];
|
|
[defaults setObject:@(YES) forKey:KeyPackageUpdatedMarked];
|
|
[defaults synchronize];
|
|
// ...need clear files later
|
|
}
|
|
else {
|
|
NSString *curVersion = updateInfo[paramCurrentVersion];
|
|
NSString *lastVersion = updateInfo[paramLastVersion];
|
|
|
|
BOOL isFirstTime = [updateInfo[paramIsFirstTime] boolValue];
|
|
BOOL isFirstLoadOK = [updateInfo[paramIsFirstLoadOk] boolValue];
|
|
|
|
NSString *loadVersioin = curVersion;
|
|
BOOL needRollback = (isFirstTime == NO && isFirstLoadOK == NO) || loadVersioin.length<=0;
|
|
if (needRollback) {
|
|
loadVersioin = lastVersion;
|
|
|
|
if (lastVersion.length) {
|
|
// roll back to last version
|
|
[defaults setObject:@{paramCurrentVersion:lastVersion,
|
|
paramIsFirstTime:@(NO),
|
|
paramIsFirstLoadOk:@(YES),
|
|
paramPackageVersion:curPackageVersion}
|
|
forKey:keyUpdateInfo];
|
|
}
|
|
else {
|
|
// roll back to bundle
|
|
[defaults setObject:nil forKey:keyUpdateInfo];
|
|
}
|
|
[defaults setObject:@(YES) forKey:keyRolledBackMarked];
|
|
[defaults synchronize];
|
|
// ...need clear files later
|
|
}
|
|
else if (isFirstTime){
|
|
NSMutableDictionary *newInfo = [[NSMutableDictionary alloc] initWithDictionary:updateInfo];
|
|
newInfo[paramIsFirstTime] = @(NO);
|
|
[defaults setObject:newInfo forKey:keyUpdateInfo];
|
|
[defaults setObject:@(YES) forKey:keyFirstLoadMarked];
|
|
[defaults synchronize];
|
|
}
|
|
|
|
if (loadVersioin.length) {
|
|
NSString *downloadDir = [RCTHotUpdate downloadDir];
|
|
|
|
NSString *bundlePath = [[downloadDir stringByAppendingPathComponent:loadVersioin] stringByAppendingPathComponent:BUNDLE_FILE_NAME];
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath:bundlePath isDirectory:NULL]) {
|
|
NSURL *bundleURL = [NSURL fileURLWithPath:bundlePath];
|
|
return bundleURL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return [RCTHotUpdate binaryBundleURL];
|
|
}
|
|
|
|
- (NSDictionary *)constantsToExport
|
|
{
|
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
|
|
NSMutableDictionary *ret = [NSMutableDictionary new];
|
|
ret[@"downloadRootDir"] = [RCTHotUpdate downloadDir];
|
|
ret[@"packageVersion"] = [RCTHotUpdate packageVersion];
|
|
ret[@"isRolledBack"] = [defaults objectForKey:keyRolledBackMarked];
|
|
ret[@"isFirstTime"] = [defaults objectForKey:keyFirstLoadMarked];
|
|
NSDictionary *updateInfo = [defaults dictionaryForKey:keyUpdateInfo];
|
|
ret[@"currentVersion"] = [updateInfo objectForKey:paramCurrentVersion];
|
|
|
|
// clear isFirstTimemarked
|
|
if ([[defaults objectForKey:keyFirstLoadMarked] boolValue]) {
|
|
[defaults setObject:nil forKey:keyFirstLoadMarked];
|
|
}
|
|
|
|
// clear rolledbackmark
|
|
if ([[defaults objectForKey:keyRolledBackMarked] boolValue]) {
|
|
[defaults setObject:nil forKey:keyRolledBackMarked];
|
|
[self clearInvalidFiles];
|
|
}
|
|
|
|
// clear packageupdatemarked
|
|
if ([[defaults objectForKey:KeyPackageUpdatedMarked] boolValue]) {
|
|
[defaults setObject:nil forKey:KeyPackageUpdatedMarked];
|
|
[self clearInvalidFiles];
|
|
}
|
|
[defaults synchronize];
|
|
|
|
return ret;
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
self = [super init];
|
|
if (self) {
|
|
_fileManager = [RCTHotUpdateManager new];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary *)options
|
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
{
|
|
[self hotUpdate:HotUpdateTypeFullDownload options:options callback:^(NSError *error) {
|
|
if (error) {
|
|
reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error);
|
|
}
|
|
else {
|
|
resolve(nil);
|
|
}
|
|
}];
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(downloadPatchFromPackage:(NSDictionary *)options
|
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
{
|
|
[self hotUpdate:HotUpdateTypePatchFromPackage options:options callback:^(NSError *error) {
|
|
if (error) {
|
|
reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error);
|
|
}
|
|
else {
|
|
resolve(nil);
|
|
}
|
|
}];
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(downloadPatchFromPpk:(NSDictionary *)options
|
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
{
|
|
[self hotUpdate:HotUpdateTypePatchFromPpk options:options callback:^(NSError *error) {
|
|
if (error) {
|
|
reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error);
|
|
}
|
|
else {
|
|
resolve(nil);
|
|
}
|
|
}];
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(setNeedUpdate:(NSDictionary *)options)
|
|
{
|
|
NSString *hashName = options[@"hashName"];
|
|
if (hashName.length) {
|
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
NSString *lastVersion = nil;
|
|
if ([defaults objectForKey:keyUpdateInfo]) {
|
|
NSDictionary *updateInfo = [defaults objectForKey:keyUpdateInfo];
|
|
lastVersion = updateInfo[paramCurrentVersion];
|
|
}
|
|
|
|
NSMutableDictionary *newInfo = [[NSMutableDictionary alloc] init];
|
|
newInfo[paramCurrentVersion] = hashName;
|
|
newInfo[paramLastVersion] = lastVersion;
|
|
newInfo[paramIsFirstTime] = @(YES);
|
|
newInfo[paramIsFirstLoadOk] = @(NO);
|
|
newInfo[paramPackageVersion] = [RCTHotUpdate packageVersion];
|
|
[defaults setObject:newInfo forKey:keyUpdateInfo];
|
|
|
|
[defaults synchronize];
|
|
}
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(reloadUpdate:(NSDictionary *)options)
|
|
{
|
|
NSString *hashName = options[@"hashName"];
|
|
if (hashName.length) {
|
|
[self setNeedUpdate:options];
|
|
|
|
// reload
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[_bridge setValue:[[self class] bundleURL] forKey:@"bundleURL"];
|
|
[_bridge reload];
|
|
});
|
|
}
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(markSuccess)
|
|
{
|
|
// update package info
|
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
NSMutableDictionary *packageInfo = [[NSMutableDictionary alloc] initWithDictionary:[defaults objectForKey:keyUpdateInfo]];
|
|
[packageInfo setObject:@(NO) forKey:paramIsFirstTime];
|
|
[packageInfo setObject:@(YES) forKey:paramIsFirstLoadOk];
|
|
[defaults setObject:packageInfo forKey:keyUpdateInfo];
|
|
[defaults synchronize];
|
|
|
|
// clear other package dir
|
|
[self clearInvalidFiles];
|
|
}
|
|
|
|
#pragma mark - private
|
|
- (void)hotUpdate:(HotUpdateType)type options:(NSDictionary *)options callback:(void (^)(NSError *error))callback
|
|
{
|
|
NSString *updateUrl = [RCTConvert NSString:options[@"updateUrl"]];
|
|
NSString *hashName = [RCTConvert NSString:options[@"hashName"]];
|
|
if (updateUrl.length<=0 || hashName.length<=0) {
|
|
callback([self errorWithMessage:ERROR_OPTIONS]);
|
|
return;
|
|
}
|
|
NSString *originHashName = [RCTConvert NSString:options[@"originHashName"]];
|
|
if (type == HotUpdateTypePatchFromPpk && originHashName<=0) {
|
|
callback([self errorWithMessage:ERROR_OPTIONS]);
|
|
return;
|
|
}
|
|
|
|
NSString *dir = [RCTHotUpdate downloadDir];
|
|
BOOL success = [_fileManager createDir:dir];
|
|
if (!success) {
|
|
callback([self errorWithMessage:ERROR_FILE_OPERATION]);
|
|
return;
|
|
}
|
|
|
|
NSString *zipFilePath = [dir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@%@",hashName, [self zipExtension:type]]];
|
|
NSString *unzipDir = [dir stringByAppendingPathComponent:hashName];
|
|
|
|
RCTLogInfo(@"RNUpdate -- download file %@", updateUrl);
|
|
[RCTHotUpdateDownloader download:updateUrl savePath:zipFilePath progressHandler:^(long long receivedBytes, long long totalBytes) {
|
|
[self.bridge.eventDispatcher sendAppEventWithName:EVENT_PROGRESS_DOWNLOAD
|
|
body:@{
|
|
PARAM_PROGRESS_HASHNAME:hashName,
|
|
PARAM_PROGRESS_RECEIVED:[NSNumber numberWithLongLong:receivedBytes],
|
|
PARAM_PROGRESS_TOTAL:[NSNumber numberWithLongLong:totalBytes]
|
|
}];
|
|
} completionHandler:^(NSString *path, NSError *error) {
|
|
if (error) {
|
|
callback(error);
|
|
}
|
|
else {
|
|
RCTLogInfo(@"RNUpdate -- unzip file %@", zipFilePath);
|
|
NSString *unzipFilePath = [dir stringByAppendingPathComponent:hashName];
|
|
[_fileManager unzipFileAtPath:zipFilePath toDestination:unzipFilePath progressHandler:^(NSString *entry,long entryNumber, long total) {
|
|
[self.bridge.eventDispatcher sendAppEventWithName:EVENT_PROGRESS_UNZIP
|
|
body:@{
|
|
PARAM_PROGRESS_HASHNAME:hashName,
|
|
PARAM_PROGRESS_RECEIVED:[NSNumber numberWithLong:entryNumber],
|
|
PARAM_PROGRESS_TOTAL:[NSNumber numberWithLong:total]
|
|
}];
|
|
|
|
} completionHandler:^(NSString *path, BOOL succeeded, NSError *error) {
|
|
dispatch_async(_methodQueue, ^{
|
|
if (error) {
|
|
callback(error);
|
|
}
|
|
else {
|
|
switch (type) {
|
|
case HotUpdateTypePatchFromPackage:
|
|
{
|
|
NSString *sourceOrigin = [[NSBundle mainBundle] resourcePath];
|
|
NSString *bundleOrigin = [[RCTHotUpdate binaryBundleURL] path];
|
|
[self patch:hashName fromBundle:bundleOrigin source:sourceOrigin callback:callback];
|
|
}
|
|
break;
|
|
case HotUpdateTypePatchFromPpk:
|
|
{
|
|
NSString *lastVertionDir = [dir stringByAppendingPathComponent:originHashName];
|
|
|
|
NSString *sourceOrigin = lastVertionDir;
|
|
NSString *bundleOrigin = [lastVertionDir stringByAppendingPathComponent:BUNDLE_FILE_NAME];
|
|
[self patch:hashName fromBundle:bundleOrigin source:sourceOrigin callback:callback];
|
|
}
|
|
break;
|
|
default:
|
|
callback(nil);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}];
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)patch:(NSString *)hashName fromBundle:(NSString *)bundleOrigin source:(NSString *)sourceOrigin callback:(void (^)(NSError *error))callback
|
|
{
|
|
NSString *unzipDir = [[RCTHotUpdate downloadDir] stringByAppendingPathComponent:hashName];
|
|
NSString *sourcePatch = [unzipDir stringByAppendingPathComponent:SOURCE_PATCH_NAME];
|
|
NSString *bundlePatch = [unzipDir stringByAppendingPathComponent:BUNDLE_PATCH_NAME];
|
|
|
|
NSString *destination = [unzipDir stringByAppendingPathComponent:BUNDLE_FILE_NAME];
|
|
[_fileManager bsdiffFileAtPath:bundlePatch fromOrigin:bundleOrigin toDestination:destination completionHandler:^(BOOL success) {
|
|
if (success) {
|
|
NSData *data = [NSData dataWithContentsOfFile:sourcePatch];
|
|
NSError *error = nil;
|
|
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
|
|
if (error) {
|
|
callback(error);
|
|
return;
|
|
}
|
|
|
|
NSDictionary *copies = json[@"copies"];
|
|
NSDictionary *deletes = json[@"deletes"];
|
|
|
|
[_fileManager copyFiles:copies fromDir:sourceOrigin toDir:unzipDir deletes:deletes completionHandler:^(NSError *error) {
|
|
if (error) {
|
|
callback(error);
|
|
}
|
|
else {
|
|
callback(nil);
|
|
}
|
|
}];
|
|
}
|
|
else {
|
|
callback([self errorWithMessage:ERROR_BSDIFF]);
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)clearInvalidFiles
|
|
{
|
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
NSDictionary *updateInfo = [defaults objectForKey:keyUpdateInfo];
|
|
NSString *curVersion = [updateInfo objectForKey:paramCurrentVersion];
|
|
|
|
NSString *downloadDir = [RCTHotUpdate downloadDir];
|
|
NSError *error = nil;
|
|
NSArray *list = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:downloadDir error:&error];
|
|
if (error) {
|
|
return;
|
|
}
|
|
|
|
for(NSString *fileName in list) {
|
|
if (![fileName isEqualToString:curVersion]) {
|
|
[_fileManager removeFile:[downloadDir stringByAppendingPathComponent:fileName] completionHandler:nil];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (NSString *)zipExtension:(HotUpdateType)type
|
|
{
|
|
switch (type) {
|
|
case HotUpdateTypeFullDownload:
|
|
return @".ppk";
|
|
case HotUpdateTypePatchFromPackage:
|
|
return @".apk.patch";
|
|
case HotUpdateTypePatchFromPpk:
|
|
return @".ppk.patch";
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
- (NSError *)errorWithMessage:(NSString *)errorMessage
|
|
{
|
|
return [NSError errorWithDomain:@"cn.reactnative.hotupdate"
|
|
code:-1
|
|
userInfo:@{ NSLocalizedDescriptionKey: errorMessage}];
|
|
}
|
|
|
|
+ (NSString *)downloadDir
|
|
{
|
|
NSString *directory = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) firstObject];
|
|
NSString *downloadDir = [directory stringByAppendingPathComponent:@"reactnativecnhotupdate"];
|
|
|
|
return downloadDir;
|
|
}
|
|
|
|
+ (NSURL *)binaryBundleURL
|
|
{
|
|
NSURL *url = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
|
|
return url;
|
|
}
|
|
|
|
+ (NSString *)packageVersion
|
|
{
|
|
static NSString *version = nil;
|
|
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
|
|
version = [infoDictionary objectForKey:@"CFBundleShortVersionString"];
|
|
});
|
|
return version;
|
|
}
|
|
|
|
@end |