|
|
//
|
|
|
// RNAutoUpdater.m
|
|
|
// YH_Vendor
|
|
|
//
|
|
|
// Created by 盖剑秋 on 16/6/2.
|
|
|
// Copyright © 2016年 Facebook. All rights reserved.
|
|
|
//
|
|
|
|
|
|
#import "RNAutoUpdater.h"
|
|
|
#import "RCTBridgeModule.h"
|
|
|
#import <CommonCrypto/CommonDigest.h>
|
|
|
#import "SSZipArchive.h"
|
|
|
|
|
|
#ifdef DEBUG
|
|
|
# define DLog(fmt, ...) NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__);
|
|
|
#else
|
|
|
# define DLog(...)
|
|
|
#endif
|
|
|
|
|
|
#define kYH_JSBundleVersion(appVersion) [NSString stringWithFormat:@"YH_JSBundleVersion_%@", appVersion]
|
|
|
|
|
|
#define kRNBundleURL @"http://m.yohobuy.com/rn/v1"
|
|
|
#define kFileMD5Key @"yohorn2016"
|
|
|
|
|
|
typedef void (^YH_UpdateCallback)(NSError *error);
|
|
|
|
|
|
typedef enum {
|
|
|
YH_UpdateErrorHTTPStatusFailed = -1001,
|
|
|
YH_UpdateErrorResponseDataFailed,
|
|
|
YH_UpdateErrorFetchFileFailed,
|
|
|
YH_UpdateErrorVerifyFailed,
|
|
|
YH_UpdateErrorUnzipFailed,
|
|
|
YH_UpdateErrorCannotFindJSBundle,
|
|
|
} YH_UpdateError;
|
|
|
|
|
|
@interface RNAutoUpdater()<RCTBridgeModule>
|
|
|
|
|
|
@end
|
|
|
|
|
|
@implementation RNAutoUpdater
|
|
|
|
|
|
RCT_EXPORT_MODULE()
|
|
|
|
|
|
RCT_EXPORT_METHOD(checkUpdate:(NSDictionary *)options){
|
|
|
|
|
|
INSTALL_MODE mode = [[options objectForKey:@"installMode"] integerValue];
|
|
|
|
|
|
[[self class] checkUpdateWithCallback:^(NSError *error) {
|
|
|
|
|
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
- (NSDictionary *)constantsToExport {
|
|
|
|
|
|
return @{
|
|
|
@"jsBundleVersion":[[self class] currentVersion],
|
|
|
@"INSTALL_MODE_IMMEDIATE":@(INSTALL_MODE_IMMEDIATE),
|
|
|
@"INSTALL_MODE_ON_NEXT_START":@(INSTALL_MODE_ON_NEXT_START),
|
|
|
@"INSTALL_MODE_ON_NEXT_RESUME":@(INSTALL_MODE_ON_NEXT_RESUME),
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+ (void)checkUpdate
|
|
|
{
|
|
|
[self checkUpdateWithCallback:^(NSError *error) {
|
|
|
if (!error) {
|
|
|
|
|
|
}
|
|
|
}];
|
|
|
}
|
|
|
|
|
|
+ (NSURL *)currentJSBundleLocation
|
|
|
{
|
|
|
NSURL *bundleURL;
|
|
|
NSString *version = [self currentVersion];
|
|
|
if ([version isEqualToString:@""]) {
|
|
|
bundleURL = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
|
|
|
} else {
|
|
|
NSString *scriptDirectory = [self fetchScriptDirectory];
|
|
|
NSString *fileName = [self fileName:[self currentVersion]];
|
|
|
NSString *filePath = [scriptDirectory stringByAppendingPathComponent:fileName];
|
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
|
|
|
bundleURL = [NSURL URLWithString:filePath];
|
|
|
} else {
|
|
|
bundleURL = nil;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return bundleURL;
|
|
|
}
|
|
|
|
|
|
+ (NSMutableURLRequest *)createCheckUpdateRequest
|
|
|
{
|
|
|
//create url request
|
|
|
NSURL *hotfixURL = [NSURL URLWithString:kRNBundleURL];
|
|
|
NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:hotfixURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:20.0];
|
|
|
mutableRequest.HTTPMethod = @"POST";
|
|
|
|
|
|
NSString *appVersion = [self containerVersion];
|
|
|
|
|
|
|
|
|
NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
|
|
|
parameters[@"app_version"] = appVersion;
|
|
|
parameters[@"client_type"] = @"iOS";
|
|
|
|
|
|
NSMutableArray *pairs = [NSMutableArray array];
|
|
|
for (NSString *key in [parameters allKeys]) {
|
|
|
NSString *value = parameters[key];
|
|
|
[pairs addObject:[NSString stringWithFormat:@"%@=%@", key, value]];
|
|
|
}
|
|
|
NSString *bodyString = [pairs componentsJoinedByString:@"&"];
|
|
|
|
|
|
[mutableRequest setHTTPBody:[bodyString dataUsingEncoding:NSUTF8StringEncoding]];
|
|
|
|
|
|
return mutableRequest;
|
|
|
}
|
|
|
|
|
|
+ (void)checkUpdateWithCallback:(YH_UpdateCallback)callback
|
|
|
{
|
|
|
DLog(@"YH_JSBundle: checkUpdate");
|
|
|
|
|
|
DLog(@"YH_JSBundle: request file %@", kRNBundleURL);
|
|
|
|
|
|
NSMutableURLRequest *mutableRequest = [self createCheckUpdateRequest];
|
|
|
|
|
|
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:mutableRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
|
|
|
|
|
if (error) {
|
|
|
DLog(@"YH_JSBundle: request RNBundleURL failure, error:%@", error);
|
|
|
if (callback) {
|
|
|
callback(error);
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
//解析json
|
|
|
NSError *jsonError;
|
|
|
NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
|
|
|
if (!result) {
|
|
|
DLog(@"YH_JSBundle: unrecongized json string, error:%@", jsonError);
|
|
|
if (callback) {
|
|
|
callback(jsonError);
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
DLog(@"YH_JSBundle: request RNBundleURL success, response data:%@", result);
|
|
|
|
|
|
//接口数据处理
|
|
|
NSInteger code = [result[@"code"] integerValue];
|
|
|
if (code != 200) {
|
|
|
DLog(@"YH_JSBundle: response status error, code:%ld", code);
|
|
|
if (callback) {
|
|
|
callback([NSError errorWithDomain:@"com.yohobuy" code:YH_UpdateErrorHTTPStatusFailed userInfo:nil]);
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
NSDictionary *bundleData = result[@"data"];
|
|
|
NSString *zipURLString = bundleData[@"url"];
|
|
|
NSString *bundleVersion = bundleData[@"rnv"];
|
|
|
NSString *fileMD5 = bundleData[@"filecode"];
|
|
|
NSString *minContainerVersion = bundleData[@"minv"];
|
|
|
|
|
|
//获取脚本
|
|
|
[self fetchPatchFile:zipURLString version:bundleVersion md5:fileMD5 minContainerVersion:minContainerVersion callback:callback];
|
|
|
|
|
|
}];
|
|
|
[task resume];
|
|
|
|
|
|
}
|
|
|
|
|
|
+ (void)fetchPatchFile:(NSString *)fileURLString version:(NSString *)version md5:(NSString *)fileMD5 minContainerVersion:(NSString *)minContainerVersion callback:(YH_UpdateCallback)callback
|
|
|
{
|
|
|
if ([fileURLString isEqualToString:@""] || [version isEqualToString:@""] || [fileMD5 isEqualToString:@""]) {
|
|
|
DLog(@"YH_JSBundle: fetch js bundle file fail, url:%@, version:%@, md5:%@", fileURLString, version, fileMD5);
|
|
|
if (callback) {
|
|
|
callback([NSError errorWithDomain:@"com.yohobuy" code:YH_UpdateErrorResponseDataFailed userInfo:nil]);
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
BOOL shouldDownload = [self shouldDownloadUpdateJSBundle:version forMinContainerVersion:minContainerVersion];
|
|
|
if (!shouldDownload) {
|
|
|
DLog(@"YH_JSBundle: js bundle file already exist");
|
|
|
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:fileURLString] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:20.0];
|
|
|
NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
|
|
|
if (error) {
|
|
|
DLog(@"YH_JSBundle: fetch patch file fail, error:%@", error);
|
|
|
if (callback) {
|
|
|
callback([NSError errorWithDomain:@"com.yohobuy" code:YH_UpdateErrorFetchFileFailed userInfo:nil]);
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
NSString *appVersion = [self containerVersion];
|
|
|
|
|
|
//脚本临时存放路径
|
|
|
NSString *downloadTmpPath = [NSString stringWithFormat:@"%@jsbundle_%@_%@.zip", NSTemporaryDirectory(), appVersion, version];
|
|
|
NSString *unzipTmpDirectory = [NSString stringWithFormat:@"%@jsbundle_%@_%@_unzip/", NSTemporaryDirectory(), appVersion, version];
|
|
|
|
|
|
[data writeToFile:downloadTmpPath atomically:YES];
|
|
|
|
|
|
//验证文件签名
|
|
|
BOOL pass = [self verifyFileSign:downloadTmpPath realSign:fileMD5];
|
|
|
if (!pass) {
|
|
|
DLog(@"YH_JSBundle: file is broken");
|
|
|
//清除临时文件
|
|
|
[[NSFileManager defaultManager] removeItemAtPath:downloadTmpPath error:nil];
|
|
|
if (callback) {
|
|
|
callback([NSError errorWithDomain:@"com.yohobuy" code:YH_UpdateErrorVerifyFailed userInfo:nil]);
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
//解压zip
|
|
|
BOOL success = [SSZipArchive unzipFileAtPath:downloadTmpPath toDestination:unzipTmpDirectory];
|
|
|
if (!success) {
|
|
|
DLog(@"YH_JSBundle: unzip file error");
|
|
|
[[NSFileManager defaultManager] removeItemAtPath:unzipTmpDirectory error:nil];
|
|
|
if (callback) {
|
|
|
callback([NSError errorWithDomain:@"com.yohobuy" code:YH_UpdateErrorUnzipFailed userInfo:nil]);
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
//判断zip中是否存在 main.jsbundle
|
|
|
NSString *bundleTmpPath = [unzipTmpDirectory stringByAppendingPathComponent:@"main.jsbundle"];
|
|
|
if (![[NSFileManager defaultManager] fileExistsAtPath:bundleTmpPath]) {
|
|
|
DLog(@"YH_JSBundle: can not find js bundle in zip");
|
|
|
[[NSFileManager defaultManager] removeItemAtPath:unzipTmpDirectory error:nil];
|
|
|
if (callback) {
|
|
|
callback([NSError errorWithDomain:@"com.yohobuy" code:YH_UpdateErrorCannotFindJSBundle userInfo:nil]);
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
//存储js bundle
|
|
|
NSString *scriptDirectory = [self fetchScriptDirectory];
|
|
|
[[NSFileManager defaultManager] createDirectoryAtPath:scriptDirectory withIntermediateDirectories:YES attributes:nil error:nil];
|
|
|
NSString *fileName = [self fileName:version];
|
|
|
NSString *newFilePath = [scriptDirectory stringByAppendingPathComponent:fileName];
|
|
|
[[NSData dataWithContentsOfFile:bundleTmpPath] writeToFile:newFilePath atomically:YES];
|
|
|
|
|
|
DLog(@"YH_JSBundle: fetch patch file success, path:%@", newFilePath);
|
|
|
|
|
|
//存储最新patch版本号
|
|
|
[[NSUserDefaults standardUserDefaults] setObject:version forKey:kYH_JSBundleVersion(appVersion)];
|
|
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
|
|
|
|
if (callback) {
|
|
|
callback(nil);
|
|
|
}
|
|
|
|
|
|
//清除临时文件和目录
|
|
|
[[NSFileManager defaultManager] removeItemAtPath:downloadTmpPath error:nil];
|
|
|
[[NSFileManager defaultManager] removeItemAtPath:unzipTmpDirectory error:nil];
|
|
|
|
|
|
}];
|
|
|
[task resume];
|
|
|
|
|
|
}
|
|
|
|
|
|
+ (BOOL)shouldDownloadUpdateJSBundle:(NSString *)bundleVersion forMinContainerVersion:(NSString *)minContainerVersion
|
|
|
{
|
|
|
NSString *containerVersion = [self containerVersion];
|
|
|
//应用版本号 < bundle要求的最低版本号,不下载更新
|
|
|
if ([containerVersion compare:minContainerVersion options:NSNumericSearch] == NSOrderedAscending) {
|
|
|
return NO;
|
|
|
}
|
|
|
|
|
|
//本地bundle版本号 == 最新bundle版本号,不下载更新
|
|
|
if ([[self currentVersion] isEqualToString:bundleVersion]) {
|
|
|
return NO;
|
|
|
}
|
|
|
|
|
|
return YES;
|
|
|
}
|
|
|
|
|
|
+ (BOOL)verifyFileSign:(NSString *)filePath realSign:(NSString *)realSign
|
|
|
{
|
|
|
NSString *fileSign = [self fileMD5:filePath];
|
|
|
NSString *joinString = [NSString stringWithFormat:@"%@%@", fileSign, kFileMD5Key];
|
|
|
NSString *actualSign = [self stringMD5:joinString];
|
|
|
|
|
|
if ([actualSign isEqualToString:realSign]) {
|
|
|
return YES;
|
|
|
} else {
|
|
|
return NO;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ (NSString *)fileName:(NSString *)version
|
|
|
{
|
|
|
NSString *name = [NSString stringWithFormat:@"main_%@.jsbundle", version];
|
|
|
return name;
|
|
|
}
|
|
|
|
|
|
+ (NSString *)containerVersion
|
|
|
{
|
|
|
return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
|
|
|
}
|
|
|
|
|
|
+ (NSString *)currentVersion
|
|
|
{
|
|
|
NSString *appVersion = [self containerVersion];
|
|
|
NSString *version = [[NSUserDefaults standardUserDefaults] stringForKey:kYH_JSBundleVersion(appVersion)];
|
|
|
return version ?: @"";
|
|
|
}
|
|
|
|
|
|
+ (NSString *)fetchScriptDirectory
|
|
|
{
|
|
|
NSString *appVersion = [self containerVersion];
|
|
|
NSString *libraryDirectory = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) firstObject];
|
|
|
NSString *scriptDirectory = [libraryDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"YH_JSBundle/%@/", appVersion]];
|
|
|
return scriptDirectory;
|
|
|
}
|
|
|
|
|
|
|
|
|
#pragma mark utils
|
|
|
|
|
|
+ (NSString *)fileMD5:(NSString *)filePath
|
|
|
{
|
|
|
NSFileHandle *handle = [NSFileHandle fileHandleForReadingAtPath:filePath];
|
|
|
if(!handle)
|
|
|
{
|
|
|
return nil;
|
|
|
}
|
|
|
|
|
|
CC_MD5_CTX md5;
|
|
|
CC_MD5_Init(&md5);
|
|
|
BOOL done = NO;
|
|
|
while (!done)
|
|
|
{
|
|
|
NSData *fileData = [handle readDataOfLength:256];
|
|
|
CC_MD5_Update(&md5, [fileData bytes], (CC_LONG)[fileData length]);
|
|
|
if([fileData length] == 0)
|
|
|
done = YES;
|
|
|
}
|
|
|
|
|
|
unsigned char digest[CC_MD5_DIGEST_LENGTH];
|
|
|
CC_MD5_Final(digest, &md5);
|
|
|
|
|
|
NSString *result = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
|
|
|
digest[0], digest[1],
|
|
|
digest[2], digest[3],
|
|
|
digest[4], digest[5],
|
|
|
digest[6], digest[7],
|
|
|
digest[8], digest[9],
|
|
|
digest[10], digest[11],
|
|
|
digest[12], digest[13],
|
|
|
digest[14], digest[15]];
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
+ (NSString *)stringMD5:(NSString *)rawString
|
|
|
{
|
|
|
const char *input = [rawString UTF8String];
|
|
|
unsigned char result[CC_MD5_DIGEST_LENGTH];
|
|
|
CC_MD5(input, (CC_LONG)strlen(input), result);
|
|
|
|
|
|
NSMutableString *digest = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
|
|
|
for (NSInteger i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
|
|
|
[digest appendFormat:@"%02x", result[i]];
|
|
|
}
|
|
|
|
|
|
return digest;
|
|
|
}
|
|
|
|
|
|
@end |
...
|
...
|
|