RNAutoUpdater.m 12.1 KB
//
//  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