diff --git a/.gitignore b/.gitignore index c4fa1f63..c8f8d743 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,8 @@ pids # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git node_modules -coverage \ No newline at end of file +coverage + +# vs code +.history +.idea \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 0d59c5dd..3a0d6580 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ services: - mysql node_js: + - "8.10.0" - "7.5" - "7" - "6.9.5" @@ -27,4 +28,4 @@ before_install: - "test ! -d node_modules || npm prune" - "test ! -d node_modules || npm rebuild" script: "npm run-script coverage" -after_script: "npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls" \ No newline at end of file +after_script: "npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..c6903211 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,67 @@ +# Changelog for code-push-server + +## 0.5.x + +## 新特性 +- 针对文本增量更新进行优化,使用google `diff-match-patch` 算法计算差异 + - react-native-code-push Android客户端适配,需要合并https://github.com/Microsoft/react-native-code-push/pull/1393, 才能正常使用文本增量更新功能。 + - react-native-code-push iOS客户端适配 (需要合并https://github.com/Microsoft/react-native-code-push/pull/1399) + - react-native-code-push Windows客户端适配 (进行中) + +## fixbug + +- 修复统计数据激活数 +- 修复灰度发布bug +- rollback后增加计算和最后一次增量更新版本 + +## 如何升级到该版本 + +### 升级数据库 + +`$ npm run upgrade` + +or + +`$ code-push-server-db upgrade` + + +## 0.4.x + +### 新特性 + +- targetBinaryVersion 支持正则匹配, `deployments_versions`新增字段`min_version`,`max_version` + - `*` 匹配所有版本 + - `1.2.3` 匹配特定版本`1.2.3` + - `1.2`/`1.2.*` 匹配所有1.2补丁版本 + - `>=1.2.3<1.3.7` + - `~1.2.3` 匹配`>=1.2.3<1.3.0` + - `^1.2.3` 匹配`>=1.2.3<2.0.0` +- 添加docker编排服务部署,更新文档 +- Support Tencent cloud cos storageType + +## 如何升级到该版本 + +- 升级数据库 +`$ ./bin/db upgrade` +or +`$ mysql codepush < ./sql/codepush-v0.4.0-patch.sql` + +- 处理存量数据 +``` shell + $ git clone https://github.com/lisong/tools + $ cd tools + $ npm i + $ vim ./bin/fixMinMaxVersion //修改数据配置 + $ node ./bin/fixMinMaxVersion //出现提示 success +``` + +## 0.3.x + +- 支持灰度发布 +- 适配`code-push app add` 命令,应用不再以名字区分平台,而是以类型区分平台 + - 数据库表apps新增字段`os`,`platform` +- 完善`code-push release/release-react/release-cordova` 命令 + - 数据库表packages新增`is_disabled`,`rollout`字段 +- 适配`code-push patch`命令 +- 新增`log_report_download`,`log_report_deploy`日志表 +- 升级npm依赖包 diff --git a/Makefile b/Makefile index 0ef8c45e..7e59757b 100644 --- a/Makefile +++ b/Makefile @@ -8,18 +8,20 @@ test: test-integration test-integration: @echo "\nRunning integration tests..." + @NODE_ENV=test CONFIG_FILE=${ROOT}/config/config.test.js mocha test/api/init @NODE_ENV=test PORT=3000 HOST=127.0.0.1 CONFIG_FILE=${ROOT}/config/config.test.js node bin/www & @NODE_ENV=test CONFIG_FILE=${ROOT}/config/config.test.js mocha \ - test/api/init test/api/users test/api/auth test/api/account test/api/accessKeys test/api/sessions test/api/apps test/api/index --recursive --timeout 15000 + test/api/users test/api/auth test/api/account test/api/accessKeys test/api/apps test/api/index --recursive --timeout 15000 coverage: @echo "\n\nRunning coverage report..." rm -rf coverage # @NODE_ENV=test CONFIG_FILE=${ROOT}/config/config.test.js ./node_modules/istanbul/lib/cli.js cover --report lcovonly --dir coverage/core ./node_modules/.bin/_mocha \ # test/unit -- -R spec --recursive --timeout 15000 + @NODE_ENV=test CONFIG_FILE=${ROOT}/config/config.test.js mocha test/api/init @NODE_ENV=test PORT=3000 HOST=127.0.0.1 CONFIG_FILE=${ROOT}/config/config.test.js node bin/www & @NODE_ENV=test CONFIG_FILE=${ROOT}/config/config.test.js ./node_modules/istanbul/lib/cli.js cover --report lcovonly --dir coverage/api ./node_modules/.bin/_mocha \ - test/api/init test/api/users test/api/auth test/api/account test/api/accessKeys test/api/sessions test/api/apps test/api/index -- -R spec --recursive --timeout 15000 + test/api/users test/api/auth test/api/account test/api/accessKeys test/api/apps test/api/index -- -R spec --recursive --timeout 15000 @NODE_ENV=test CONFIG_FILE=${ROOT}/config/config.test.js ./node_modules/istanbul/lib/cli.js report .PHONY: coverage \ No newline at end of file diff --git a/README.md b/README.md index 86972353..1611e8db 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # CodePush Server [source](https://github.com/lisong/code-push-server) +[![NPM](https://nodei.co/npm/code-push-server.svg?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/code-push-server/) + [![NPM Version](https://img.shields.io/npm/v/code-push-server.svg)](https://npmjs.org/package/code-push-server) [![Node.js Version](https://img.shields.io/node/v/code-push-server.svg)](https://nodejs.org/en/download/) [![Linux Status](https://img.shields.io/travis/lisong/code-push-server/master.svg?label=linux)](https://travis-ci.org/lisong/code-push-server) @@ -9,252 +11,87 @@ [![Known Vulnerabilities](https://snyk.io/test/npm/code-push-server/badge.svg)](https://snyk.io/test/npm/code-push-server) [![Licenses](https://img.shields.io/npm/l/code-push-server.svg)](https://spdx.org/licenses/MIT) -CodePush Server is a CodePush progam server! microsoft CodePush cloud is slow in China, we can use this to build our's. I use [qiniu](http://www.qiniu.com/) to store the files, because it's simple and quick! Or you can use local storage, just modify config.js file, it's simple configure. +CodePush Server is a CodePush progam server! microsoft CodePush cloud is slow in China, we can use this to build our's. I use [qiniu](http://www.qiniu.com/) to store the files, because it's simple and quick! Or you can use [local/s3/oss/tencentcloud] storage, just modify config.js file, it's simple configure. + -## qq交流群 535491067 +## Support Storage mode + +- local *storage bundle file in local machine* +- qiniu *storage bundle file in [qiniu](http://www.qiniu.com/)* +- s3 *storage bundle file in [aws](https://aws.amazon.com/)* +- oss *storage bundle file in [aliyun](https://www.aliyun.com/product/oss)* +- tencentcloud *storage bundle file in [tencentcloud](https://cloud.tencent.com/product/cos)* ## 正确使用code-push热更新 -- 苹果允许使用热更新[Apple's developer agreement](https://developer.apple.com/programs/ios/information/iOS_Program_Information_4_3_15.pdf), 但是规定不能弹框提示用户更新,影响用户体验。 而Google Play恰好相反,必须弹框告知用户更新。然而中国的android市场都必须关闭更新弹框,否则会在审核应用时以“请上传最新版本的二进制应用包”驳回应用。 +- 苹果App允许使用热更新[Apple's developer agreement](https://developer.apple.com/programs/ios/information/iOS_Program_Information_4_3_15.pdf), 为了不影响用户体验,规定必须使用静默更新。 Google Play不能使用静默更新,必须弹框告知用户App有更新。中国的android市场必须采用静默更新(如果弹框提示,App会被“请上传最新版本的二进制应用包”原因驳回)。 - react-native 不同平台bundle包不一样,在使用code-push-server的时候必须创建不同的应用来区分(eg. CodePushDemo-ios 和 CodePushDemo-android) - react-native-code-push只更新资源文件,不会更新java和Objective C,所以npm升级依赖包版本的时候,如果依赖包使用的本地化实现, 这时候必须更改应用版本号(ios修改Info.plist中的CFBundleShortVersionString, android修改build.gradle中的versionName), 然后重新编译app发布到应用商店。 - 推荐使用code-push release-react 命令发布应用,该命令合并了打包和发布命令(eg. code-push release-react CodePushDemo-ios ios -d Production) - 每次向App Store提交新的版本时,也应该基于该提交版本同时向code-push-server发布一个初始版本。(因为后面每次向code-push-server发布版本时,code-puse-server都会和初始版本比较,生成补丁版本) -## EXAMPLE -api.code-push.com 只是一个测试server,不要将自己生产环境的项目放在上面,服务器的宽带只有1M,而且服务没有做负载均衡和监控,稳定性不能保证,烦请大家自己搭建自己的服务。 -### shell命令行端 +### shell login ```shell -$ code-push login http://api.code-push.com:8080 #登录 +$ code-push login http://api.code-push.com #登录 ``` -### [web](http://www.code-push.com:8080) +### [web](http://www.code-push.com) -访问:http://www.code-push.com:8080 +访问:http://www.code-push.com -### 客户端eg. +### client eg. [ReactNative CodePushDemo](https://github.com/lisong/code-push-demo-app) [Cordova CodePushDemo](https://github.com/lisong/code-push-cordova-demo-app) -## INSTALL FROM NPM PACKAGE - -```shell -$ npm install code-push-server -g -$ code-push-server-db init --dbhost localhost --dbuser root --dbpassword #初始化mysql数据库 -$ code-push-server #启动服务 浏览器中打开 http://127.0.0.1:3000 -``` +## HOW TO INSTALL code-push-server -## INSTALL FROM SOURCE CODE +- [docker](https://github.com/lisong/code-push-server/blob/master/docker/README.md) (recommend) +- [manual operation](https://github.com/lisong/code-push-server/blob/master/docs/README.md) -```shell -$ git clone https://github.com/lisong/code-push-server.git -$ cd code-push-server -$ npm install -$ ./bin/db init --dbhost localhost --dbuser root --dbpassword #初始化mysql数据库 -$ ./bin/www #启动服务 浏览器中打开 http://127.0.0.1:3000 -``` +## DEFAULT ACCOUNT AND PASSWORD -## UPGRADE +- account: `admin` +- password: `123456` -*from source code* +## HOW TO USE -```shell -$ cd /path/to/code-push-server -$ git pull --rebase origin master -$ ./bin/db upgrade --dbhost localhost --dbuser root --dbpassword #升级codepush数据库 -$ #restart code-push-server -``` +- [normal](https://github.com/lisong/code-push-server/blob/master/docs/react-native-code-push.md) +- [react-native-code-push](https://github.com/Microsoft/react-native-code-push) +- [code-push](https://github.com/Microsoft/code-push) -*from npm package* -```shell -$ code-push-server-db upgrade --dbhost localhost --dbuser root --dbpassword #升级codepush数据库 -$ #restart code-push-server -``` +## ISSUES -## CONFIG -```shell -$ vim config/config.js -``` -请检查如下配置是否和你的环境一致,尤其是downloadUrl参数 +[code-push-server normal solution](https://github.com/lisong/code-push-server/issues/135) -``` - db: { - username: "root", - password: null, - database: "codepush", - host: "127.0.0.1", - dialect: "mysql" - }, - //七牛云存储配置 当storageType为qiniu时需要配置 - qiniu: { - accessKey: "", - secretKey: "", - bucketName: "", - downloadUrl: "" //文件下载域名地址 - }, - //阿里云存储配置 当storageType为oss时需要配置 - oss: { - accessKeyId: "", - secretAccessKey: "", - endpoint: "", - bucketName: "", - prefix: "", // 对象Key的前缀,允许放到子文件夹里面 - downloadUrl: "", // 文件下载域名地址,需要包含前缀 - }, - //文件存储在本地配置 当storageType为local时需要配置 - local: { - storageDir: "/Users/tablee/workspaces/storage", - //文件下载地址 CodePush Server 地址 + '/download' download对应app.js里面的地址 - downloadUrl: "http://localhost:3000/download", - // public static download spacename. - public: '/download' - }, - jwt: { - // 登录jwt签名密钥,必须更改,否则有安全隐患,可以使用随机生成的字符串 - // Recommended: 63 random alpha-numeric characters - // Generate using: https://www.grc.com/passwords.htm - tokenSecret: 'INSERT_RANDOM_TOKEN_KEY' - }, - common: { - dataDir: "/Users/tablee/workspaces/data", - //选择存储类型,目前支持local,oss,qiniu,s3配置 - storageType: "local" - }, -``` -read [config.js](https://github.com/lisong/code-push-server/blob/master/config/config.js) +[An unknown error occurred](https://github.com/lisong/code-push-server/issues?utf8=%E2%9C%93&q=unknown) +[modify password](https://github.com/lisong/code-push-server/issues/43) -## Storage mode [local/qiniu/s3] -- 配置local存储,修改config/config.js中storageType值为local,配置中local下面storageDir和downloadUrl,如果不在同一台机器上,downloadUrl请指定域名或者ip地址 +# UPDATE TIME LINE +- targetBinaryVersion support + - `*` + - `1.2.3` + - `1.2`/`1.2.*` + - `1.2.3 - 1.2.7` + - `>=1.2.3 <1.2.7` + - `~1.2.3` + - `^1.2.3` -## RUN -```shell -$ node ./bin/www # or code-push-server -``` - -or point config file and ENV - -```shell -$ CONFIG_FILE=/path/to/config.js NODE_ENV=production node ./bin/www # or CONFIG_FILE=/path/to/config.js NODE_ENV=production code-push-server -``` - -notice. you have to change `tokenSecret` in config.js for security. - -## Default listen Host/Port 0.0.0.0/3000 -you can change like this. - -```shell -$ PORT=3000 HOST=127.0.0.1 NODE_ENV=production node ./bin/www # or PORT=3000 HOST=127.0.0.1 NODE_ENV=production code-push-server -``` - -## [code-push-cli](https://github.com/Microsoft/code-push) -Use code-push-cli manager CodePushServer - -```shell -$ npm install code-push-cli@latest -g -$ code-push login http://127.0.0.1:3000 #login in browser account:admin password:123456 -``` +## Advance Feature -change admin password eg. +> use google diff-match-patch calculate text file diff patch -```shell -$ curl -X PATCH -H "Authorization: Bearer mytoken" -H "Accept: application/json" -H "Content-Type:application/json" -d '{"oldPassword":"123456","newPassword":"654321"}' http://127.0.0.1:3000/users/password -``` - -## [react-native-code-push](https://github.com/Microsoft/react-native-code-push) for react-native - -```shell -$ cd /path/to/project -$ npm install react-native-code-push@latest --save -``` - -## config react-native project -Follow the react-native-code-push docs, addition iOS add a new entry named CodePushServerURL, whose value is the key of ourself CodePushServer URL. Andriod use the new CodePush constructor in MainApplication point CodePushServerUrl - -iOS eg. in file Info.plist - -```xml -... -CodePushDeploymentKey -YourCodePushKey -CodePushServerURL -YourCodePushServerUrl -... -``` - -Android eg. in file MainApplication.java - -```java -@Override -protected List getPackages() { - return Arrays.asList( - new MainReactPackage(), - new CodePush( - "YourKey", - MainApplication.this, - BuildConfig.DEBUG, - "YourCodePushServerUrl" - ) - ); -} -``` - - -## [cordova-plugin-code-push](https://github.com/Microsoft/cordova-plugin-code-push) for cordova - -```shell -$ cd /path/to/project -$ cordova plugin add cordova-plugin-code-push@latest --save -``` - -## config cordova project - -edit config.xml. add code below. - -```xml - - - - - - - - -``` - -## Production Manage -use [pm2](http://pm2.keymetrics.io/) to manage process. - -```shell -$ npm install pm2 -g -$ cp config/config.js /path/to/production/config.js -$ vim /path/to/production/config.js #configure your env. -$ cp docs/process.json /path/to/production/process.json -$ vim /path/to/production/process.json #configure your env. -$ pm2 start /path/to/production/process.json -``` - -## Use [CodePush Web](https://github.com/lisong/code-push-web) manage apps - -add codePushWebUrl config in ./config/config.js - -eg. - -```json -... -"common": { - "codePushWebUrl": "Your CodePush Web address", -} -... -``` +- support iOS and Android +- use `"react-native-code-push": "git+https://git@github.com/lisong/react-native-code-push.git"` instead `"react-native-code-push": "x.x.x"` in `package.json` +- change `apps`.`is_use_diff_text` to `1` in mysql codepush database ## License MIT License [read](https://github.com/lisong/code-push-server/blob/master/LICENSE) diff --git a/app.js b/app.js index 172965ab..ee93e3cc 100644 --- a/app.js +++ b/app.js @@ -9,9 +9,9 @@ var _ = require('lodash'); var fs = require('fs'); var routes = require('./routes/index'); +var indexV1 = require('./routes/indexV1'); var auth = require('./routes/auth'); var accessKeys = require('./routes/accessKeys'); -var sessions = require('./routes/sessions'); var account = require('./routes/account'); var users = require('./routes/users'); var apps = require('./routes/apps'); @@ -33,16 +33,16 @@ app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); //use nginx in production -if (app.get('env') === 'development') { +//if (app.get('env') === 'development') { log.debug("set Access-Control Header"); app.all('*', function(req, res, next) { res.header("Access-Control-Allow-Origin", "*"); - res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization"); + res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-CodePush-Plugin-Version, X-CodePush-Plugin-Name, X-CodePush-SDK-Version"); res.header("Access-Control-Allow-Methods","PUT,POST,GET,PATCH,DELETE,OPTIONS"); log.debug("use set Access-Control Header"); next(); }); -} +//} log.debug("config common.storageType value: " + _.get(config, 'common.storageType')); @@ -73,9 +73,9 @@ if (_.get(config, 'common.storageType') === 'local') { } app.use('/', routes); +app.use('/v0.1/public/codepush', indexV1); app.use('/auth', auth); app.use('/accessKeys', accessKeys); -app.use('/sessions', sessions); app.use('/account', account); app.use('/users', users); app.use('/apps', apps); @@ -93,34 +93,27 @@ if (app.get('env') === 'development') { log.error(err); }); app.use(function(err, req, res, next) { - if (err instanceof AppError.AppError) { - res.send(err); - log.debug(err); - } else { - res.status(err.status || 500); - res.render('error', { - message: err.message, - error: err - }); - log.error(err); - } + res.status(err.status || 500); + res.render('error', { + message: err.message, + error: err + }); + log.error(err); }); } else { app.use(function(req, res, next) { var e = new AppError.NotFound(); - res.status(404).send(e); + res.status(404).send(e.message); log.debug(e); }); // production error handler // no stacktraces leaked to user app.use(function(err, req, res, next) { if (err instanceof AppError.AppError) { - res.send(err); + res.send(err.message); + log.debug(err); } else { - var status = err.status || 500; - var error = new AppError.AppError(`服务器繁忙,请稍后再试!`); - error.status = status; - res.status(status).send(error); + res.status(err.status || 500).send(err.message); log.error(err); } }); diff --git a/appveyor.yml b/appveyor.yml index 3fca1216..977b5a1e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,11 +3,7 @@ init: environment: matrix: - - nodejs_version: "7.5" - - nodejs_version: "7" - - nodejs_version: "6.9.5" - - nodejs_version: "6.1" - - nodejs_version: "6" + - nodejs_version: "8.10.0" services: - mysql @@ -27,4 +23,4 @@ test_script: - set CONFIG_FILE=config/config.testwin.js - node_modules\.bin\npm run test-win build: off -version: "{build}" \ No newline at end of file +version: "{build}" diff --git a/bin/db b/bin/db index a856921f..005b8810 100755 --- a/bin/db +++ b/bin/db @@ -6,8 +6,10 @@ var fs = require('fs'); var path = require('path'); var _ = require('lodash'); -var mysql = require('mysql'); +var mysql = require('mysql2'); var Promise = require("bluebird"); +var common = require("../core/utils/common"); +var constConfig = require('../core/const'); var yargs = require('yargs') .usage('Usage: $0 [options]') .command('init', '初始化数据库', { @@ -76,37 +78,50 @@ if (command === 'init') { if(connection2) connection2.end() }); } else if (command == 'upgrade'){ - var connection = mysql.createConnection({ - host: dbhost, - user: dbuser, - password: dbpassword, - database: dbname, - multipleStatements: true, - port: dbport - }); - Promise.promisifyAll(connection); - connection.connect(); + try { + var connection = mysql.createConnection({ + host: dbhost, + user: dbuser, + password: dbpassword, + database: dbname, + multipleStatements: true, + port: dbport + }); + Promise.promisifyAll(connection); + connection.connect() + } catch(e) { + console.error('connect mysql error, check params',e); + return; + } + return Promise.coroutine(function*(val){ var version_no = '0.0.1'; - try { - var rs = yield connection.queryAsync('select `version` from `versions` where `type`=1 limit 1'); - version_no = _.get(rs,'0.version', '0.0.1'); - } catch (e) { - } - if (version_no == '0.2.15') { + var rs = yield connection.queryAsync('select `version` from `versions` where `type`=1 limit 1'); + version_no = _.get(rs,'0.version', '0.0.1'); + if (version_no == constConfig.CURRENT_DB_VERSION) { console.log('Everything up-to-date.'); process.exit(0); } var allSqlFile = [ - {version:'0.2.14', 'path':path.resolve(__dirname, '../sql/codepush-v0.2.14.sql')}, - {version:'0.2.15', 'path':path.resolve(__dirname, '../sql/codepush-v0.2.15.sql')} + {version:'0.2.14', 'path':path.resolve(__dirname, '../sql/codepush-v0.2.14-patch.sql')}, + {version:'0.2.15', 'path':path.resolve(__dirname, '../sql/codepush-v0.2.15-patch.sql')}, + {version:'0.3.0', 'path':path.resolve(__dirname, '../sql/codepush-v0.3.0-patch.sql')}, + {version:'0.4.0', 'path':path.resolve(__dirname, '../sql/codepush-v0.4.0-patch.sql')}, + {version:'0.5.0', 'path':path.resolve(__dirname, '../sql/codepush-v0.5.0-patch.sql')} ]; for (var i = 0; i < allSqlFile.length; i++) { if(!_.gt(allSqlFile[i]['version'], version_no)) { continue; } - var sql = fs.readFileSync(allSqlFile[i]['path'], 'utf-8'); - yield connection.queryAsync(sql); + try { + var sql = fs.readFileSync(allSqlFile[i]['path'], 'utf-8'); + console.log('exec sql file:' + allSqlFile[i]['path']); + yield connection.queryAsync(sql); + console.log('success exec sql file:' + allSqlFile[i]['path']); + } catch (e) { + console.error('error exec sql file:' + allSqlFile[i]['path']); + throw e; + } } })() .then(function(){ diff --git a/bin/www b/bin/www index a34f74be..4f086a7b 100755 --- a/bin/www +++ b/bin/www @@ -9,7 +9,11 @@ var http = require('http'); var validator = require('validator') var _ = require('lodash') var config = require('../core/config'); -log4js.configure(_.get(config, 'log4js', {appenders: [{ type: 'console'}], levels : {"[all]": "ERROR", http: "ERROR"} })); +var constConfig = require('../core/const'); +log4js.configure(_.get(config, 'log4js', { + appenders: {console: { type: 'console'}}, + categories : { default: { appenders: ['console'], level: 'info' }} +})); var log = log4js.getLogger("startup") var app = require('../app'); @@ -45,8 +49,8 @@ var server = http.createServer(app); var models = require('../models'); models.Versions.findOne({where:{type:1}}) .then(function(v){ - if (!v || v.get('version') != '0.2.15') { - throw new Error(`Please upgrade your database. usage bin/db upgrade or code-push-server-db upgrade`); + if (!v || v.get('version') != constConfig.CURRENT_DB_VERSION) { + throw new Error('Please upgrade your database. usage `npm run upgrade` or `code-push-server-db upgrade`'); } server.listen(port, host); server.on('error', onError); diff --git a/config/config.js b/config/config.js index 79c4638b..0789b5ee 100644 --- a/config/config.js +++ b/config/config.js @@ -1,3 +1,5 @@ +var os = require('os'); + var config = {}; config.development = { // Config for database, only support mysql. @@ -8,7 +10,8 @@ config.development = { host: process.env.RDS_HOST || "127.0.0.1", port: process.env.RDS_PORT || 3306, dialect: "mysql", - logging: false + logging: false, + operatorsAliases: false, }, // Config for qiniu (http://www.qiniu.com/) cloud storage when storageType value is "qiniu". qiniu: { @@ -17,6 +20,14 @@ config.development = { bucketName: "", downloadUrl: "" // Binary files download host address. }, + // Config for upyun (https://www.upyun.com/) storage when storageType value is "upyun" + upyun: { + storageDir: process.env.UPYUN_STORAGE_DIR, + serviceName: process.env.UPYUN_SERVICE_NAME, + operatorName: process.env.UPYUN_OPERATOR_NAME, + operatorPass: process.env.UPYUN_OPERATOR_PASS, + downloadUrl: process.env.DOWNLOAD_URL, + }, // Config for Amazon s3 (https://aws.amazon.com/cn/s3/) storage when storageType value is "s3". s3: { accessKeyId: process.env.AWS_ACCESS_KEY_ID, @@ -35,14 +46,22 @@ config.development = { prefix: "", // Key prefix in object key downloadUrl: "", // binary files download host address. }, + // Config for tencentyun COS (https://cloud.tencent.com/product/cos) when storageType value is "oss". + tencentcloud: { + accessKeyId: "", + secretAccessKey: "", + bucketName: "", + region: "", + downloadUrl: "", // binary files download host address. + }, // Config for local storage when storageType value is "local". local: { // Binary files storage dir, Do not use tmpdir and it's public download dir. storageDir: process.env.STORAGE_DIR || "/Users/tablee/workspaces/storage", // Binary files download host address which Code Push Server listen to. the files storage in storageDir. - downloadUrl: process.env.LOCAL_DOWNLOAD_URL || "http://localhost:3000/download", + downloadUrl: process.env.LOCAL_DOWNLOAD_URL || "http://127.0.0.1:3000/download", // public static download spacename. - public: process.env.PUBLIC || '/download' + public: '/download' }, jwt: { // Recommended: 63 random alpha-numeric characters @@ -57,19 +76,21 @@ config.development = { */ tryLoginTimes: 0, // CodePush Web(https://github.com/lisong/code-push-web) login address. - //codePushWebUrl: "http://localhost:3001/login", + //codePushWebUrl: "http://127.0.0.1:3001/login", // create patch updates's number. default value is 3 diffNums: 3, // data dir for caclulate diff files. it's optimization. - dataDir: process.env.DATA_DIR || "/Users/tablee/workspaces/data", - // storageType which is your binary package files store. options value is ("local" | "qiniu" | "s3") + dataDir: process.env.DATA_DIR || os.tmpdir(), + // storageType which is your binary package files store. options value is ("local" | "qiniu" | "s3"| "oss" || "tencentcloud") storageType: process.env.STORAGE_TYPE || "local", // options value is (true | false), when it's true, it will cache updateCheck results in redis. - updateCheckCache: false + updateCheckCache: false, + // options value is (true | false), when it's true, it will cache rollout results in redis + rolloutClientUniqueIdCache: false, }, // Config for smtp email,register module need validate user email project source https://github.com/nodemailer/nodemailer smtpConfig:{ - host: "smtp.mxhichina.com", + host: "smtp.aliyun.com", port: 465, secure: true, auth: { @@ -103,13 +124,11 @@ config.development = { } config.development.log4js = { - appenders: [ - { type: 'console'} - ], - levels : { - "[all]": "ERROR", - "startup": "INFO", - "http" : "INFO" + appenders: {console: { type: 'console'}}, + categories : { + "default": { appenders: ['console'], level:'error'}, + "startup": { appenders: ['console'], level:'info'}, + "http": { appenders: ['console'], level:'info'} } } diff --git a/config/config.test.js b/config/config.test.js index fbfd4cd5..22990cff 100644 --- a/config/config.test.js +++ b/config/config.test.js @@ -9,11 +9,12 @@ config.test = { host: "127.0.0.1", port: 3306, dialect: "mysql", - logging: false + logging: false, + operatorsAliases: false, }, local: { storageDir: os.tmpdir(), - downloadUrl: "http://localhost:3000/download", + downloadUrl: "http://127.0.0.1:3000/download", public: '/download' }, jwt: { @@ -24,7 +25,8 @@ config.test = { diffNums: 3, dataDir: os.tmpdir(), storageType: "local", - updateCheckCache: true + updateCheckCache: true, + rolloutClientUniqueIdCache: false, }, smtpConfig: false, redis: { @@ -47,13 +49,11 @@ config.test = { } } config.test.log4js = { - appenders: [ - { type: 'console'} - ], - levels : { - "[all]": "ERROR", - "startup": "INFO", - "http" : "INFO" + appenders: {console: { type: 'console'}}, + categories : { + "default": { appenders: ['console'], level:'error'}, + "startup": { appenders: ['console'], level:'info'}, + "http": { appenders: ['console'], level:'info'} } } module.exports = config; diff --git a/config/config.testwin.js b/config/config.testwin.js index 2f6b1f82..2f5fa96d 100644 --- a/config/config.testwin.js +++ b/config/config.testwin.js @@ -9,11 +9,12 @@ config.test = { host: "127.0.0.1", port: 3306, dialect: "mysql", - logging: false + logging: false, + operatorsAliases: false, }, local: { storageDir: os.tmpdir(), - downloadUrl: "http://localhost:3000/download", + downloadUrl: "http://127.0.0.1:3000/download", public: '/download' }, jwt: { @@ -24,7 +25,8 @@ config.test = { diffNums: 3, dataDir: os.tmpdir(), storageType: "local", - updateCheckCache: true + updateCheckCache: true, + rolloutClientUniqueIdCache: false, }, smtpConfig: false, redis: { @@ -48,13 +50,11 @@ config.test = { } } config.test.log4js = { - appenders: [ - { type: 'console'} - ], - levels : { - "[all]": "ERROR", - "startup": "INFO", - "http" : "INFO" + appenders: {console: { type: 'console'}}, + categories : { + "default": { appenders: ['console'], level:'error'}, + "startup": { appenders: ['console'], level:'info'}, + "http": { appenders: ['console'], level:'info'} } } module.exports = config; diff --git a/core/config.js b/core/config.js index 6c8f71a7..a67eae86 100644 --- a/core/config.js +++ b/core/config.js @@ -1,15 +1,17 @@ -var env = process.env.NODE_ENV || 'development'; +var env = process.env.NODE_ENV || 'development'; var _ = require('lodash'); var path = require('path'); -var config = {}; var log4js = require('log4js'); var log = log4js.getLogger("cps:config"); var CONFIG_PATH = path.join(__dirname, '../config/config.js'); if (process.env.CONFIG_FILE) { CONFIG_PATH = path.join(__dirname, path.relative(__dirname, process.env.CONFIG_FILE)); - log.info(`process.env.CONFIG_FILE value: ` +process.env.CONFIG_FILE) + log.info(`process.env.CONFIG_FILE value: ${process.env.CONFIG_FILE}`) } log.info(`use config file ${CONFIG_PATH}`) log.info(`use env ${env}`) -config = _.get(require(CONFIG_PATH), env); +var config = _.get(require(CONFIG_PATH), env); +if (_.isEmpty(config)) { + throw new Error(`config is {}, check the env and config`); +} module.exports = config; diff --git a/core/const.js b/core/const.js new file mode 100644 index 00000000..f1d9b23b --- /dev/null +++ b/core/const.js @@ -0,0 +1,49 @@ +function define(name, value) { + Object.defineProperty(exports, name, { + value: value, + enumerable: true + }); +} + +//定义支持的平台 +define("IOS", 1); +define("IOS_NAME", 'iOS'); +define("ANDROID", 2); +define("ANDROID_NAME", 'Android'); +define("WINDOWS", 3); +define("WINDOWS_NAME", 'Windows'); + +//定义支持的应用类型 +define("REACT_NATIVE", 1); +define("REACT_NATIVE_NAME", 'React-Native'); +define("CORDOVA", 2); +define("CORDOVA_NAME", 'Cordova'); + +define("PRODUCTION", 'Production'); +define("STAGING", 'Staging'); + + +define("IS_MANDATORY_YES", 1); +define("IS_MANDATORY_NO", 0); + + +define("IS_DISABLED_YES", 1); +define("IS_DISABLED_NO", 0); + + +define("RELEAS_EMETHOD_PROMOTE", 'Promote'); +define("RELEAS_EMETHOD_UPLOAD", 'Upload'); + +define("DEPLOYMENT_SUCCEEDED", 1); +define("DEPLOYMENT_FAILED", 2); + +define("DIFF_MANIFEST_FILE_NAME", 'hotcodepush.json'); + +//文本文件是否使用google diff-match-patch 计算差异 +define("IS_USE_DIFF_TEXT_NO", 0); +define("IS_USE_DIFF_TEXT_YES", 1); + + +define("CURRENT_DB_VERSION", '0.5.0'); + + diff --git a/core/middleware.js b/core/middleware.js index da4676a2..fc4054f2 100644 --- a/core/middleware.js +++ b/core/middleware.js @@ -17,8 +17,9 @@ var checkAuthToken = function (authToken) { if (_.isEmpty(users)) { throw new AppError.Unauthorized(); } + var Sequelize = require('sequelize'); return models.UserTokens.findOne({ - where: {tokens: authToken, uid: users.id, expires_at: { gt: moment().format('YYYY-MM-DD HH:mm:ss') }} + where: {tokens: authToken, uid: users.id, expires_at: { [Sequelize.Op.gt]: moment().format('YYYY-MM-DD HH:mm:ss') }} }) .then((tokenInfo) => { if (_.isEmpty(tokenInfo)){ @@ -34,7 +35,7 @@ var checkAuthToken = function (authToken) { var checkAccessToken = function (accessToken) { return new Promise((resolve, reject) => { if (_.isEmpty(accessToken)) { - throw new AppError.Unauthorized(); + return reject(new AppError.Unauthorized()); } var config = require('../core/config'); var tokenSecret = _.get(config, 'jwt.tokenSecret'); @@ -42,7 +43,7 @@ var checkAccessToken = function (accessToken) { try { var authData = jwt.verify(accessToken, tokenSecret); } catch (e) { - reject(new AppError.Unauthorized()); + return reject(new AppError.Unauthorized()); } var uid = _.get(authData, 'uid', null); var hash = _.get(authData, 'hash', null); @@ -73,18 +74,19 @@ middleware.checkToken = function(req, res, next) { var authType = 1; var authToken = null; if (_.eq(authArr[0], 'Bearer')) { - authType = 1; authToken = authArr[1]; //Bearer + if (authToken && authToken.length > 64) { + authType = 2; + } else { + authType = 1; + } } else if(_.eq(authArr[0], 'Basic')) { authType = 2; var b = new Buffer(authArr[1], 'base64'); var user = _.split(b.toString(), ':'); authToken = _.get(user, '1'); - } else { - authType = 2; - authToken = _.trim(_.trimStart(_.get(req, 'query.access_token', null))); } - if (authType == 1) { + if (authToken && authType == 1) { checkAuthToken(authToken) .then((users) => { req.users = users; @@ -98,7 +100,7 @@ middleware.checkToken = function(req, res, next) { next(e); } }); - } else if (authType == 2) { + } else if (authToken && authType == 2) { checkAccessToken(authToken) .then((users) => { req.users = users; diff --git a/core/services/account-manager.js b/core/services/account-manager.js index dd4a06ef..0eae4a9e 100644 --- a/core/services/account-manager.js +++ b/core/services/account-manager.js @@ -67,13 +67,11 @@ proto.getAllAccessKeyByUid = function (uid) { .then((tokens) => { return _.map(tokens, function(v){ return { - id: v.id + "", name: '(hidden)', createdTime: parseInt(moment(v.created_at).format('x')), createdBy: v.created_by, expires: parseInt(moment(v.expires_at).format('x')), friendlyName: v.name, - isSession: v.is_session == 0 ? false : true, description: v.description, }; }); @@ -86,13 +84,12 @@ proto.isExsitAccessKeyName = function (uid, friendlyName) { }); }; -proto.createAccessKey = function (uid, newAccessKey, isSession, ttl, friendlyName, createdBy, description) { +proto.createAccessKey = function (uid, newAccessKey, ttl, friendlyName, createdBy, description) { return models.UserTokens.create({ uid: uid, name: friendlyName, tokens: newAccessKey, description: description, - is_session: isSession ? 1 : 0, created_by: createdBy, expires_at: moment().add(ttl/1000, 'seconds').format('YYYY-MM-DD HH:mm:ss'), created_at: moment().format('YYYY-MM-DD HH:mm:ss'), @@ -132,7 +129,8 @@ proto.login = function (account, password) { throw new AppError.AppError(`您输入密码错误次数超过限制,帐户已经锁定`); } return users; - }); + }) + .finally(() => client.quit()); } else { return users; } @@ -152,7 +150,8 @@ proto.login = function (account, password) { }) .then(() => { return client.incrAsync(loginKey); - }); + }) + .finally(() => client.quit()); } throw new AppError.AppError("您输入的邮箱或密码有误"); } else { @@ -178,10 +177,12 @@ proto.sendRegisterCode = function (email) { .then(() => { //将token临时存储到redis var token = security.randToken(40); - return factory.getRedisClient("default").setexAsync(`${REGISTER_CODE}${security.md5(email)}`, EXPIRED, token) + var client = factory.getRedisClient("default"); + return client.setexAsync(`${REGISTER_CODE}${security.md5(email)}`, EXPIRED, token) .then(() => { return token; - }); + }) + .finally(() => client.quit()); }) .then((token) => { //将token发送到用户邮箱 @@ -213,6 +214,7 @@ proto.checkRegisterCode = function (email, token) { } return ttl; }) + .finally(() => client.quit()); throw new AppError.AppError(`您输入的验证码不正确,请重新输入`); } return storageToken; diff --git a/core/services/app-manager.js b/core/services/app-manager.js index 0d484b49..b3a56811 100644 --- a/core/services/app-manager.js +++ b/core/services/app-manager.js @@ -17,21 +17,24 @@ proto.findAppByName = function (uid, appName) { return models.Apps.findOne({where: {name: appName, uid: uid}}); }; -proto.addApp = function (uid, appName, identical) { +proto.addApp = function (uid, appName, os, platform, identical) { return models.sequelize.transaction((t) => { return models.Apps.create({ name: appName, - uid: uid + uid: uid, + os: os, + platform: platform },{ transaction: t }) .then((apps) => { + var constName = require('../const'); var appId = apps.id; var deployments = []; var deploymentKey = security.randToken(28) + identical; deployments.push({ appid: appId, - name: 'Production', + name: constName.PRODUCTION, last_deployment_version_id: 0, label_id: 0, deployment_key: deploymentKey @@ -39,7 +42,7 @@ proto.addApp = function (uid, appName, identical) { deploymentKey = security.randToken(28) + identical; deployments.push({ appid: appId, - name: 'Staging', + name: constName.STAGING, last_deployment_version_id: 0, label_id: 0, deployment_key: deploymentKey @@ -91,13 +94,27 @@ proto.listApps = function (uid) { return []; } else { var appIds = _.map(data, (v) => { return v.appid }); - return models.Apps.findAll({where: {id: {in: appIds}}}); + var Sequelize = require('sequelize'); + return models.Apps.findAll({where: {id: {[Sequelize.Op.in]: appIds}}}); } }) .then((appInfos) => { var rs = Promise.map(_.values(appInfos), (v) => { return self.getAppDetailInfo(v, uid) .then((info) => { + var constName = require('../const'); + if (info.os == constName.IOS) { + info.os = constName.IOS_NAME; + } else if (info.os == constName.ANDROID) { + info.os = constName.ANDROID_NAME; + } else if (info.os == constName.WINDOWS) { + info.os = constName.WINDOWS_NAME; + } + if (info.platform == constName.REACT_NATIVE) { + info.platform = constName.REACT_NATIVE_NAME; + } else if (info.platform == constName.CORDOVA) { + info.platform = constName.CORDOVA_NAME; + } return info; }); }); @@ -128,8 +145,10 @@ proto.getAppDetailInfo = function (appInfo, currentUid) { deployments: _.map(deploymentInfos, (item) => { return _.get(item, 'name'); }), - - name: appInfo.get('name') + os: appInfo.get('os'), + platform: appInfo.get('platform'), + name: appInfo.get('name'), + id: appInfo.get('id') }); }); }; diff --git a/core/services/client-manager.js b/core/services/client-manager.js index da2dfdea..87a16625 100644 --- a/core/services/client-manager.js +++ b/core/services/client-manager.js @@ -5,6 +5,10 @@ var _ = require('lodash'); var common = require('../utils/common'); var factory = require('../utils/factory'); var AppError = require('../app-error'); +var config = require('../config'); +var log4js = require('log4js'); +var log = log4js.getLogger("cps:ClientManager"); +var Sequelize = require('sequelize'); var proto = module.exports = function (){ function ClientManager() { @@ -15,6 +19,7 @@ var proto = module.exports = function (){ }; const UPDATE_CHECK = "UPDATE_CHECK"; +const CHOSEN_MAN = "CHOSEN_MAN"; const EXPIRED = 600; proto.getUpdateCheckCacheKey = function(deploymentKey, appVersion, label, packageHash) { @@ -22,6 +27,7 @@ proto.getUpdateCheckCacheKey = function(deploymentKey, appVersion, label, packag } proto.clearUpdateCheckCache = function(deploymentKey, appVersion, label, packageHash) { + log.debug('clear cache Deployments key:', deploymentKey); let redisCacheKey = this.getUpdateCheckCacheKey(deploymentKey, appVersion, label, packageHash); var client = factory.getRedisClient("default"); return client.keysAsync(redisCacheKey) @@ -32,12 +38,13 @@ proto.clearUpdateCheckCache = function(deploymentKey, appVersion, label, package }); } return null; - }); + }) + .finally(() => client.quit()); } -proto.updateCheckFromCache = function(deploymentKey, appVersion, label, packageHash) { +proto.updateCheckFromCache = function(deploymentKey, appVersion, label, packageHash, clientUniqueId) { const self = this; - var updateCheckCache = _.get(require('../config'), 'common.updateCheckCache', false); + var updateCheckCache = _.get(config, 'common.updateCheckCache', false); if (updateCheckCache === false) { return self.updateCheck(deploymentKey, appVersion, label, packageHash); } @@ -47,14 +54,16 @@ proto.updateCheckFromCache = function(deploymentKey, appVersion, label, packageH .then((data) => { if (data) { try { + log.debug('updateCheckFromCache read from catch'); var obj = JSON.parse(data); return obj; } catch (e) { } } - return self.updateCheck(deploymentKey, appVersion, label, packageHash) + return self.updateCheck(deploymentKey, appVersion, label, packageHash, clientUniqueId) .then((rs) => { try { + log.debug('updateCheckFromCache read from db'); var strRs = JSON.stringify(rs); client.setexAsync(redisCacheKey, EXPIRED, strRs); } catch (e) { @@ -62,30 +71,103 @@ proto.updateCheckFromCache = function(deploymentKey, appVersion, label, packageH return rs; }); }) + .finally(() => client.quit()); +} + +proto.getChosenManCacheKey = function(packageId, rollout, clientUniqueId) { + return [CHOSEN_MAN, packageId, rollout, clientUniqueId].join(':'); +} + +proto.random = function(rollout) { + var r = Math.ceil(Math.random()*10000); + if (r < rollout * 100) { + return Promise.resolve(true); + } else { + return Promise.resolve(false); + } } -proto.updateCheck = function(deploymentKey, appVersion, label, packageHash) { +proto.chosenMan = function (packageId, rollout, clientUniqueId) { + var self = this; + if (rollout >= 100) { + return Promise.resolve(true); + } + var rolloutClientUniqueIdCache = _.get(config, 'common.rolloutClientUniqueIdCache', false); + if (rolloutClientUniqueIdCache === false) { + return self.random(rollout); + } else { + var client = factory.getRedisClient("default"); + var redisCacheKey = self.getChosenManCacheKey(packageId, rollout, clientUniqueId); + return client.getAsync(redisCacheKey) + .then((data) => { + if (data == 1) { + return true; + } else if (data == 2) { + return false; + } else { + return self.random(rollout) + .then((r)=>{ + return client.setexAsync(redisCacheKey, 60*60*24*7, r ? 1:2) + .then(()=>{ + return r; + }); + }); + } + }) + .finally(() => client.quit()); + } +} + +proto.updateCheck = function(deploymentKey, appVersion, label, packageHash, clientUniqueId) { var rs = { + packageId: 0, downloadURL: "", + downloadUrl: "", description: "", isAvailable: false, + isDisabled: true, isMandatory: false, appVersion: appVersion, + targetBinaryRange: "", packageHash: "", label: "", packageSize: 0, updateAppVersion: false, - shouldRunBinaryVersion: false + shouldRunBinaryVersion: false, + rollout: 100 }; + var self = this; if (_.isEmpty(deploymentKey) || _.isEmpty(appVersion)) { return Promise.reject(new AppError.AppError("please input deploymentKey and appVersion")) } return models.Deployments.findOne({where: {deployment_key: deploymentKey}}) .then((dep) => { if (_.isEmpty(dep)) { - throw new AppError.AppError('does not found deployment'); + throw new AppError.AppError('Not found deployment, check deployment key is right.'); } - return models.DeploymentsVersions.findOne({where: {deployment_id: dep.id, app_version: appVersion}}); + var version = common.parseVersion(appVersion); + return models.DeploymentsVersions.findAll({where: { + deployment_id: dep.id, + min_version: { [Sequelize.Op.lte]: version }, + max_version: { [Sequelize.Op.gt]: version } + }}) + .then((deploymentsVersionsMore) => { + var distance = 0; + var item = null; + _.map(deploymentsVersionsMore, function(value, index) { + if (index == 0) { + item = value; + distance = value.max_version - value.min_version; + } else { + if (distance > (value.max_version - value.min_version)) { + distance = value.max_version - value.min_version; + item = value; + } + } + }); + log.debug(item); + return item; + }); }) .then((deploymentsVersions) => { var packageId = _.get(deploymentsVersions, 'current_package_id', 0); @@ -97,26 +179,29 @@ proto.updateCheck = function(deploymentKey, appVersion, label, packageHash) { if (packages && _.eq(packages.deployment_id, deploymentsVersions.deployment_id) && !_.eq(packages.package_hash, packageHash)) { - rs.downloadURL = common.getBlobDownloadUrl(_.get(packages, 'blob_url')); + rs.packageId = packageId; + rs.targetBinaryRange = deploymentsVersions.app_version; + rs.downloadUrl = rs.downloadURL = common.getBlobDownloadUrl(_.get(packages, 'blob_url')); rs.description = _.get(packages, 'description', ''); - rs.isAvailable = true; + rs.isAvailable = _.eq(packages.is_disabled, 1) ? false : true; + rs.isDisabled = _.eq(packages.is_disabled, 1) ? true : false; rs.isMandatory = _.eq(packages.is_mandatory, 1) ? true : false; rs.appVersion = appVersion; rs.packageHash = _.get(packages, 'package_hash', ''); rs.label = _.get(packages, 'label', ''); rs.packageSize = _.get(packages, 'size', 0); - rs.shouldRunBinaryVersion = false; + rs.rollout = _.get(packages, 'rollout', 100); } return packages; }) - .then((packages) => { - //差异化更新 + //增量更新 if (!_.isEmpty(packages) && !_.eq(_.get(packages, 'package_hash', ""), packageHash)) { return models.PackagesDiff.findOne({where: {package_id:packages.id, diff_against_package_hash: packageHash}}) .then((diffPackage) => { if (!_.isEmpty(diffPackage)) { rs.downloadURL = common.getBlobDownloadUrl(_.get(diffPackage, 'diff_blob_url')); + rs.downloadUrl = common.getBlobDownloadUrl(_.get(diffPackage, 'diff_blob_url')); rs.packageSize = _.get(diffPackage, 'diff_size', 0); } return; @@ -153,26 +238,82 @@ proto.getPackagesInfo = function (deploymentKey, label) { proto.reportStatusDownload = function(deploymentKey, label, clientUniqueId) { return this.getPackagesInfo(deploymentKey, label) .then((packages) => { - return models.PackagesMetrics.addOneOnDownloadById(packages.id); + return Promise.all([ + models.PackagesMetrics.findOne({where: {package_id: packages.id}}) + .then((metrics)=>{ + if (metrics) { + return metrics.increment('downloaded'); + } + return; + }), + models.LogReportDownload.create({ + package_id: packages.id, + client_unique_id: clientUniqueId + }) + ]); }); }; proto.reportStatusDeploy = function (deploymentKey, label, clientUniqueId, others) { return this.getPackagesInfo(deploymentKey, label) .then((packages) => { - var status = _.get(others, "status"); + var constConfig = require('../const'); + var statusText = _.get(others, "status"); + var status = 0; + if (_.eq(statusText, "DeploymentSucceeded")) { + status = constConfig.DEPLOYMENT_SUCCEEDED; + } else if (_.eq(statusText, "DeploymentFailed")) { + status = constConfig.DEPLOYMENT_FAILED; + } var packageId = packages.id; - if (_.eq(status, "DeploymentSucceeded")) { - return Promise.all([ - models.PackagesMetrics.addOneOnInstalledById(packageId), - models.PackagesMetrics.addOneOnActiveById(packageId), - ]); - } else if (_.eq(status, "DeploymentFailed")) { + var previous_deployment_key = _.get(others, 'previousDeploymentKey'); + var previous_label = _.get(others, 'previousLabelOrAppVersion'); + if (status > 0) { return Promise.all([ - models.PackagesMetrics.addOneOnInstalledById(packageId), - models.PackagesMetrics.addOneOnFailedById(packageId) - ]); - }else { + models.LogReportDeploy.create({ + package_id: packageId, + client_unique_id: clientUniqueId, + previous_label: previous_label, + previous_deployment_key: previous_deployment_key, + status: status + }), + models.PackagesMetrics.findOne({where: {package_id: packageId}}) + .then((metrics)=>{ + if (_.isEmpty(metrics)) { + return; + } + if (_.eq(status, constConfig.DEPLOYMENT_SUCCEEDED)) { + return metrics.increment(['installed', 'active'],{by: 1}); + } else { + return metrics.increment(['installed', 'failed'],{by: 1}); + } + }) + ]) + .then(()=>{ + if (previous_deployment_key && previous_label) { + return models.Deployments.findOne({where: {deployment_key: previous_deployment_key}}) + .then((dep)=>{ + if (_.isEmpty(dep)) { + return; + } + return models.Packages.findOne({where: {deployment_id: dep.id, label: previous_label}}) + .then((p)=>{ + if (_.isEmpty(p)) { + return; + } + return models.PackagesMetrics.findOne({where:{package_id: p.id}}); + }); + }) + .then((metrics)=>{ + if (metrics) { + return metrics.decrement('active'); + } + return; + }); + } + return; + }); + } else { return; } }); diff --git a/core/services/collaborators.js b/core/services/collaborators.js index 13241cca..b2371103 100644 --- a/core/services/collaborators.js +++ b/core/services/collaborators.js @@ -21,7 +21,8 @@ proto.listCollaborators = function (appId) { }, []); }) .then((coInfo) => { - return models.Users.findAll({where: {id: {in: coInfo.uids}}}) + var Sequelize = require('sequelize'); + return models.Users.findAll({where: {id: {[Sequelize.Op.in]: coInfo.uids}}}) .then((data2) => { return _.reduce(data2, function (result, value, key) { var permission = ""; diff --git a/core/services/datacenter-manager.js b/core/services/datacenter-manager.js index 1193d2ea..c59d29ff 100644 --- a/core/services/datacenter-manager.js +++ b/core/services/datacenter-manager.js @@ -11,6 +11,7 @@ const CONTENTS_NAME = 'contents'; var AppError = require('../app-error'); var log4js = require('log4js'); var log = log4js.getLogger("cps:DataCenterManager"); +var path = require('path'); var proto = module.exports = function (){ function DataCenterManager() { @@ -30,18 +31,18 @@ proto.getDataDir = function () { proto.hasPackageStoreSync = function (packageHash) { var dataDir = this.getDataDir(); - var packageHashPath = `${dataDir}/${packageHash}`; - var manifestFile = `${packageHashPath}/${MANIFEST_FILE_NAME}`; - var contentPath = `${packageHashPath}/${CONTENTS_NAME}`; + var packageHashPath = path.join(dataDir, packageHash); + var manifestFile = path.join(packageHashPath, MANIFEST_FILE_NAME); + var contentPath = path.join(packageHashPath, CONTENTS_NAME); return fs.existsSync(manifestFile) && fs.existsSync(contentPath); } proto.getPackageInfo = function (packageHash) { if (this.hasPackageStoreSync(packageHash)){ var dataDir = this.getDataDir(); - var packageHashPath = `${dataDir}/${packageHash}`; - var manifestFile = `${packageHashPath}/${MANIFEST_FILE_NAME}`; - var contentPath = `${packageHashPath}/${CONTENTS_NAME}`; + var packageHashPath = path.join(dataDir, packageHash); + var manifestFile = path.join(packageHashPath, MANIFEST_FILE_NAME); + var contentPath = path.join(packageHashPath, CONTENTS_NAME); return this.buildPackageInfo(packageHash, packageHashPath, contentPath, manifestFile); } else { throw new AppError.AppError('can\'t get PackageInfo'); @@ -59,9 +60,9 @@ proto.buildPackageInfo = function (packageHash, packageHashPath, contentPath, ma proto.validateStore = function (providePackageHash) { var dataDir = this.getDataDir(); - var packageHashPath = `${dataDir}/${providePackageHash}`; - var manifestFile = `${packageHashPath}/${MANIFEST_FILE_NAME}`; - var contentPath = `${packageHashPath}/${CONTENTS_NAME}`; + var packageHashPath = path.join(dataDir, providePackageHash); + var manifestFile = path.join(packageHashPath, MANIFEST_FILE_NAME); + var contentPath = path.join(packageHashPath, CONTENTS_NAME); if (!this.hasPackageStoreSync(providePackageHash)) { log.debug(`validateStore providePackageHash not exist`); return Promise.resolve(false); @@ -98,9 +99,9 @@ proto.storePackage = function (sourceDst, force) { var packageHash = security.packageHashSync(manifestJson); log.debug('storePackage manifestJson packageHash:', packageHash); var dataDir = self.getDataDir(); - var packageHashPath = `${dataDir}/${packageHash}`; - var manifestFile = `${packageHashPath}/${MANIFEST_FILE_NAME}`; - var contentPath = `${packageHashPath}/${CONTENTS_NAME}`; + var packageHashPath = path.join(dataDir, packageHash); + var manifestFile = path.join(packageHashPath, MANIFEST_FILE_NAME); + var contentPath = path.join(packageHashPath, CONTENTS_NAME); return self.validateStore(packageHash) .then((isValidate) => { if (!force && isValidate) { @@ -109,7 +110,7 @@ proto.storePackage = function (sourceDst, force) { log.debug(`storePackage cover from sourceDst:`, sourceDst); return common.createEmptyFolder(packageHashPath) .then(() => { - return common.move(sourceDst, contentPath) + return common.copy(sourceDst, contentPath) .then(() => { var manifestString = JSON.stringify(manifestJson); fs.writeFileSync(manifestFile, manifestString); diff --git a/core/services/package-manager.js b/core/services/package-manager.js index 6d68cda7..fe259c7c 100644 --- a/core/services/package-manager.js +++ b/core/services/package-manager.js @@ -12,6 +12,7 @@ var common = require('../utils/common'); var os = require('os'); var path = require('path'); var AppError = require('../app-error'); +var constConfig = require('../const'); var log4js = require('log4js'); var log = log4js.getLogger("cps:PackageManager"); @@ -23,14 +24,31 @@ var proto = module.exports = function (){ return PackageManager; }; -proto.getMetricsbyPackageId= function(packageId) { +proto.getMetricsbyPackageId = function(packageId) { return models.PackagesMetrics.findOne({where: {package_id: packageId}}); } +proto.findPackageInfoByDeploymentIdAndLabel = function (deploymentId, label) { + return models.Packages.findOne({where: {deployment_id: deploymentId, label:label}}); +} + +proto.findLatestPackageInfoByDeployVersion = function (deploymentsVersionsId) { + return models.DeploymentsVersions.findById(deploymentsVersionsId) + .then((deploymentsVersions)=>{ + if (!deploymentsVersions || deploymentsVersions.current_package_id < 0) { + var e = new AppError.AppError("not found last packages"); + log.debug(e); + throw e; + } + return models.Packages.findById(deploymentsVersions.current_package_id); + }); +} + proto.parseReqFile = function (req) { log.debug('parseReqFile'); return new Promise((resolve, reject) => { var form = new formidable.IncomingForm(); + form.maxFieldsSize = 200 * 1024 * 1024; form.parse(req, (err, fields, files) => { if (err) { log.debug('parseReqFile:', err); @@ -50,9 +68,9 @@ proto.parseReqFile = function (req) { }); }; -proto.createDeploymentsVersionIfNotExist = function (deploymentId, appVersion, t) { +proto.createDeploymentsVersionIfNotExist = function (deploymentId, appVersion, minVersion, maxVersion, t) { return models.DeploymentsVersions.findOrCreate({ - where: {deployment_id: deploymentId, app_version: appVersion}, + where: {deployment_id: deploymentId, app_version: appVersion, min_version:minVersion, max_version:maxVersion}, defaults: {current_package_id: 0}, transaction: t }) @@ -84,18 +102,20 @@ proto.isMatchPackageHash = function (packageId, packageHash) { }; proto.createPackage = function (deploymentId, appVersion, packageHash, manifestHash, blobHash, params) { - var releaseMethod = params.releaseMethod || 'Upload'; + var releaseMethod = params.releaseMethod || constConfig.RELEAS_EMETHOD_UPLOAD; var releaseUid = params.releaseUid || 0; - var isMandatory = params.isMandatory ? 1 : 0; + var isMandatory = params.isMandatory || 0; var size = params.size || 0; + var rollout = params.rollout || 100; var description = params.description || ""; var originalLabel = params.originalLabel || ""; + var isDisabled = params.isDisabled || 0; var originalDeployment = params.originalDeployment || ""; var self = this; return models.Deployments.generateLabelId(deploymentId) .then((labelId) => { return models.sequelize.transaction((t) => { - return self.createDeploymentsVersionIfNotExist(deploymentId, appVersion, t) + return self.createDeploymentsVersionIfNotExist(deploymentId, appVersion, params.min_version, params.max_version, t) .then((deploymentsVersions) => { return models.Packages.create({ deployment_version_id: deploymentsVersions.id, @@ -109,6 +129,8 @@ proto.createPackage = function (deploymentId, appVersion, packageHash, manifestH label: "v" + labelId, released_by: releaseUid, is_mandatory: isMandatory, + is_disabled: isDisabled, + rollout: rollout, original_label: originalLabel, original_deployment: originalDeployment },{transaction: t}) @@ -144,9 +166,9 @@ proto.downloadPackageAndExtract = function (workDirectoryPath, packageHash, blob return dataCenterManager.getPackageInfo(packageHash); } else { var downloadURL = common.getBlobDownloadUrl(blobHash); - return common.createFileFromRequest(downloadURL, `${workDirectoryPath}/${blobHash}`) + return common.createFileFromRequest(downloadURL, path.join(workDirectoryPath, blobHash)) .then((download) => { - return common.unzipFile(`${workDirectoryPath}/${blobHash}`, `${workDirectoryPath}/current`) + return common.unzipFile(path.join(workDirectoryPath, blobHash), path.join(workDirectoryPath, 'current')) .then((outputPath) => { return dataCenterManager.storePackage(outputPath, true); }); @@ -171,14 +193,22 @@ proto.zipDiffPackage = function (fileName, files, baseDirectoryPath, hotCodePush }); for (var i = 0; i < files.length; ++i) { var file = files[i]; - zipFile.addFile(`${baseDirectoryPath}/${file}`, slash(file)); + zipFile.addFile(path.join(baseDirectoryPath, file), slash(file)); } - zipFile.addFile(hotCodePushFile, 'hotcodepush.json'); + zipFile.addFile(hotCodePushFile, constConfig.DIFF_MANIFEST_FILE_NAME); zipFile.end(); }); } -proto.generateOneDiffPackage = function (workDirectoryPath, packageId, dataCenter, diffPackageHash, diffManifestBlobHash) { +proto.generateOneDiffPackage = function ( + workDirectoryPath, + packageId, + originDataCenter, + oldPackageDataCenter, + diffPackageHash, + diffManifestBlobHash, + isUseDiffText +) { var self = this; return models.PackagesDiff.findOne({ where:{ @@ -190,24 +220,53 @@ proto.generateOneDiffPackage = function (workDirectoryPath, packageId, dataCente if (!_.isEmpty(diffPackage)) { return; } + log.debug('originDataCenter', originDataCenter); + log.debug('oldPackageDataCenter', oldPackageDataCenter); var downloadURL = common.getBlobDownloadUrl(diffManifestBlobHash); - return common.createFileFromRequest(downloadURL, `${workDirectoryPath}/${diffManifestBlobHash}`) + return common.createFileFromRequest(downloadURL, path.join(workDirectoryPath,diffManifestBlobHash)) .then(() => { - var originContentPath = dataCenter.contentPath; - var originManifestJson = JSON.parse(fs.readFileSync(dataCenter.manifestFilePath, "utf8")) - var diffManifestJson = JSON.parse(fs.readFileSync(`${workDirectoryPath}/${diffManifestBlobHash}`, "utf8")) + var dataCenterContentPath = path.join(workDirectoryPath, 'dataCenter'); + common.copySync(originDataCenter.contentPath, dataCenterContentPath); + var oldPackageDataCenterContentPath = oldPackageDataCenter.contentPath; + var originManifestJson = JSON.parse(fs.readFileSync(originDataCenter.manifestFilePath, "utf8")) + var diffManifestJson = JSON.parse(fs.readFileSync(path.join(workDirectoryPath, diffManifestBlobHash), "utf8")) var json = common.diffCollectionsSync(originManifestJson, diffManifestJson); var files = _.concat(json.diff, json.collection1Only); - var hotcodepush = {deletedFiles: json.collection2Only}; - var hotCodePushFile = `${workDirectoryPath}/${diffManifestBlobHash}_hotcodepush`; + var hotcodepush = {deletedFiles: json.collection2Only, patchedFiles:[]}; + if (isUseDiffText == constConfig.IS_USE_DIFF_TEXT_YES) { + //使用google diff-match-patch + _.forEach(json.diff, function(tmpFilePath) { + var dataCenterContentPathTmpFilePath = path.join(dataCenterContentPath, tmpFilePath); + var oldPackageDataCenterContentPathTmpFilePath = path.join(oldPackageDataCenterContentPath, tmpFilePath); + if ( + fs.existsSync(dataCenterContentPathTmpFilePath) + && fs.existsSync(oldPackageDataCenterContentPathTmpFilePath) + && common.detectIsTextFile(dataCenterContentPathTmpFilePath) + && common.detectIsTextFile(oldPackageDataCenterContentPathTmpFilePath) + ) { + var textOld = fs.readFileSync(oldPackageDataCenterContentPathTmpFilePath, 'utf-8'); + var textNew = fs.readFileSync(dataCenterContentPathTmpFilePath, 'utf-8'); + if (!textOld || !textNew) { + return; + } + var DiffMatchPatch = require('diff-match-patch'); + var dmp = new DiffMatchPatch(); + var patchs = dmp.patch_make(textOld, textNew); + var patchText = dmp.patch_toText(patchs); + if (patchText && patchText.length < _.parseInt(textNew.length * 0.8)) { + fs.writeFileSync(dataCenterContentPathTmpFilePath, patchText); + hotcodepush.patchedFiles.push(tmpFilePath); + } + } + }); + } + var hotCodePushFile = path.join(workDirectoryPath,`${diffManifestBlobHash}_hotcodepush`);; fs.writeFileSync(hotCodePushFile, JSON.stringify(hotcodepush)); - var fileName = `${workDirectoryPath}/${diffManifestBlobHash}.zip`; - - return self.zipDiffPackage(fileName, files, originContentPath, hotCodePushFile) + var fileName = path.join(workDirectoryPath,`${diffManifestBlobHash}.zip`);; + return self.zipDiffPackage(fileName, files, dataCenterContentPath, hotCodePushFile) .then((data) => { return security.qetag(data.path) .then((diffHash) => { - log.debug('diff'); return common.uploadFileToStorage(diffHash, fileName) .then(() => { var stats = fs.statSync(fileName); @@ -224,39 +283,36 @@ proto.generateOneDiffPackage = function (workDirectoryPath, packageId, dataCente }); }; -proto.createDiffPackagesByLastNums = function (packageId, num) { +proto.createDiffPackagesByLastNums = function (appId, originalPackage, num) { var self = this; - return models.Packages.findById(packageId) - .then((originalPackage) => { - if (_.isEmpty(originalPackage)) { - throw AppError.AppError('can\'t find Package'); - } - return Promise.all([ - models.Packages.findAll({ - where:{ - deployment_version_id: originalPackage.deployment_version_id, - id: {$lt: packageId}}, - order: [['id','desc']], - limit: num - }), - models.Packages.findAll({ - where:{ - deployment_version_id: originalPackage.deployment_version_id, - id: {$lt: packageId}}, - order: [['id','asc']], - limit: 2 - }) - ]) - .spread((lastNumsPackages, basePackages) => { - return _.unionBy(lastNumsPackages, basePackages, 'id'); - }) - .then((lastNumsPackages) => { - return self.createDiffPackages(originalPackage, lastNumsPackages); - }); + var Sequelize = require('sequelize'); + var packageId = originalPackage.id; + return Promise.all([ + models.Packages.findAll({ + where:{ + deployment_version_id: originalPackage.deployment_version_id, + id: {[Sequelize.Op.lt]: packageId}}, + order: [['id','desc']], + limit: num + }), + models.Packages.findAll({ + where:{ + deployment_version_id: originalPackage.deployment_version_id, + id: {[Sequelize.Op.lt]: packageId}}, + order: [['id','asc']], + limit: 2 + }), + models.Apps.findById(appId), + ]) + .spread((lastNumsPackages, basePackages, appInfo) => { + return [_.uniqBy(_.unionBy(lastNumsPackages, basePackages, 'id'), 'package_hash'), appInfo]; + }) + .spread((lastNumsPackages, appInfo) => { + return self.createDiffPackages(originalPackage, lastNumsPackages, _.get(appInfo, 'is_use_diff_text', constConfig.IS_USE_DIFF_TEXT_NO)); }); }; -proto.createDiffPackages = function (originalPackage, destPackages) { +proto.createDiffPackages = function (originalPackage, destPackages, isUseDiffText) { if (!_.isArray(destPackages)) { return Promise.reject(new AppError.AppError('第二个参数必须是数组')); } @@ -268,60 +324,67 @@ proto.createDiffPackages = function (originalPackage, destPackages) { var manifest_blob_url = _.get(originalPackage, 'manifest_blob_url'); var blob_url = _.get(originalPackage, 'blob_url'); var workDirectoryPath = path.join(os.tmpdir(), 'codepush_' + security.randToken(32)); + log.debug('workDirectoryPath', workDirectoryPath); return common.createEmptyFolder(workDirectoryPath) .then(() => self.downloadPackageAndExtract(workDirectoryPath, package_hash, blob_url)) - .then((dataCenter) => Promise.map(destPackages, - (v) => self.generateOneDiffPackage(workDirectoryPath, originalPackage.id, dataCenter, v.package_hash, v.manifest_blob_url) + .then((originDataCenter) => Promise.map(destPackages, + (v) => { + var diffWorkDirectoryPath = path.join(workDirectoryPath, _.get(v, 'package_hash')); + common.createEmptyFolderSync(diffWorkDirectoryPath); + return self.downloadPackageAndExtract(diffWorkDirectoryPath, _.get(v, 'package_hash'), _.get(v, 'blob_url')) + .then((oldPackageDataCenter) => + self.generateOneDiffPackage( + diffWorkDirectoryPath, + originalPackage.id, + originDataCenter, + oldPackageDataCenter, + v.package_hash, + v.manifest_blob_url, + isUseDiffText + ) + ) + } )) .finally(() => common.deleteFolderSync(workDirectoryPath)); } -proto.releasePackage = function (deploymentId, packageInfo, fileType, filePath, releaseUid, pubType) { +proto.releasePackage = function (appId, deploymentId, packageInfo, filePath, releaseUid) { var self = this; var appVersion = packageInfo.appVersion; - if (!/^([0-9.]+)$/.test(appVersion)) { + var versionInfo = common.validatorVersion(appVersion); + if (!versionInfo[0]) { log.debug(`releasePackage targetBinaryVersion ${appVersion} not support.`); return Promise.reject(new AppError.AppError(`targetBinaryVersion ${appVersion} not support.`)) } - var description = packageInfo.description; - var isMandatory = packageInfo.isMandatory; + var description = packageInfo.description; //描述 + var isDisabled = packageInfo.isDisabled; //是否立刻下载 + var rollout = packageInfo.rollout; //灰度百分比 + var isMandatory = packageInfo.isMandatory; //是否强制更新,无法跳过 var tmpDir = os.tmpdir(); - var directoryPath = path.join(tmpDir, 'codepush_' + security.randToken(32)); + var directoryPathParent = path.join(tmpDir, 'codepuh_' + security.randToken(32)); + var directoryPath = path.join(directoryPathParent, 'current'); log.debug(`releasePackage generate an random dir path: ${directoryPath}`); return Promise.all([ security.qetag(filePath), common.createEmptyFolder(directoryPath) .then(() => { - if (fileType == "application/zip") { - return common.unzipFile(filePath, directoryPath) - } else { - log.debug(`上传的文件格式不对`); - throw new AppError.AppError("上传的文件格式不对"); - } + return common.unzipFile(filePath, directoryPath) }) ]) .spread((blobHash) => { return security.uploadPackageType(directoryPath) .then((type) => { - if (type === 1) { - //android - if (pubType == 'ios' ) { - var e = new AppError.AppError("it must be publish it by ios type"); - log.debug(e); - throw e; - } - } else if (type === 2) { - //ios - if (pubType == 'android'){ - var e = new AppError.AppError("it must be publish it by android type"); - log.debug(e); - throw e; + return models.Apps.findById(appId).then((appInfo)=>{ + if (type > 0 && appInfo.os > 0 && appInfo.os != type) { + var e = new AppError.AppError("it must be publish it by ios type"); + log.debug(e); + throw e; + } else { + //不验证 + log.debug(`Unknown package type:`, type, ',db os:', appInfo.os); } - } else { - //不验证 - log.debug(`Unknown package type`); - } - return blobHash; + return blobHash; + }); }); }) .then((blobHash) => { @@ -330,8 +393,11 @@ proto.releasePackage = function (deploymentId, packageInfo, fileType, filePath, .then((dataCenter) => { var packageHash = dataCenter.packageHash; var manifestFile = dataCenter.manifestFilePath; - return self.createDeploymentsVersionIfNotExist(deploymentId, appVersion) + return models.DeploymentsVersions.findOne({where: {deployment_id: deploymentId, app_version:appVersion}}) .then((deploymentsVersions) => { + if (!deploymentsVersions) { + return false; + } return self.isMatchPackageHash(deploymentsVersions.get('current_package_id'), packageHash); }) .then((isExist) => { @@ -354,90 +420,181 @@ proto.releasePackage = function (deploymentId, packageInfo, fileType, filePath, .spread((packageHash, manifestHash, blobHash) => { var stats = fs.statSync(filePath); var params = { - releaseMethod: 'Upload', + releaseMethod: constConfig.RELEAS_EMETHOD_UPLOAD, releaseUid: releaseUid, - isMandatory: isMandatory, + isMandatory: isMandatory ? constConfig.IS_MANDATORY_YES : constConfig.IS_MANDATORY_NO, + isDisabled: isDisabled ? constConfig.IS_DISABLED_YES : constConfig.IS_DISABLED_NO, + rollout: rollout, size: stats.size, - description: description + description: description, + min_version: versionInfo[1], + max_version: versionInfo[2], } return self.createPackage(deploymentId, appVersion, packageHash, manifestHash, blobHash, params); }) - .finally(() => common.deleteFolderSync(directoryPath)) + .finally(() => common.deleteFolderSync(directoryPathParent)) }; -proto.modifyReleasePackage = function(deploymentId, deploymentVersionId, packageInfo) { - var appVersion = _.get(packageInfo, 'appVersion'); - var description = _.get(packageInfo, 'description'); - var isMandatory = _.get(packageInfo, 'isMandatory'); - var isDisabled = _.get(packageInfo, 'isDisabled'); - return models.DeploymentsVersions.findById(deploymentVersionId) - .then((deploymentsVersions) => { - if (_.isBoolean(isDisabled)) { - throw new AppError.AppError(`--disabled -x function is not implements`); - } - if (!appVersion) { - if (!/^([0-9.]+)$/.test(appVersion)) { - return Promise.reject(new AppError.AppError(`targetBinaryVersion ${appVersion} not support.`)) - } - return models.DeploymentsVersions.findOne({deployment_id: deploymentId, app_version: appVersion}) - .then((d) => { - if (d) { - throw new AppError.AppError(`version ${appVersion} already exist`); - } - }); +proto.modifyReleasePackage = function(packageId, params) { + var appVersion = _.get(params, 'appVersion'); + var description = _.get(params, 'description'); + var isMandatory = _.get(params, 'isMandatory'); + var isDisabled = _.get(params, 'isDisabled'); + var rollout = _.get(params, 'rollout'); + return models.Packages.findById(packageId) + .then((packageInfo) => { + if (!packageInfo) { + throw new AppError.AppError(`packageInfo not found`); } - if(!deploymentsVersions) { - throw new AppError.AppError(`packages were not found in db`); + if (!_.isNull(appVersion)) { + var versionInfo = common.validatorVersion(appVersion); + if (!versionInfo[0]) { + throw new AppError.AppError(`--targetBinaryVersion ${appVersion} not support.`); + } + return Promise.all([ + models.DeploymentsVersions.findOne({where: {deployment_id:packageInfo.deployment_id, app_version:appVersion}}), + models.DeploymentsVersions.findById(packageInfo.deployment_version_id) + ]) + .spread((v1, v2) => { + if (v1 && !_.eq(v1.id, v2.id)) { + log.debug(v1); + throw new AppError.AppError(`${appVersion} already exist.`); + } + if (!v2) { + throw new AppError.AppError(`packages not found.`); + } + return models.DeploymentsVersions.update({ + app_version:appVersion, + min_version:versionInfo[1], + max_version:versionInfo[2] + },{where: {id:v2.id}}); + }) + .then(()=>{ + return packageInfo + }); } + return packageInfo; }) - .then(() => { - + .then((packageInfo) => { + var new_params = { + description: description || packageInfo.description, + }; + if (_.isInteger(rollout)) { + new_params.rollout = rollout; + } + if (_.isBoolean(isMandatory)) { + new_params.is_mandatory = isMandatory ? constConfig.IS_MANDATORY_YES : constConfig.IS_MANDATORY_NO; + } + if (_.isBoolean(isDisabled)) { + new_params.is_disabled = isDisabled ? constConfig.IS_DISABLED_YES : constConfig.IS_DISABLED_NO; + } + return models.Packages.update(new_params,{where: {id: packageId}}); }); }; -proto.promotePackage = function (sourceDeploymentId, destDeploymentId, promoteUid) { +proto.promotePackage = function (sourceDeploymentInfo, destDeploymentInfo, params) { var self = this; - return models.Deployments.findById(sourceDeploymentId) - .then((sourceDeployment) => { - var lastDeploymentVersionId = _.get(sourceDeployment, 'last_deployment_version_id', 0); - if (_.lte(lastDeploymentVersionId, 0)) { - throw new AppError.AppError('does not exist last_deployment_version_id.'); - } - return models.DeploymentsVersions.findById(lastDeploymentVersionId) - .then((deploymentsVersions) => { - var packageId = _.get(deploymentsVersions, 'current_package_id', 0); - if (_.lte(packageId, 0)) { - throw new AppError.AppError('does not exist packages.'); + var appVersion = _.get(params,'appVersion', null); + var label = _.get(params,'label', null); + return new Promise((resolve, reject) => { + if (label) { + return models.Packages.findOne({where: {deployment_id: sourceDeploymentInfo.id, label:label}}) + .then((sourcePack)=>{ + if (!sourcePack) { + throw new AppError.AppError('label does not exist.'); + } + return models.DeploymentsVersions.findById(sourcePack.deployment_version_id) + .then((deploymentsVersions)=>{ + if (!deploymentsVersions) { + throw new AppError.AppError('deploymentsVersions does not exist.'); + } + resolve([sourcePack, deploymentsVersions]); + }); + }) + .catch((e) => { + reject(e); + }); + } else { + var lastDeploymentVersionId = _.get(sourceDeploymentInfo, 'last_deployment_version_id', 0); + if (_.lte(lastDeploymentVersionId, 0)) { + throw new AppError.AppError(`does not exist last_deployment_version_id.`); } - return models.Packages.findById(packageId) - .then((packages) => { - if (!packages) { - throw new AppError.AppError('does not exist packages.'); + return models.DeploymentsVersions.findById(lastDeploymentVersionId) + .then((deploymentsVersions)=>{ + var sourcePackId = _.get(deploymentsVersions, 'current_package_id', 0); + if (_.lte(sourcePackId, 0)) { + throw new AppError.AppError(`packageInfo not found.`); } - return self.createDeploymentsVersionIfNotExist(destDeploymentId, deploymentsVersions.app_version) - .then((deploymentsVersions) => { - return self.isMatchPackageHash(deploymentsVersions.get('current_package_id'), packages.package_hash); - }) - .then((isExist) => { - if (isExist){ - throw new AppError.AppError("The uploaded package is identical to the contents of the specified deployment's current release."); + return models.Packages.findById(sourcePackId) + .then((sourcePack) =>{ + if (!sourcePack) { + throw new AppError.AppError(`packageInfo not found.`); } - }) - .then(() => [sourceDeployment, deploymentsVersions, packages]); + resolve([sourcePack, deploymentsVersions]); + }); + }) + .catch((e) => { + reject(e); }); + } + }) + .spread((sourcePack, deploymentsVersions)=>{ + var appFinalVersion = appVersion || deploymentsVersions.app_version; + log.debug('sourcePack',sourcePack); + log.debug('deploymentsVersions',deploymentsVersions); + log.debug('appFinalVersion', appFinalVersion); + return models.DeploymentsVersions.findOne({where: { + deployment_id:destDeploymentInfo.id, + app_version: appFinalVersion, + }}) + .then((destDeploymentsVersions)=>{ + if (!destDeploymentsVersions) { + return false; + } + return self.isMatchPackageHash(destDeploymentsVersions.get('current_package_id'), sourcePack.package_hash); + }) + .then((isExist) => { + if (isExist){ + throw new AppError.AppError("The uploaded package is identical to the contents of the specified deployment's current release."); + } + return [sourcePack, deploymentsVersions, appFinalVersion]; }); }) - .spread((sourceDeployment, deploymentsVersions, packages) => { - var params = { - releaseMethod: 'Promote', - releaseUid: promoteUid, - isMandatory: packages.is_mandatory == 1 ? true : false, - size: packages.size, - description: packages.description, - originalLabel: packages.label, - originalDeployment: sourceDeployment.name + .spread((sourcePack, deploymentsVersions, appFinalVersion) => { + var versionInfo = common.validatorVersion(appFinalVersion); + if (!versionInfo[0]) { + log.debug(`targetBinaryVersion ${appVersion} not support.`); + throw new AppError.AppError(`targetBinaryVersion ${appVersion} not support.`); + } + var create_params = { + releaseMethod: constConfig.RELEAS_EMETHOD_PROMOTE, + releaseUid: params.promoteUid || 0, + rollout: params.rollout || 100, + size: sourcePack.size, + description: params.description || sourcePack.description, + originalLabel: sourcePack.label, + originalDeployment: sourceDeploymentInfo.name, + min_version: versionInfo[1], + max_version: versionInfo[2], }; - return self.createPackage(destDeploymentId, deploymentsVersions.app_version, packages.package_hash, packages.manifest_blob_url, packages.blob_url, params); + if (_.isBoolean(params.isMandatory)) { + create_params.isMandatory = params.isMandatory ? constConfig.IS_MANDATORY_YES : constConfig.IS_MANDATORY_NO; + } else { + create_params.isMandatory = sourcePack.is_mandatory + } + if (_.isBoolean(params.isDisabled)) { + create_params.isDisabled = params.isDisabled ? constConfig.IS_DISABLED_YES : constConfig.IS_DISABLED_NO; + } else { + create_params.isDisabled = sourcePack.is_disabled + } + return self.createPackage( + destDeploymentInfo.id, + appFinalVersion, + sourcePack.package_hash, + sourcePack.manifest_blob_url, + sourcePack.blob_url, + create_params + ); }); }; @@ -476,11 +633,15 @@ proto.rollbackPackage = function (deploymentVersionId, targetLabel, rollbackUid) var params = { releaseMethod: 'Rollback', releaseUid: rollbackUid, - isMandatory: rollbackPackage.is_mandatory == 1 ? true : false, + isMandatory: rollbackPackage.is_mandatory, + isDisabled: rollbackPackage.is_disabled, + rollout: rollbackPackage.rollout, size: rollbackPackage.size, description: rollbackPackage.description, originalLabel: rollbackPackage.label, - originalDeployment: '' + originalDeployment: '', + min_version: deploymentsVersions.min_version, + max_version: deploymentsVersions.max_version, }; return self.createPackage(deploymentsVersions.deployment_id, deploymentsVersions.app_version, @@ -494,7 +655,11 @@ proto.rollbackPackage = function (deploymentVersionId, targetLabel, rollbackUid) } proto.getCanRollbackPackages = function (deploymentVersionId) { + var Sequelize = require('sequelize'); return models.Packages.findAll({ - where: {deployment_version_id: deploymentVersionId, release_method: {$in: ['Upload', 'Promote'] }}, order: [['id','desc']], limit: 2 + where: { + deployment_version_id: deploymentVersionId, + release_method: {[Sequelize.Op.in]: [constConfig.RELEAS_EMETHOD_UPLOAD, constConfig.RELEAS_EMETHOD_PROMOTE] } + }, order: [['id','desc']], limit: 2 }); } diff --git a/core/utils/common.js b/core/utils/common.js index ca328c18..6d221535 100644 --- a/core/utils/common.js +++ b/core/utils/common.js @@ -5,18 +5,91 @@ var fsextra = require("fs-extra"); var extract = require('extract-zip') var config = require('../config'); var _ = require('lodash'); +var validator = require('validator'); var qiniu = require("qiniu"); +var upyun = require('upyun'); var common = {}; var AppError = require('../app-error'); +var jschardet = require("jschardet"); var log4js = require('log4js'); +var path = require('path'); var log = log4js.getLogger("cps:utils:common"); module.exports = common; +common.detectIsTextFile = function(filePath) { + var fd = fs.openSync(filePath, 'r'); + var buffer = new Buffer(4096); + fs.readSync(fd, buffer, 0, 4096, 0); + fs.closeSync(fd); + var rs = jschardet.detect(buffer); + log.debug('detectIsTextFile:', filePath, rs); + if (rs.confidence == 1) { + return true; + } + return false; +} + +common.parseVersion = function (versionNo) { + var version = '0'; + var data = null; + if (data = versionNo.match(/^([0-9]{1,3}).([0-9]{1,5}).([0-9]{1,10})$/)) { + // "1.2.3" + version = data[1] + _.padStart(data[2], 5, '0') + _.padStart(data[3], 10, '0'); + } else if (data = versionNo.match(/^([0-9]{1,3}).([0-9]{1,5})$/)) { + // "1.2" + version = data[1] + _.padStart(data[2], 5, '0') + _.padStart('0', 10, '0'); + } + return version; +}; + +common.validatorVersion = function (versionNo) { + var flag = false; + var min = '0'; + var max = '9999999999999999999'; + var data = null; + if (versionNo == "*") { + // "*" + flag = true; + } else if (data = versionNo.match(/^([0-9]{1,3}).([0-9]{1,5}).([0-9]{1,10})$/)) { + // "1.2.3" + flag = true; + min = data[1] + _.padStart(data[2], 5, '0') + _.padStart(data[3], 10, '0'); + max = data[1] + _.padStart(data[2], 5, '0') + _.padStart((parseInt(data[3])+1), 10, '0'); + } else if (data = versionNo.match(/^([0-9]{1,3}).([0-9]{1,5})(\.\*){0,1}$/)) { + // "1.2" "1.2.*" + flag = true; + min = data[1] + _.padStart(data[2], 5, '0') + _.padStart('0', 10, '0'); + max = data[1] + _.padStart((parseInt(data[2])+1), 5, '0') + _.padStart('0', 10, '0'); + } else if (data = versionNo.match(/^\~([0-9]{1,3}).([0-9]{1,5}).([0-9]{1,10})$/)) { + //"~1.2.3" + flag = true; + min = data[1] + _.padStart(data[2], 5, '0') + _.padStart(data[3], 10, '0'); + max = data[1] + _.padStart((parseInt(data[2])+1), 5, '0') + _.padStart('0', 10, '0'); + } else if (data = versionNo.match(/^\^([0-9]{1,3}).([0-9]{1,5}).([0-9]{1,10})$/)) { + //"^1.2.3" + flag = true; + min = data[1] + _.padStart(data[2], 5, '0') + _.padStart(data[3], 10, '0'); + max = _.toString((parseInt(data[1])+1)) + _.padStart(0, 5, '0') + _.padStart('0', 10, '0'); + } else if (data = versionNo.match(/^([0-9]{1,3}).([0-9]{1,5}).([0-9]{1,10})\s?-\s?([0-9]{1,3}).([0-9]{1,5}).([0-9]{1,10})$/)) { + // "1.2.3 - 1.2.7" + flag = true; + min = data[1] + _.padStart(data[2], 5, '0') + _.padStart(data[3], 10, '0'); + max = data[4] + _.padStart(data[5], 5, '0') + _.padStart((parseInt(data[6])+1), 10, '0'); + } else if (data = versionNo.match(/^>=([0-9]{1,3}).([0-9]{1,5}).([0-9]{1,10})\s?<([0-9]{1,3}).([0-9]{1,5}).([0-9]{1,10})$/)) { + // ">=1.2.3 <1.2.7" + flag = true; + min = data[1] + _.padStart(data[2], 5, '0') + _.padStart(data[3], 10, '0'); + max = data[4] + _.padStart(data[5], 5, '0') + _.padStart(data[6], 10, '0'); + } + return [flag, min, max]; +}; + common.createFileFromRequest = function (url, filePath) { return new Promise((resolve, reject) => { fs.exists(filePath, function (exists) { if (!exists) { var request = require('request'); + log.debug(`createFileFromRequest url:${url}`) request(url).on('error', function (error) { reject(error); }) @@ -39,11 +112,29 @@ common.createFileFromRequest = function (url, filePath) { } }); }); -} +}; + +common.copySync = function (sourceDst, targertDst) { + return fsextra.copySync(sourceDst, targertDst, {overwrite: true}); +}; + +common.copy = function (sourceDst, targertDst) { + return new Promise((resolve, reject) => { + fsextra.copy(sourceDst, targertDst, {overwrite: true}, function (err) { + if (err) { + log.error(err); + reject(err); + } else { + log.debug(`copy success sourceDst:${sourceDst} targertDst:${targertDst}`); + resolve(); + } + }); + }); +}; common.move = function (sourceDst, targertDst) { return new Promise((resolve, reject) => { - fsextra.move(sourceDst, targertDst, {clobber: true, limit: 16}, function (err) { + fsextra.move(sourceDst, targertDst, {overwrite: true}, function (err) { if (err) { log.error(err); reject(err); @@ -117,20 +208,30 @@ common.unzipFile = function (zipFile, outputPath) { }); }; -common.uptoken = function (bucket, key) { - var putPolicy = new qiniu.rs.PutPolicy(bucket+":"+key); - return putPolicy.token(); +common.getUploadTokenQiniu = function (mac, bucket, key) { + var options = { + scope: bucket + ":" + key + } + var putPolicy = new qiniu.rs.PutPolicy(options); + return putPolicy.uploadToken(mac); }; common.uploadFileToStorage = function (key, filePath) { - if (_.get(config, 'common.storageType') === 'local') { + var storageType = _.get(config, 'common.storageType'); + if ( storageType === 'local') { return common.uploadFileToLocal(key, filePath); - } else if (_.get(config, 'common.storageType') === 's3') { + } else if (storageType === 's3') { return common.uploadFileToS3(key, filePath); - } else if (_.get(config, 'common.storageType') === 'oss') { + } else if (storageType === 'oss') { return common.uploadFileToOSS(key, filePath); + } else if (storageType === 'qiniu') { + return common.uploadFileToQiniu(key, filePath); + } else if (storageType === 'upyun') { + return common.uploadFileToUpyun(key, filePath); + } else if (storageType === 'tencentcloud') { + return common.uploadFileToTencentCloud(key, filePath); } - return common.uploadFileToQiniu(key, filePath); + throw new AppError.AppError(`${storageType} storageType does not support.`); }; common.uploadFileToLocal = function (key, filePath) { @@ -139,6 +240,10 @@ common.uploadFileToLocal = function (key, filePath) { if (!storageDir) { throw new AppError.AppError('please set config local storageDir'); } + if (key.length < 3) { + log.error(`generate key is too short, key value:${key}`); + throw new AppError.AppError('generate key is too short.'); + } try { log.debug(`uploadFileToLocal check directory ${storageDir} fs.R_OK`); fs.accessSync(storageDir, fs.W_OK); @@ -147,7 +252,10 @@ common.uploadFileToLocal = function (key, filePath) { log.error(e); throw new AppError.AppError(e.message); } - if (fs.existsSync(`${storageDir}/${key}`)) { + var subDir = key.substr(0, 2).toLowerCase(); + var finalDir = path.join(storageDir, subDir); + var fileName = path.join(finalDir, key); + if (fs.existsSync(fileName)) { return resolve(key); } var stats = fs.statSync(storageDir); @@ -156,6 +264,10 @@ common.uploadFileToLocal = function (key, filePath) { log.error(e); throw e; } + if (!fs.existsSync(`${finalDir}`)) { + fs.mkdirSync(`${finalDir}`); + log.debug(`uploadFileToLocal mkdir:${finalDir}`); + } try { fs.accessSync(filePath, fs.R_OK); } catch (e) { @@ -165,10 +277,10 @@ common.uploadFileToLocal = function (key, filePath) { stats = fs.statSync(filePath); if (!stats.isFile()) { var e = new AppError.AppError(`${filePath} must be file`); - log.debug(e); + log.error(e); throw e; } - fsextra.copy(filePath, `${storageDir}/${key}`,(err) => { + fsextra.copy(filePath, fileName,(err) => { if (err) { log.error(new AppError.AppError(err.message)); return reject(new AppError.AppError(err.message)); @@ -179,44 +291,61 @@ common.uploadFileToLocal = function (key, filePath) { }); }; -common.getDownloadUrl = function () { - if (_.get(config, 'common.storageType') === 'local') { - return _.get(config, 'local.downloadUrl'); - } else if (_.get(config, 'common.storageType') === 's3') { - return _.get(config, 's3.downloadUrl'); - } else if (_.get(config, 'common.storageType') === 'oss') { - return _.get(config, 'oss.downloadUrl'); - } - return _.get(config, 'qiniu.downloadUrl'); -} - common.getBlobDownloadUrl = function (blobUrl) { - return `${common.getDownloadUrl()}/${blobUrl}` + var fileName = blobUrl; + var storageType = _.get(config, 'common.storageType'); + var downloadUrl = _.get(config, `${storageType}.downloadUrl`); + if ( storageType === 'local') { + fileName = blobUrl.substr(0, 2).toLowerCase() + '/' + blobUrl; + } + if (!validator.isURL(downloadUrl)) { + var e = new AppError.AppError(`Please config ${storageType}.downloadUrl in config.js`); + log.error(e); + throw e; + } + return `${downloadUrl}/${fileName}` }; + common.uploadFileToQiniu = function (key, filePath) { return new Promise((resolve, reject) => { - qiniu.conf.ACCESS_KEY = _.get(config, "qiniu.accessKey"); - qiniu.conf.SECRET_KEY = _.get(config, "qiniu.secretKey"); - var bucket = _.get(config, "qiniu.bucketName", "jukang"); - var client = new qiniu.rs.Client(); - client.stat(bucket, key, (err, ret) => { - if (!err) { - resolve(ret.hash); + var accessKey = _.get(config, "qiniu.accessKey"); + var secretKey = _.get(config, "qiniu.secretKey"); + var bucket = _.get(config, "qiniu.bucketName", ""); + var mac = new qiniu.auth.digest.Mac(accessKey, secretKey); + var conf = new qiniu.conf.Config(); + var bucketManager = new qiniu.rs.BucketManager(mac, conf); + bucketManager.stat(bucket, key, (respErr, respBody, respInfo) => { + if (respErr) { + log.debug('uploadFileToQiniu file stat:', respErr); + return reject(new AppError.AppError(respErr.message)); + } + log.debug('uploadFileToQiniu file stat respBody:', respBody); + log.debug('uploadFileToQiniu file stat respInfo:', respInfo); + if (respInfo.statusCode == 200) { + resolve(respBody.hash); } else { try { - var uptoken = common.uptoken(bucket, key); + var uploadToken = common.getUploadTokenQiniu(mac, bucket, key); } catch (e) { - return reject(e); + return reject(new AppError.AppError(e.message)); } - var extra = new qiniu.io.PutExtra(); - qiniu.io.putFile(uptoken, key, filePath, extra, (err, ret) => { - if(!err) { - // 上传成功, 处理返回值 - resolve(ret.hash); - } else { + var formUploader = new qiniu.form_up.FormUploader(conf); + var putExtra = new qiniu.form_up.PutExtra(); + formUploader.putFile(uploadToken, key, filePath, putExtra, (respErr, respBody, respInfo) => { + if(respErr) { + log.error('uploadFileToQiniu putFile:', respErr); // 上传失败, 处理返回代码 - reject(new AppError.AppError(JSON.stringify(err))); + return reject(new AppError.AppError(JSON.stringify(respErr))); + } else { + log.debug('uploadFileToQiniu putFile respBody:', respBody); + log.debug('uploadFileToQiniu putFile respInfo:', respInfo); + // 上传成功, 处理返回值 + if (respInfo.statusCode == 200) { + return resolve(respBody.hash); + } else { + return reject(new AppError.AppError(respBody.error)); + } } }); } @@ -224,6 +353,43 @@ common.uploadFileToQiniu = function (key, filePath) { }); }; +common.uploadFileToUpyun = function (key, filePath) { + var serviceName = _.get(config, "upyun.serviceName"); + var operatorName = _.get(config, "upyun.operatorName"); + var operatorPass = _.get(config, "upyun.operatorPass", ""); + var storageDir = _.get(config, "upyun.storageDir", ""); + var service = new upyun.Service(serviceName, operatorName, operatorPass); + var client = new upyun.Client(service); + return ( + new Promise((resolve, reject) => { + client.makeDir(storageDir).then(result => { + if(!storageDir) { + reject(new AppError.AppError('Please config the upyun remoteDir!')); + return; + } + let remotePath = storageDir + '/' + key; + log.debug('uploadFileToUpyun remotePath:', remotePath); + log.debug('uploadFileToUpyun mkDir result:', result); + client.putFile(remotePath, fs.createReadStream(filePath)).then(data => { + log.debug('uploadFileToUpyun putFile response:', data); + if(data) { + resolve(key) + } else { + log.debug('uploadFileToUpyun putFile failed!', data); + reject(new AppError.AppError('Upload file to upyun failed!')); + } + }).catch(e1 => { + log.debug('uploadFileToUpyun putFile exception e1:', e1); + reject(new AppError.AppError(JSON.stringify(e1))); + }) + }).catch(e => { + log.debug('uploadFileToUpyun putFile exception e:', e); + reject(new AppError.AppError(JSON.stringify(e))); + }); + }) + ); +}; + common.uploadFileToS3 = function (key, filePath) { var AWS = require('aws-sdk'); return ( @@ -272,16 +438,41 @@ common.uploadFileToOSS = function (key, filePath) { return new Promise((resolve, reject) => { upload.on('error', (error) => { + log.debug("uploadFileToOSS", error); reject(error); }); upload.on('uploaded', (details) => { + log.debug("uploadFileToOSS", details); resolve(details.ETag); }); fs.createReadStream(filePath).pipe(upload); }); }; +common.uploadFileToTencentCloud = function (key, filePath) { + return new Promise((resolve, reject) => { + var COS = require('cos-nodejs-sdk-v5'); + var cosIn = new COS({ + SecretId: _.get(config, 'tencentcloud.accessKeyId'), + SecretKey: _.get(config, 'tencentcloud.secretAccessKey') + }); + cosIn.sliceUploadFile({ + Bucket: _.get(config, 'tencentcloud.bucketName'), + Region: _.get(config, 'tencentcloud.region'), + Key: key, + FilePath: filePath + }, function (err, data) { + log.debug("uploadFileToTencentCloud", err, data); + if (err) { + reject(new AppError.AppError(JSON.stringify(err))); + }else { + resolve(data.Key); + } + }); + }); +} + common.diffCollectionsSync = function (collection1, collection2) { var diffFiles = []; var collection1Only = []; diff --git a/core/utils/security.js b/core/utils/security.js index 401fd040..a4054acd 100644 --- a/core/utils/security.js +++ b/core/utils/security.js @@ -61,13 +61,15 @@ security.stringSha256Sync = function (contents) { security.packageHashSync = function (jsonData) { var sortedArr = security.sortJsonToArr(jsonData); - var manifestData = _.map(sortedArr, (v) => { + var manifestData = _.filter(sortedArr, (v) => { + return !security.isPackageHashIgnored(v.path); + }).map((v) => { return v.path + ':' + v.hash; }); log.debug('packageHashSync manifestData:', manifestData); var manifestString = JSON.stringify(manifestData.sort()); manifestString = _.replace(manifestString, /\\\//g, '/'); - log.debug('packageHashSync manifestString:', manifestData); + log.debug('packageHashSync manifestString:', manifestString); return security.stringSha256Sync(manifestString); } @@ -112,28 +114,29 @@ security.sha256AllFiles = function (files) { security.uploadPackageType = function (directoryPath) { return new Promise((resolve, reject) => { - var recursiveFs = require("recursive-fs"); + var recursive = require("recursive-readdir"); var path = require('path'); var slash = require("slash"); - recursiveFs.readdirr(directoryPath, (err, directories, files) => { + recursive(directoryPath, (err, files) => { if (err) { log.error(new AppError.AppError(err.message)); - reject(err); + reject(new AppError.AppError(err.message)); } else { if (files.length == 0) { log.debug(`uploadPackageType empty files`); reject(new AppError.AppError("empty files")); } else { + var constName = require('../const'); const AREGEX=/android\.bundle/ const AREGEX_IOS=/main\.jsbundle/ var packageType = 0; _.forIn(files, function (value) { if (AREGEX.test(value)) { - packageType = 1; + packageType = constName.ANDROID; return false; } if (AREGEX_IOS.test(value)) { - packageType = 2; + packageType = constName.IOS; return false; } }); @@ -145,16 +148,51 @@ security.uploadPackageType = function (directoryPath) { }); } +// some files are ignored in calc hash in client sdk +// https://github.com/Microsoft/react-native-code-push/pull/974/files#diff-21b650f88429c071b217d46243875987R15 +security.isHashIgnored = function (relativePath) { + if (!relativePath) { + return true; + } + + const IgnoreMacOSX = '__MACOSX/'; + const IgnoreDSStore = '.DS_Store'; + + return relativePath.startsWith(IgnoreMacOSX) + || relativePath === IgnoreDSStore + || relativePath.endsWith(IgnoreDSStore); +} + +security.isPackageHashIgnored = function (relativePath) { + if (!relativePath) { + return true; + } + + // .codepushrelease contains code sign JWT + // it should be ignored in package hash but need to be included in package manifest + const IgnoreCodePushMetadata = '.codepushrelease'; + return relativePath === IgnoreCodePushMetadata + || relativePath.endsWith(IgnoreCodePushMetadata) + || security.isHashIgnored(relativePath); +} + + security.calcAllFileSha256 = function (directoryPath) { return new Promise((resolve, reject) => { - var recursiveFs = require("recursive-fs"); + var recursive = require("recursive-readdir"); var path = require('path'); var slash = require("slash"); - recursiveFs.readdirr(directoryPath, (error, directories, files) => { + recursive(directoryPath, (error, files) => { if (error) { log.error(error); - reject(error); + reject(new AppError.AppError(error.message)); } else { + // filter files that should be ignored + files = files.filter((file) => { + var relative = path.relative(directoryPath, file); + return !security.isHashIgnored(relative); + }); + if (files.length == 0) { log.debug(`calcAllFileSha256 empty files in directoryPath:`, directoryPath); reject(new AppError.AppError("empty files")); @@ -164,6 +202,10 @@ security.calcAllFileSha256 = function (directoryPath) { var data = {}; _.forIn(results, (value, key) => { var relativePath = path.relative(directoryPath, key); + var matchresult = relativePath.match(/(\/|\\).*/); + if (matchresult) { + relativePath = path.join('CodePush', matchresult[0]); + } relativePath = slash(relativePath); data[relativePath] = value; }); diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..d1fb6da1 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,137 @@ +# docker 部署 code-push-server + +>该文档用于描述docker部署code-push-server,实例包含三个部分 + +- code-push-server部分 + - 更新包默认采用`local`存储(即存储在本地机器上)。使用docker volume存储方式,容器销毁不会导致数据丢失,除非人为删除volume。 + - 内部使用pm2 cluster模式管理进程,默认开启进程数为cpu数,可以根据自己机器配置设置docker-compose.yml文件中deploy参数。 + - docker-compose.yml只提供了应用的一部分参数设置,如需要设置其他配置,可以修改文件config.js。 +- mysql部分 + - 数据使用docker volume存储方式,容器销毁不会导致数据丢失,除非人为删除volume。 + - 应用请勿使用root用户,为了安全可以创建权限相对较小的权限供code-push-server使用,只需要给予`select,update,insert`权限即可。初始化数据库需要使用root或有建表权限用户 +- redis部分 + - `tryLoginTimes` 登录错误次数限制 + - `updateCheckCache` 提升应用性能 + - `rolloutClientUniqueIdCache` 灰度发布 + +## 安装docker + +参考docker官方安装教程 + +- [>>mac点这里](https://docs.docker.com/docker-for-mac/install/) +- [>>windows点这里](https://docs.docker.com/docker-for-windows/install/) +- [>>linux点这里](https://docs.docker.com/install/linux/docker-ce/ubuntu/) + + +`$ docker info` 能成功输出相关信息,则安装成功,才能继续下面步骤 + +## 启动swarm + +```shell +$ sudo docker swarm init +``` + + +## 获取代码 + +```shell +$ git clone https://github.com/lisong/code-push-server.git +$ cd code-push-server/docker +``` + +## 修改配置文件 + +```shell +$ vim docker-compose.yml +``` + +*将`DOWNLOAD_URL`中`YOU_MACHINE_IP`替换成本机外网ip或者域名* + +*将`MYSQL_HOST`中`YOU_MACHINE_IP`替换成本机内网ip* + +*将`REDIS_HOST`中`YOU_MACHINE_IP`替换成本机内网ip* + +## jwt.tokenSecret修改 + +> code-push-server 验证登录验证方式使用的json web token加密方式,该对称加密算法是公开的,所以修改config.js中tokenSecret值很重要。 + +*非常重要!非常重要! 非常重要!* + +> 可以打开连接`https://www.grc.com/passwords.htm`获取 `63 random alpha-numeric characters`类型的随机生成数作为密钥 + +## 部署 + +```shell +$ sudo docker stack deploy -c docker-compose.yml code-push-server +``` + +> 如果网速不佳,需要漫长而耐心的等待。。。去和妹子聊会天吧^_^ + + +## 查看进展 + +```shell +$ sudo docker service ls +$ sudo docker service ps code-push-server_db +$ sudo docker service ps code-push-server_redis +$ sudo docker service ps code-push-server_server +``` + +> 确认`CURRENT STATE` 为 `Running about ...`, 则已经部署完成 + +## 访问接口简单验证 + +`$ curl -I http://YOUR_CODE_PUSH_SERVER_IP:3000/` + +返回`200 OK` + +```http +HTTP/1.1 200 OK +X-DNS-Prefetch-Control: off +X-Frame-Options: SAMEORIGIN +Strict-Transport-Security: max-age=15552000; includeSubDomains +X-Download-Options: noopen +X-Content-Type-Options: nosniff +X-XSS-Protection: 1; mode=block +Content-Type: text/html; charset=utf-8 +Content-Length: 592 +ETag: W/"250-IiCMcM1ZUFSswSYCU0KeFYFEMO8" +Date: Sat, 25 Aug 2018 15:45:46 GMT +Connection: keep-alive +``` + +## 浏览器登录 + +> 默认用户名:admin 密码:123456 记得要修改默认密码哦 +> 如果登录连续输错密码超过一定次数,会限定无法再登录. 需要清空redis缓存 + +```shell +$ redis-cli -p6388 # 进入redis +> flushall +> quit +``` + + +## 查看服务日志 + +```shell +$ sudo docker service logs code-push-server_server +$ sudo docker service logs code-push-server_db +$ sudo docker service logs code-push-server_redis +``` + +## 查看存储 `docker volume ls` + +DRIVER | VOLUME NAME | 描述 +------ | ----- | ------- +local | code-push-server_data-mysql | 数据库存储数据目录 +local | code-push-server_data-storage | 存储打包文件目录 +local | code-push-server_data-tmp | 用于计算更新包差异文件临时目录 +local | code-push-server_data-redis | redis落地数据 + +## 销毁退出应用 + +```bash +$ sudo docker stack rm code-push-server +$ sudo docker swarm leave --force +``` diff --git a/docker/code-push-server/Dockerfile b/docker/code-push-server/Dockerfile new file mode 100644 index 00000000..004406ba --- /dev/null +++ b/docker/code-push-server/Dockerfile @@ -0,0 +1,10 @@ +FROM node:8.11.4-alpine + +RUN npm config set registry https://registry.npm.taobao.org/ \ +&& npm i -g code-push-server@0.5.2 pm2@latest --no-optional + +COPY ./process.json /process.json + +EXPOSE 3000 + +CMD ["pm2-docker", "start", "/process.json"] diff --git a/docker/code-push-server/process.json b/docker/code-push-server/process.json new file mode 100644 index 00000000..493010d3 --- /dev/null +++ b/docker/code-push-server/process.json @@ -0,0 +1,11 @@ +{ + "apps" : [ + { + "name" : "code-push-server", + "max_memory_restart" : "500M", + "script" : "code-push-server", + "instances" : "max", //开启实例数量,max为cpu核数 + "exec_mode" : "cluster", //集群模式,最大提升网站并发 + } + ] +} \ No newline at end of file diff --git a/docker/config.js b/docker/config.js new file mode 100644 index 00000000..f78fc3b6 --- /dev/null +++ b/docker/config.js @@ -0,0 +1,93 @@ +var config = {}; +config.development = { + // Config for database, only support mysql. + db: { + username: process.env.MYSQL_USERNAME, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE, + host: process.env.MYSQL_HOST, + port: process.env.MYSQL_PORT || 3306, + dialect: "mysql", + logging: false, + operatorsAliases: false, + }, + // Config for local storage when storageType value is "local". + local: { + // Binary files storage dir, Do not use tmpdir and it's public download dir. + storageDir: process.env.STORAGE_DIR, + // Binary files download host address which Code Push Server listen to. the files storage in storageDir. + downloadUrl: process.env.DOWNLOAD_URL, + // public static download spacename. + public: '/download' + }, + jwt: { + // Recommended: 63 random alpha-numeric characters + // Generate using: https://www.grc.com/passwords.htm + tokenSecret: 'INSERT_RANDOM_TOKEN_KEY' + }, + common: { + /* + * tryLoginTimes is control login error times to avoid force attack. + * if value is 0, no limit for login auth, it may not safe for account. when it's a number, it means you can + * try that times today. but it need config redis server. + */ + tryLoginTimes: 4, + // CodePush Web(https://github.com/lisong/code-push-web) login address. + //codePushWebUrl: "http://127.0.0.1:3001/login", + // create patch updates's number. default value is 3 + diffNums: 3, + // data dir for caclulate diff files. it's optimization. + dataDir: process.env.DATA_DIR, + // storageType which is your binary package files store. options value is ("local" | "qiniu" | "s3") + storageType: "local", + // options value is (true | false), when it's true, it will cache updateCheck results in redis. + updateCheckCache: false, + // options value is (true | false), when it's true, it will cache rollout results in redis + rolloutClientUniqueIdCache: false, + }, + // Config for smtp email,register module need validate user email project source https://github.com/nodemailer/nodemailer + smtpConfig:{ + host: "smtp.aliyun.com", + port: 465, + secure: true, + auth: { + user: "", + pass: "" + } + }, + // Config for redis (register module, tryLoginTimes module) + redis: { + default: { + host: process.env.REDIS_HOST, + port: process.env.REDIS_PORT || 6379, + retry_strategy: function (options) { + if (options.error.code === 'ECONNREFUSED') { + // End reconnecting on a specific error and flush all commands with a individual error + return new Error('The server refused the connection'); + } + if (options.total_retry_time > 1000 * 60 * 60) { + // End reconnecting after a specific timeout and flush all commands with a individual error + return new Error('Retry time exhausted'); + } + if (options.times_connected > 10) { + // End reconnecting with built in error + return undefined; + } + // reconnect after + return Math.max(options.attempt * 100, 3000); + } + } + } +} + +config.development.log4js = { + appenders: {console: { type: 'console'}}, + categories : { + "default": { appenders: ['console'], level:'error'}, + "startup": { appenders: ['console'], level:'info'}, + "http": { appenders: ['console'], level:'info'} + } +} + +config.production = Object.assign({}, config.development); +module.exports = config; diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000..2b625f8f --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,63 @@ +version: "3.7" +services: + server: + image: tablee/code-push-server:v0.5.2 + volumes: + - data-storage:/data/storage + - data-tmp:/data/tmp + - ./config.js:/config.js + environment: + DOWNLOAD_URL: "http://YOU_MACHINE_IP:3000/download" + MYSQL_HOST: "YOU_MACHINE_IP" + MYSQL_PORT: "3308" + MYSQL_USERNAME: "codepush" + MYSQL_PASSWORD: "123456" + MYSQL_DATABASE: "codepush" + STORAGE_DIR: "/data/storage" + DATA_DIR: "/data/tmp" + NODE_ENV: "production" + CONFIG_FILE: "/config.js" + REDIS_HOST: "YOU_MACHINE_IP" + REDIS_PORT: "6388" + deploy: + resources: + limits: + cpus: "2" + memory: 1000M + restart_policy: + condition: on-failure + ports: + - "3000:3000" + networks: + - servernet + depends_on: + - db + - redis + db: + image: mysql:5.7.23 + volumes: + - data-mysql:/var/lib/mysql + - ./sql/codepush-all.sql:/docker-entrypoint-initdb.d/codepush-all.sql + ports: + - "3308:3306" + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: "On" + networks: + - dbnet + redis: + image: redis:4.0.11-alpine + volumes: + - data-redis:/data + ports: + - "6388:6379" + networks: + - redisnet +networks: + servernet: + dbnet: + redisnet: +volumes: + data-storage: + data-tmp: + data-mysql: + data-redis: diff --git a/sql/codepush-v0.0.1.sql b/docker/sql/codepush-all.sql similarity index 64% rename from sql/codepush-v0.0.1.sql rename to docker/sql/codepush-all.sql index ee7ed007..7bf4047d 100644 --- a/sql/codepush-v0.0.1.sql +++ b/docker/sql/codepush-all.sql @@ -1,17 +1,25 @@ -DROP TABLE IF EXISTS `apps`; -CREATE TABLE `apps` ( +CREATE DATABASE IF NOT EXISTS `codepush`; + +GRANT SELECT,UPDATE,INSERT ON `codepush`.* TO 'codepush'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION; + +flush privileges; + +use `codepush`; +CREATE TABLE IF NOT EXISTS `apps` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL DEFAULT '', `uid` bigint(20) unsigned NOT NULL DEFAULT '0', + `os` tinyint(3) unsigned NOT NULL DEFAULT '0', + `platform` tinyint(3) unsigned NOT NULL DEFAULT '0', + `is_use_diff_text` tinyint(3) unsigned NOT NULL DEFAULT '0', `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `created_at` timestamp NULL DEFAULT NULL, `deleted_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_name` (`name`(12)) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `collaborators`; -CREATE TABLE `collaborators` ( +CREATE TABLE IF NOT EXISTS `collaborators` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `appid` int(10) unsigned NOT NULL DEFAULT '0', `uid` bigint(20) unsigned NOT NULL DEFAULT '0', @@ -25,8 +33,7 @@ CREATE TABLE `collaborators` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `deployments`; -CREATE TABLE `deployments` ( +CREATE TABLE IF NOT EXISTS `deployments` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `appid` int(10) unsigned NOT NULL DEFAULT '0', `name` varchar(20) NOT NULL DEFAULT '', @@ -42,8 +49,7 @@ CREATE TABLE `deployments` ( KEY `idx_deploymentkey` (`deployment_key`(40)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `deployments_history`; -CREATE TABLE `deployments_history` ( +CREATE TABLE IF NOT EXISTS `deployments_history` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `deployment_id` int(11) unsigned NOT NULL DEFAULT '0', `package_id` int(10) unsigned NOT NULL DEFAULT '0', @@ -54,20 +60,23 @@ CREATE TABLE `deployments_history` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `deployments_versions`; -CREATE TABLE `deployments_versions` ( +CREATE TABLE IF NOT EXISTS `deployments_versions` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `deployment_id` int(11) unsigned NOT NULL DEFAULT '0', - `app_version` varchar(14) NOT NULL DEFAULT '', + `app_version` varchar(100) NOT NULL DEFAULT '', `current_package_id` int(10) unsigned NOT NULL DEFAULT '0', `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `created_at` timestamp NULL DEFAULT NULL, + `deleted_at` timestamp NULL DEFAULT NULL, + `min_version` bigint(20) unsigned NOT NULL DEFAULT '0', + `max_version` bigint(20) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), - UNIQUE KEY `idx_did_appversion` (`deployment_id`,`app_version`) + KEY `idx_did_minversion` (`deployment_id`,`min_version`), + KEY `idx_did_maxversion` (`deployment_id`,`max_version`), + KEY `idx_did_appversion` (`deployment_id`,`app_version`(30)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `packages`; -CREATE TABLE `packages` ( +CREATE TABLE IF NOT EXISTS `packages` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `deployment_version_id` int(10) unsigned NOT NULL DEFAULT '0', `deployment_id` int(10) unsigned NOT NULL DEFAULT '0', @@ -82,14 +91,17 @@ CREATE TABLE `packages` ( `original_deployment` varchar(20) NOT NULL DEFAULT '', `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `created_at` timestamp NULL DEFAULT NULL, - `released_by` bigint(20) unsigned NOT NULL, + `released_by` bigint(20) unsigned NOT NULL DEFAULT '0', + `is_mandatory` tinyint(3) unsigned NOT NULL DEFAULT '0', + `is_disabled` tinyint(3) unsigned NOT NULL DEFAULT '0', + `rollout` tinyint(3) unsigned NOT NULL DEFAULT '0', + `deleted_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_deploymentid_label` (`deployment_id`,`label`(8)), KEY `idx_versions_id` (`deployment_version_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `packages_diff`; -CREATE TABLE `packages_diff` ( +CREATE TABLE IF NOT EXISTS `packages_diff` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `package_id` int(11) unsigned NOT NULL DEFAULT '0', `diff_against_package_hash` varchar(64) NOT NULL DEFAULT '', @@ -97,12 +109,12 @@ CREATE TABLE `packages_diff` ( `diff_size` int(11) unsigned NOT NULL DEFAULT '0', `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `created_at` timestamp NULL DEFAULT NULL, + `deleted_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_packageid_hash` (`package_id`,`diff_against_package_hash`(40)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `packages_metrics`; -CREATE TABLE `packages_metrics` ( +CREATE TABLE IF NOT EXISTS `packages_metrics` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `package_id` int(10) unsigned NOT NULL DEFAULT '0', `active` int(10) unsigned NOT NULL DEFAULT '0', @@ -111,12 +123,12 @@ CREATE TABLE `packages_metrics` ( `installed` int(10) unsigned NOT NULL DEFAULT '0', `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `created_at` timestamp NULL DEFAULT NULL, + `deleted_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), - UNIQUE KEY `udx_packageid` (`package_id`) + KEY `idx_packageid` (`package_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `user_tokens`; -CREATE TABLE `user_tokens` ( +CREATE TABLE IF NOT EXISTS `user_tokens` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `uid` bigint(20) unsigned NOT NULL DEFAULT '0', `name` varchar(50) NOT NULL DEFAULT '', @@ -132,8 +144,7 @@ CREATE TABLE `user_tokens` ( KEY `idx_tokens` (`tokens`) KEY_BLOCK_SIZE=16 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `users`; -CREATE TABLE `users` ( +CREATE TABLE IF NOT EXISTS `users` ( `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL DEFAULT '', `password` varchar(255) NOT NULL DEFAULT '', @@ -150,4 +161,38 @@ CREATE TABLE `users` ( INSERT INTO `users` (`id`, `username`, `password`, `email`, `identical`, `ack_code`, `updated_at`, `created_at`) VALUES - (1,'admin','$2a$12$mvUY9kTqW4kSoGuZFDW0sOSgKmNY8SPHVyVrSckBTLtXKf6vKX3W.','lisong2010@gmail.com','4ksvOXqog','oZmGE','2016-11-14 10:46:55','2016-02-29 21:24:49'); + (1,'admin','$2a$12$mvUY9kTqW4kSoGuZFDW0sOSgKmNY8SPHVyVrSckBTLtXKf6vKX3W.','lisong2010@gmail.com','4ksvOXqog','oZmGE','2016-11-14 10:46:55','2016-02-29 21:24:49'); + + +CREATE TABLE IF NOT EXISTS `versions` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `type` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '1.DBversion', + `version` varchar(10) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + UNIQUE KEY `udx_type` (`type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +LOCK TABLES `versions` WRITE; +INSERT INTO `versions` (`id`, `type`, `version`) +VALUES + (1,1,'0.5.0'); +UNLOCK TABLES; + +CREATE TABLE IF NOT EXISTS `log_report_deploy` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `status` tinyint(3) unsigned NOT NULL DEFAULT '0', + `package_id` int(10) unsigned NOT NULL DEFAULT '0', + `client_unique_id` varchar(100) NOT NULL DEFAULT '', + `previous_label` varchar(20) NOT NULL DEFAULT '', + `previous_deployment_key` varchar(64) NOT NULL DEFAULT '', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `log_report_download` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `package_id` int(10) unsigned NOT NULL DEFAULT '0', + `client_unique_id` varchar(100) NOT NULL DEFAULT '', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..9419e94d --- /dev/null +++ b/docs/README.md @@ -0,0 +1,238 @@ + +## INSTALL NODE AND NPM + +[see](https://nodejs.org/en/download/) + +> (chosen latest LTS version) + +## INSTALL PM2 + +```bash +$ sudo npm i -g pm2 +``` + +## INSTALL MYSQL + +- [Linux](https://dev.mysql.com/doc/refman/8.0/en/linux-installation.html) +- [macOS](https://dev.mysql.com/doc/refman/8.0/en/osx-installation.html) +- [Microsoft Windows](https://dev.mysql.com/doc/refman/8.0/en/windows-installation.html) +- [Others](https://dev.mysql.com/doc/refman/8.0/en/installing.html) + +> notice. mysql8.x default auth caching_sha2_pasword not support in node-mysql2 see [issue](https://github.com/mysqljs/mysql/pull/1962) + + + +## GET code-push-server FROM NPM + +```shell +$ npm install code-push-server@latest -g +``` + + +## GET code-push-server FROM SOURCE CODE + +```shell +$ git clone https://github.com/lisong/code-push-server.git +$ cd code-push-server +$ npm install +``` + +## INIT DATABASE + +```shell +$ code-push-server-db init --dbhost "your mysql host" --dbport "your mysql port" --dbuser "your mysql user" --dbpassword "your mysql password" +``` + +or from source code + +```shell +$ ./bin/db init --dbhost "your mysql host" --dbport "your mysql port" --dbuser "your mysql user" --dbpassword "your mysql password" +``` + +> output: success + +## CONFIGURE for code-push-server + +save the file [config.js](https://github.com/lisong/code-push-server/blob/master/config/config.js) + +some config have to change: + +- `local`.`storageDir` change to your directory,make sure have read/write permissions. +- `local`.`downloadUrl` replace `127.0.0.1` to your machine ip. +- `common`.`dataDir` change to your directory,make sure have read/write permissions. +- `jwt`.`tokenSecret` get the random string from `https://www.grc.com/passwords.htm`, and replace the value `INSERT_RANDOM_TOKEN_KEY`. +- `db` config: `username`,`password`,`host`,`port` change your own's + +## CONFIGURE for pm2 + +save the file [process.json](https://github.com/lisong/code-push-server/blob/master/docs/process.json) + +some config have to change: + +- `script` if you install code-push-server from npm use `code-push-server`,or use `"your source code dir"/bin/www` +- `CONFIG_FILE` above config.js file path,use absolute path. + +## START SERVICE + +```shell +$ pm2 start process.json +``` + +## RESTART SERVICE + +```shell +$ pm2 restart process.json +``` + +## STOP SERVICE + +```shell +$ pm2 stop process.json +``` + +## CHECK SERVICE IS OK + +```shell +$ curl -I http://YOUR_CODE_PUSH_SERVER_IP:3000/ +``` + +> return httpCode `200 OK` + +```http +HTTP/1.1 200 OK +X-DNS-Prefetch-Control: off +X-Frame-Options: SAMEORIGIN +Strict-Transport-Security: max-age=15552000; includeSubDomains +X-Download-Options: noopen +X-Content-Type-Options: nosniff +X-XSS-Protection: 1; mode=block +Content-Type: text/html; charset=utf-8 +Content-Length: 592 +ETag: W/"250-IiCMcM1ZUFSswSYCU0KeFYFEMO8" +Date: Sat, 25 Aug 2018 15:45:46 GMT +Connection: keep-alive +``` + + +## Use redis impove concurrent and security + +> config redis in config.js + +- `updateCheckCache` +- `rolloutClientUniqueIdCache` +- `tryLoginTimes` + + +## UPGRADE + +*from npm package* + +```shell +$ npm install -g code-push-server@latest +$ code-push-server-db upgrade --dbhost "your mysql host" --dbport "your mysql port" --dbuser "your mysql user" --dbpassword "your mysql password" # upgrade codepush database +$ pm2 restart code-push-server # restart service +``` + +*from source code* + +```shell +$ cd /path/to/code-push-server +$ git pull --rebase origin master +$ ./bin/db upgrade --dbhost "your mysql host" --dbport "your mysql port" --dbuser "your mysql user" --dbpassword "your mysql password" +# upgrade codepush database +$ pm2 restart code-push-server # restart service +``` + + +## view pm2 logs + +```shell +$ pm2 ls +$ pm2 show code-push-server +$ tail -f "output file path" +``` + + +## Support Storage mode + +- local (default) +- qiniu (qiniu) +- s3 (aws) +- oss (aliyun) +- tencentcloud + +## Default listen Host/Port 0.0.0.0/3000 + +> you can change it in process.json, env: PORT,HOST + + +## [code-push-cli](https://github.com/Microsoft/code-push) + +> Use code-push-cli manager CodePushServer + +```shell +$ npm install code-push-cli@latest -g +$ code-push login http://YOU_SERVICE_IP:3000 #login in browser account:admin password:123456 +``` + +> change admin password eg. + +```shell +$ curl -X PATCH -H "Authorization: Bearer mytoken" -H "Accept: application/json" -H "Content-Type:application/json" -d '{"oldPassword":"123456","newPassword":"654321"}' http://YOU_SERVICE_IP:3000/users/password +``` + + +## config react-native project + +> Follow the react-native-code-push docs, addition iOS add a new entry named CodePushServerURL, whose value is the key of ourself CodePushServer URL. Android use the new CodePush constructor in MainApplication point CodePushServerUrl + +iOS eg. in file Info.plist + +```xml +... +CodePushDeploymentKey +YourCodePushKey +CodePushServerURL +YourCodePushServerUrl +... +``` + +Android eg. in file MainApplication.java + +```java +@Override +protected List getPackages() { + return Arrays.asList( + new MainReactPackage(), + new CodePush( + "YourKey", + MainApplication.this, + BuildConfig.DEBUG, + "YourCodePushServerUrl" + ) + ); +} +``` + + +## [cordova-plugin-code-push](https://github.com/Microsoft/cordova-plugin-code-push) for cordova + +```shell +$ cd /path/to/project +$ cordova plugin add cordova-plugin-code-push@latest --save +``` + +## config cordova project + +edit config.xml. add code below. + +```xml + + + + + + + + +``` diff --git a/docs/process.json b/docs/process.json index de88022b..8ef0e3e1 100644 --- a/docs/process.json +++ b/docs/process.json @@ -10,6 +10,14 @@ "NODE_ENV" : "production", "PORT" : 3000, "CONFIG_FILE" : "/path/to/production/config.js" + + // Must set add config when STORAGE_TYPE is upyun + // "STORAGE_TYPE" : "upyun", + // "DOWNLOAD_URL" : "", + // "UPYUN_STORAGE_DIR" : "", + // "UPYUN_SERVICE_NAME" : "", + // "UPYUN_OPERATOR_NAME" : "", + // "UPYUN_OPERATOR_PASS" : "" } } ] diff --git a/docs/react-native-code-push.md b/docs/react-native-code-push.md index 3dce5fe1..0b70bf37 100644 --- a/docs/react-native-code-push.md +++ b/docs/react-native-code-push.md @@ -36,26 +36,23 @@ $ npm install code-push-cli@latest -g ```shell $ react-native init CodePushDemo #初始化一个react-native项目 $ cd CodePushDemo -$ npm install --save react-native-code-push #安装react-native-code-push +$ npm install --save react-native-code-push@latest #安装react-native-code-push $ react-native link react-native-code-push #连接到项目中,提示输入配置可以先行忽略 ``` -#### 4. [code-push-server](https://github.com/lisong/code-push-server) 微软云服务在中国太慢,可以用它搭建自己的服务端。具体配置参考该项目 +#### 4. [code-push-server](https://github.com/lisong/code-push-server) 微软云服务在中国太慢,可以用它搭建自己的服务端。 -```shell -$ npm install code-push-server -g -$ code-push-server-db init --dbhost localhost --dbuser root --dbpassword #初始化数据库 -$ code-push-server #启动服务 浏览器中打开 http://127.0.0.1:3000 -``` +- [docker](https://github.com/lisong/code-push-server/blob/master/docker/README.md) (recommend) +- [manual operation](https://github.com/lisong/code-push-server/blob/master/docs/README.md) ## 创建服务端应用 基于code-push-server服务 ```shell -$ code-push login http://127.0.0.1:3000 #浏览器中登录获取token,用户名:admin, 密码:123456 -$ code-push app add CodePushDemo-ios #创建iOS版, 获取Production DeploymentKey -$ code-push app add CodePushDemo-android #创建android版,获取获取Production DeploymentKey +$ code-push login http://YOUR_CODE_PUSH_SERVER_IP:3000 #浏览器中登录获取token,用户名:admin, 密码:123456 +$ code-push app add CodePushDemoiOS ios react-native #创建iOS版, 获取Production DeploymentKey +$ code-push app add CodePushDemoAndroid android react-native #创建android版,获取获取Production DeploymentKey ``` ## 配置CodePushDemo react-native项目 @@ -66,7 +63,7 @@ $ code-push app add CodePushDemo-android #创建android版,获取获取Product 1. `CodePushDeploymentKey`值设置为CodePushDemo-ios的Production DeploymentKey值。 -2. `CodePushServerURL`值设置为code-push-server服务地址 http://127.0.0.1:3000/ 不在同一台机器的时候,请将127.0.0.1改成外网ip或者域名地址。 +2. `CodePushServerURL`值设置为code-push-server服务地址 http://YOUR_CODE_PUSH_SERVER_IP:3000/ 不在同一台机器的时候,请将YOUR_CODE_PUSH_SERVER_IP改成外网ip或者域名地址。 3. 将默认版本号1.0改成三位1.0.0 @@ -85,12 +82,10 @@ $ code-push app add CodePushDemo-android #创建android版,获取获取Product 1. `YourKey`替换成CodePushDemo-android的Production DeploymentKey值 -2. `YourCodePushServerUrl`值设置为code-push-server服务地址 http://127.0.0.1:3000/ 不在同一台机器的时候,请将127.0.0.1改成外网ip或者域名地址。 +2. `YourCodePushServerUrl`值设置为code-push-server服务地址 http://YOUR_CODE_PUSH_SERVER_IP:3000/ 不在同一台机器的时候,请将YOUR_CODE_PUSH_SERVER_IP改成外网ip或者域名地址。 3. 将默认版本号1.0改成三位1.0.0 -4. android模拟器和code-push-server在同一台机器上时,需要额外运行命令`adb reverse tcp:3000 tcp:3000` 代理端口,否则无法访问127.0.0.1:3000端口 - ```java @Override protected List getPackages() { @@ -108,7 +103,7 @@ protected List getPackages() { ## 添加更新检查 -可以参考[demo.js](https://github.com/lisong/code-push-demo-app/blob/master/demo.js) +可以参考[code-push-demo-app](https://github.com/lisong/code-push-demo-app/) 可以在入口componentDidMount添加 ```javascript @@ -124,16 +119,6 @@ CodePush.sync({ import CodePush from "react-native-code-push" ``` -> notice: -> -> demo.js中用到ECMAScript中Decorators语法,需要安装`$ npm install babel-preset-react-native-stage-0 --save`, -> 同时在.babelrc中添加'react-native-stage-0/decorator-support'. -> -> eg. -> { -> "presets": ["react-native", "react-native-stage-0/decorator-support"] -> } - ## 运行CodePushDemo react-native项目 #### iOS @@ -163,16 +148,11 @@ $ code-push release-react CodePushDemo-ios ios -d Production #iOS版 $ code-push release-react CodePushDemo-android android -d Production #android版 ``` +## 例子 -## 注意事项 +[code-push-demo-app](https://github.com/lisong/code-push-demo-app) -- 苹果允许使用热更新[Apple's developer agreement](https://developer.apple.com/programs/ios/information/iOS_Program_Information_4_3_15.pdf), 但是规定不能弹框提示用户更新,影响用户体验。 而Google Play恰好相反,必须弹框告知用户更新。然而中国的android市场都必须关闭更新弹框,否则会在审核应用时以“请上传最新版本的二进制应用包”驳回应用。 -- react-native 不同平台bundle包不一样,在使用code-push-server的时候必须创建不同的应用来区分(eg. CodePushDemo-ios 和 CodePushDemo-android) -- react-native-code-push只更新资源文件,不会更新java和Objective C,所以npm升级依赖包版本的时候,如果依赖包使用的本地化实现, 这时候必须更改应用版本号(ios修改Info.plist中的CFBundleShortVersionString, android修改build.gradle中的versionName), 然后重新编译app发布到应用商店。 -- 推荐使用code-push release-react 命令发布应用,该命令合并了打包和发布命令(eg. code-push release-react CodePushDemo-ios ios -d Production) -- 每次向App Store提交新的版本时,也应该基于该提交版本同时向code-push-server发布一个初始版本。(因为后面每次向code-push-server发布版本时,code-puse-server都会和初始版本比较,生成补丁版本) -## 例子 +### 更多信息参考[code-push-server](https://github.com/lisong/code-push-server) -[code-push-demo-app](https://github.com/lisong/code-push-demo-app) diff --git a/models/apps.js b/models/apps.js index 3261bc6e..00e0372f 100644 --- a/models/apps.js +++ b/models/apps.js @@ -10,6 +10,9 @@ module.exports = function(sequelize, DataTypes) { }, name: DataTypes.STRING, uid: DataTypes.BIGINT(20), + os: DataTypes.INTEGER(3), + platform: DataTypes.INTEGER(3), + is_use_diff_text: DataTypes.INTEGER(3), created_at: DataTypes.DATE, updated_at: DataTypes.DATE, }, { diff --git a/models/collaborators.js b/models/collaborators.js index 6440b358..c5f8d268 100644 --- a/models/collaborators.js +++ b/models/collaborators.js @@ -16,17 +16,14 @@ module.exports = function(sequelize, DataTypes) { }, { tableName: 'collaborators', underscored: true, - paranoid: true, - classMethods: { - findByAppNameAndUid: function(uid, appName) { - var sql = "SELECT b.* FROM `apps` as a left join `collaborators` as b on (a.id = b.appid) where a.name= :appName and b.uid = :uid and a.`deleted_at` IS NULL and b.`deleted_at` IS NULL limit 0,1"; - return sequelize.query(sql, { replacements: { appName: appName, uid: uid }, model: Collaborators}) - .then(function(data) { - return data.pop(); - }); - } - } - + paranoid: true }); + Collaborators.findByAppNameAndUid = function (uid, appName) { + var sql = "SELECT b.* FROM `apps` as a left join `collaborators` as b on (a.id = b.appid) where a.name= :appName and b.uid = :uid and a.`deleted_at` IS NULL and b.`deleted_at` IS NULL limit 0,1"; + return sequelize.query(sql, { replacements: { appName: appName, uid: uid }, model: Collaborators}) + .then(function(data) { + return data.pop(); + }); + }; return Collaborators; }; diff --git a/models/deployments.js b/models/deployments.js index 122b1b36..fade4b93 100644 --- a/models/deployments.js +++ b/models/deployments.js @@ -22,25 +22,24 @@ module.exports = function(sequelize, DataTypes) { }, { tableName: 'deployments', underscored: true, - paranoid: true, - classMethods: { - generateLabelId: function(deploymentId) { - var self = this; - return sequelize.transaction(function (t) { - return self.findById(deploymentId, {transaction: t,lock: t.LOCK.UPDATE}).then(function (data) { - if (_.isEmpty(data)){ - throw new AppError.AppError("does not find deployment"); - } - data.label_id = data.label_id + 1; - return data.save({transaction: t}) - .then(function (data) { - return data.label_id; - }); - }); - }); - } - } + paranoid: true }); + Deployments.generateLabelId = function(deploymentId) { + var self = this; + return sequelize.transaction(function (t) { + return self.findById(deploymentId, {transaction: t,lock: t.LOCK.UPDATE}).then(function (data) { + if (_.isEmpty(data)){ + throw new AppError.AppError("does not find deployment"); + } + data.label_id = data.label_id + 1; + return data.save({transaction: t}) + .then(function (data) { + return data.label_id; + }); + }); + }); + }; + return Deployments; }; diff --git a/models/deployments_versions.js b/models/deployments_versions.js index d42d06dc..41105ec8 100644 --- a/models/deployments_versions.js +++ b/models/deployments_versions.js @@ -11,6 +11,8 @@ module.exports = function(sequelize, DataTypes) { deployment_id: DataTypes.INTEGER(10), app_version: DataTypes.STRING, current_package_id: DataTypes.INTEGER(10), + min_version: DataTypes.BIGINT(20), + max_version: DataTypes.BIGINT(20), created_at: DataTypes.DATE, updated_at: DataTypes.DATE, }, { diff --git a/models/log_report_deploy.js b/models/log_report_deploy.js new file mode 100644 index 00000000..f8c39d5e --- /dev/null +++ b/models/log_report_deploy.js @@ -0,0 +1,24 @@ +"use strict"; + +module.exports = function(sequelize, DataTypes) { + var LogReportDeploy = sequelize.define("LogReportDeploy", { + id: { + type: DataTypes.BIGINT(20), + allowNull: false, + autoIncrement: true, + primaryKey: true + }, + status: DataTypes.INTEGER(3), + package_id : DataTypes.INTEGER(10), + client_unique_id : DataTypes.STRING, + previous_label : DataTypes.STRING, + previous_deployment_key : DataTypes.STRING, + created_at: DataTypes.DATE, + }, { + tableName: 'log_report_deploy', + underscored: true, + updatedAt: false, + paranoid: true + }); + return LogReportDeploy; +}; diff --git a/models/log_report_download.js b/models/log_report_download.js new file mode 100644 index 00000000..62966cf9 --- /dev/null +++ b/models/log_report_download.js @@ -0,0 +1,21 @@ +"use strict"; + +module.exports = function(sequelize, DataTypes) { + var LogReportDownload = sequelize.define("LogReportDownload", { + id: { + type: DataTypes.BIGINT(20), + allowNull: false, + autoIncrement: true, + primaryKey: true + }, + package_id : DataTypes.INTEGER(10), + client_unique_id : DataTypes.STRING, + created_at: DataTypes.DATE, + }, { + tableName: 'log_report_download', + underscored: true, + updatedAt: false, + paranoid: true + }); + return LogReportDownload; +}; diff --git a/models/packages.js b/models/packages.js index 6ec423db..989eb32a 100644 --- a/models/packages.js +++ b/models/packages.js @@ -21,6 +21,8 @@ module.exports = function(sequelize, DataTypes) { original_deployment: DataTypes.STRING, released_by: DataTypes.STRING, is_mandatory: DataTypes.INTEGER(3), + is_disabled: DataTypes.INTEGER(3), + rollout: DataTypes.INTEGER(3), created_at: DataTypes.DATE, updated_at: DataTypes.DATE, }, { diff --git a/models/packages_metrics.js b/models/packages_metrics.js index 86a1acff..d95bce92 100644 --- a/models/packages_metrics.js +++ b/models/packages_metrics.js @@ -20,41 +20,7 @@ module.exports = function(sequelize, DataTypes) { }, { tableName: 'packages_metrics', underscored: true, - paranoid: true, - classMethods: { - addOne : function (packageId, fieldName) { - var self = this; - var sql = 'UPDATE packages_metrics SET `' + fieldName + '`=`' + fieldName + '` + 1 WHERE package_id = :package_id'; - return sequelize.query(sql, { replacements: { package_id: packageId}}) - .spread(function(results, metadata) { - if (_.eq(results.affectedRows, 0)) { - var params = { - package_id: packageId, - active: 0, - downloaded: 0, - failed: 0, - installed: 0, - }; - params[fieldName] = 1; - return self.create(params); - }else { - return true; - } - }); - }, - addOneOnDownloadById: function (packageId) { - return this.addOne(packageId, 'downloaded'); - }, - addOneOnFailedById: function (packageId) { - return this.addOne(packageId, 'failed'); - }, - addOneOnInstalledById: function (packageId) { - return this.addOne(packageId, 'installed'); - }, - addOneOnActiveById: function (packageId) { - return this.addOne(packageId, 'active'); - }, - } + paranoid: true }); return PackagesMetrics; }; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..79668d14 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3860 @@ +{ + "name": "code-push-server", + "version": "0.5.4", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/babel-types": { + "version": "7.0.1", + "resolved": "http://registry.npm.taobao.org/@types/babel-types/download/@types/babel-types-7.0.1.tgz", + "integrity": "sha1-FAXlOWloxDAplLAWHOQFtyuHQlc=" + }, + "@types/babylon": { + "version": "6.16.2", + "resolved": "http://registry.npm.taobao.org/@types/babylon/download/@types/babylon-6.16.2.tgz", + "integrity": "sha1-BizmO2k9mvHCRvWu35KLycMFicg=", + "requires": { + "@types/babel-types": "*" + } + }, + "@types/geojson": { + "version": "1.0.6", + "resolved": "http://registry.npm.taobao.org/@types/geojson/download/@types/geojson-1.0.6.tgz", + "integrity": "sha1-PgKXJyjGkkjCrwjWCkjLuGgP/98=" + }, + "@types/node": { + "version": "9.4.7", + "resolved": "http://registry.npm.taobao.org/@types/node/download/@types/node-9.4.7.tgz", + "integrity": "sha1-V9gc2YcZ3yyd4Rjy1fOxEg3NcnU=" + }, + "abbrev": { + "version": "1.1.1", + "resolved": "http://registry.npm.taobao.org/abbrev/download/abbrev-1.1.1.tgz", + "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg=" + }, + "accepts": { + "version": "1.3.5", + "resolved": "http://registry.npm.taobao.org/accepts/download/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "~2.1.18", + "negotiator": "0.6.1" + } + }, + "acorn": { + "version": "3.3.0", + "resolved": "http://registry.npm.taobao.org/acorn/download/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" + }, + "acorn-globals": { + "version": "3.1.0", + "resolved": "http://registry.npm.taobao.org/acorn-globals/download/acorn-globals-3.1.0.tgz", + "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", + "requires": { + "acorn": "^4.0.4" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "http://registry.npm.taobao.org/acorn/download/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, + "address": { + "version": "1.0.3", + "resolved": "http://registry.npm.taobao.org/address/download/address-1.0.3.tgz", + "integrity": "sha1-tfUGMfjWzsi9IMljljr7VeBsvOk=" + }, + "agentkeepalive": { + "version": "3.3.0", + "resolved": "http://registry.npm.taobao.org/agentkeepalive/download/agentkeepalive-3.3.0.tgz", + "integrity": "sha1-bV3lgpr9O+JxIgGjknX9EcZRhXw=", + "requires": { + "humanize-ms": "^1.2.1" + } + }, + "ajv": { + "version": "5.5.2", + "resolved": "http://registry.npm.taobao.org/ajv/download/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "align-text": { + "version": "0.1.4", + "resolved": "http://registry.npm.taobao.org/align-text/download/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "aliyun-oss-upload-stream": { + "version": "1.3.0", + "resolved": "http://registry.npm.taobao.org/aliyun-oss-upload-stream/download/aliyun-oss-upload-stream-1.3.0.tgz", + "integrity": "sha1-ODAbGfA0QGhDjrY5d6DNldYEcMA=" + }, + "aliyun-sdk": { + "version": "1.11.10", + "resolved": "http://registry.npm.taobao.org/aliyun-sdk/download/aliyun-sdk-1.11.10.tgz", + "integrity": "sha1-ZDvH+GDrv08F7mmfoGp05eQR8lA=", + "requires": { + "node_memcached": "1.1.3", + "pomelo-protobuf": "^0.4.0", + "protobufjs": "^4.1.2", + "xml2js": "0.4.4", + "xmlbuilder": "^2.4.5" + } + }, + "ambi": { + "version": "2.5.0", + "resolved": "http://registry.npm.taobao.org/ambi/download/ambi-2.5.0.tgz", + "integrity": "sha1-fI43K+SIkRV+fOoBy2+RQ9H3QiA=", + "requires": { + "editions": "^1.1.1", + "typechecker": "^4.3.0" + }, + "dependencies": { + "typechecker": { + "version": "4.5.0", + "resolved": "http://registry.npm.taobao.org/typechecker/download/typechecker-4.5.0.tgz", + "integrity": "sha1-w4KSAJeBI2S7r0WVsKtliCRBF6Y=", + "requires": { + "editions": "^1.3.4" + } + } + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/amdefine/download/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "http://registry.npm.taobao.org/ansi-regex/download/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansicolors": { + "version": "0.2.1", + "resolved": "http://registry.npm.taobao.org/ansicolors/download/ansicolors-0.2.1.tgz", + "integrity": "sha1-vgiVmQl7dKXJxKhKDNvNtivYeu8=" + }, + "any-promise": { + "version": "1.3.0", + "resolved": "http://registry.npm.taobao.org/any-promise/download/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, + "argparse": { + "version": "1.0.10", + "resolved": "http://registry.npm.taobao.org/argparse/download/argparse-1.0.10.tgz", + "integrity": "sha1-vNZ5HqWuCXJeF+WtmIE0zUCz2RE=", + "requires": { + "sprintf-js": "~1.0.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.0.3", + "resolved": "http://registry.npm.taobao.org/sprintf-js/download/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + } + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "http://registry.npm.taobao.org/array-flatten/download/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "asap": { + "version": "2.0.6", + "resolved": "http://registry.npm.taobao.org/asap/download/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "ascli": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/ascli/download/ascli-1.0.1.tgz", + "integrity": "sha1-vPpZdKYvGOgcq660lzKrSoj5Brw=", + "requires": { + "colour": "~0.7.1", + "optjs": "~3.2.2" + } + }, + "asn1": { + "version": "0.2.3", + "resolved": "http://registry.npm.taobao.org/asn1/download/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/assert-plus/download/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "async": { + "version": "1.5.2", + "resolved": "http://registry.npm.taobao.org/async/download/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "http://registry.npm.taobao.org/asynckit/download/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sdk": { + "version": "2.211.0", + "resolved": "http://registry.npm.taobao.org/aws-sdk/download/aws-sdk-2.211.0.tgz", + "integrity": "sha1-kCt1Jedv+fpyzQOLc7ikfIxnGmI=", + "requires": { + "buffer": "4.9.1", + "events": "^1.1.1", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.1.0", + "xml2js": "0.4.17", + "xmlbuilder": "4.2.1" + }, + "dependencies": { + "sax": { + "version": "1.2.1", + "resolved": "http://registry.npm.taobao.org/sax/download/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "xml2js": { + "version": "0.4.17", + "resolved": "http://registry.npm.taobao.org/xml2js/download/xml2js-0.4.17.tgz", + "integrity": "sha1-F76T6q4/O3eTWceVtBlwWogX6Gg=", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "^4.1.0" + } + }, + "xmlbuilder": { + "version": "4.2.1", + "resolved": "http://registry.npm.taobao.org/xmlbuilder/download/xmlbuilder-4.2.1.tgz", + "integrity": "sha1-qlijBBoGb5DqoWwvU4n/GfP0YaU=", + "requires": { + "lodash": "^4.0.0" + } + } + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "http://registry.npm.taobao.org/aws-sign2/download/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.6.0", + "resolved": "http://registry.npm.taobao.org/aws4/download/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, + "axios": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", + "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==", + "requires": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" + } + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "http://registry.npm.taobao.org/babel-runtime/download/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "http://registry.npm.taobao.org/babel-types/download/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "http://registry.npm.taobao.org/babylon/download/babylon-6.18.0.tgz", + "integrity": "sha1-ry87iPpvXB5MY00aD46sT1WzleM=" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/balanced-match/download/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs=" + }, + "base64-js": { + "version": "1.2.3", + "resolved": "http://registry.npm.taobao.org/base64-js/download/base64-js-1.2.3.tgz", + "integrity": "sha1-+xNmgjPZYUz1+0vOlam6QJbN+AE=" + }, + "base64url": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/base64url/download/base64url-2.0.0.tgz", + "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" + }, + "basic-auth": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/basic-auth/download/basic-auth-2.0.0.tgz", + "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/bcrypt-pbkdf/download/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "http://registry.npm.taobao.org/bcryptjs/download/bcryptjs-2.4.3.tgz", + "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" + }, + "bluebird": { + "version": "3.5.1", + "resolved": "http://registry.npm.taobao.org/bluebird/download/bluebird-3.5.1.tgz", + "integrity": "sha1-2VUfnemPH82h5oPRfukaBgLuLrk=" + }, + "body-parser": { + "version": "1.18.2", + "resolved": "http://registry.npm.taobao.org/body-parser/download/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.1", + "http-errors": "~1.6.2", + "iconv-lite": "0.4.19", + "on-finished": "~2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "~1.6.15" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "http://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "boom": { + "version": "4.3.1", + "resolved": "http://registry.npm.taobao.org/boom/download/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "requires": { + "hoek": "4.x.x" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "http://registry.npm.taobao.org/brace-expansion/download/brace-expansion-1.1.11.tgz", + "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "http://registry.npm.taobao.org/browser-stdout/download/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "buffer": { + "version": "4.9.1", + "resolved": "http://registry.npm.taobao.org/buffer/download/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "http://registry.npm.taobao.org/buffer-crc32/download/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/buffer-equal-constant-time/download/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "bufferview": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/bufferview/download/bufferview-1.0.1.tgz", + "integrity": "sha1-ev10pF+Tf6QiodM4wIu/3HbNcl0=" + }, + "bytebuffer": { + "version": "4.1.0", + "resolved": "http://registry.npm.taobao.org/bytebuffer/download/bytebuffer-4.1.0.tgz", + "integrity": "sha1-TFgmngUqseSx9/82T9+zzogpBqo=", + "requires": { + "bufferview": "~1", + "long": "~2 >=2.3.0" + } + }, + "bytes": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/bytes/download/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "camelcase": { + "version": "2.1.1", + "resolved": "http://registry.npm.taobao.org/camelcase/download/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + }, + "camelize": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/camelize/download/camelize-1.0.0.tgz", + "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" + }, + "cardinal": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/cardinal/download/cardinal-1.0.0.tgz", + "integrity": "sha1-UOIcGwqjdyn5N33vGWtanOyTLuk=", + "requires": { + "ansicolors": "~0.2.1", + "redeyed": "~1.0.0" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "http://registry.npm.taobao.org/caseless/download/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "center-align": { + "version": "0.1.3", + "resolved": "http://registry.npm.taobao.org/center-align/download/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "character-parser": { + "version": "2.2.0", + "resolved": "http://registry.npm.taobao.org/character-parser/download/character-parser-2.2.0.tgz", + "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", + "requires": { + "is-regex": "^1.0.3" + } + }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, + "circular-json": { + "version": "0.5.5", + "resolved": "http://registry.npm.taobao.org/circular-json/download/circular-json-0.5.5.tgz", + "integrity": "sha1-ZBgu81kELTfNjnZ/yd6Hix6UR9M=" + }, + "clean-css": { + "version": "3.4.28", + "resolved": "http://registry.npm.taobao.org/clean-css/download/clean-css-3.4.28.tgz", + "integrity": "sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=", + "requires": { + "commander": "2.8.x", + "source-map": "0.4.x" + }, + "dependencies": { + "commander": { + "version": "2.8.1", + "resolved": "http://registry.npm.taobao.org/commander/download/commander-2.8.1.tgz", + "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "requires": { + "graceful-readlink": ">= 1.0.0" + } + }, + "source-map": { + "version": "0.4.4", + "resolved": "http://registry.npm.taobao.org/source-map/download/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "cliui": { + "version": "3.2.0", + "resolved": "http://registry.npm.taobao.org/cliui/download/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "cls-bluebird": { + "version": "2.1.0", + "resolved": "http://registry.npm.taobao.org/cls-bluebird/download/cls-bluebird-2.1.0.tgz", + "integrity": "sha1-N+8eCAqP+1XC9BZPU28ZGeeWiu4=", + "requires": { + "is-bluebird": "^1.0.2", + "shimmer": "^1.1.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "http://registry.npm.taobao.org/co/download/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/code-point-at/download/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "coffee-script": { + "version": "1.12.7", + "resolved": "http://registry.npm.taobao.org/coffee-script/download/coffee-script-1.12.7.tgz", + "integrity": "sha1-wF2uDLeVkdBbMHCoQzqYyaiczFM=" + }, + "colour": { + "version": "0.7.1", + "resolved": "http://registry.npm.taobao.org/colour/download/colour-0.7.1.tgz", + "integrity": "sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g=" + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "http://registry.npm.taobao.org/combined-stream/download/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "http://registry.npm.taobao.org/component-emitter/download/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "http://registry.npm.taobao.org/concat-map/download/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.0", + "resolved": "http://registry.npm.taobao.org/concat-stream/download/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "configstore": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", + "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "requires": { + "dot-prop": "^4.1.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "unique-string": "^1.0.0", + "write-file-atomic": "^2.0.0", + "xdg-basedir": "^3.0.0" + } + }, + "constantinople": { + "version": "3.1.2", + "resolved": "http://registry.npm.taobao.org/constantinople/download/constantinople-3.1.2.tgz", + "integrity": "sha1-1F7XJPV9PRBQABen06iJwTga5kc=", + "requires": { + "@types/babel-types": "^7.0.0", + "@types/babylon": "^6.16.2", + "babel-types": "^6.26.0", + "babylon": "^6.18.0" + } + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "http://registry.npm.taobao.org/content-disposition/download/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-security-policy-builder": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/content-security-policy-builder/download/content-security-policy-builder-2.0.0.tgz", + "integrity": "sha1-h0mh1UL8voIjcoHqn3Fs5os5TdI=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "http://registry.npm.taobao.org/content-type/download/content-type-1.0.4.tgz", + "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=" + }, + "cookie": { + "version": "0.3.1", + "resolved": "http://registry.npm.taobao.org/cookie/download/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-parser": { + "version": "1.4.3", + "resolved": "http://registry.npm.taobao.org/cookie-parser/download/cookie-parser-1.4.3.tgz", + "integrity": "sha1-D+MfoZ0AC5X0qt8fU/3CuKIDuqU=", + "requires": { + "cookie": "0.3.1", + "cookie-signature": "1.0.6" + } + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "http://registry.npm.taobao.org/cookie-signature/download/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "cookiejar": { + "version": "2.1.1", + "resolved": "http://registry.npm.taobao.org/cookiejar/download/cookiejar-2.1.1.tgz", + "integrity": "sha1-Qa1XsbVVlR7BcUEqgZQrHoIA00o=", + "dev": true + }, + "core-js": { + "version": "2.5.3", + "resolved": "http://registry.npm.taobao.org/core-js/download/core-js-2.5.3.tgz", + "integrity": "sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "http://registry.npm.taobao.org/core-util-is/download/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cos-nodejs-sdk-v5": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/cos-nodejs-sdk-v5/-/cos-nodejs-sdk-v5-2.4.10.tgz", + "integrity": "sha512-espPlCyyOc4vBKKI6Mwb5ZKzjYDgd0GOK4H0msiryupc3wlQCfPqJM+PubVlUEssqK58wIiV9Bq4Bo/ts2irjg==", + "requires": { + "configstore": "^3.1.2", + "qcloudapi-sdk": "^0.2.0", + "request": "^2.81.0", + "xml2js": "^0.4.19" + }, + "dependencies": { + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + } + } + }, + "crc32": { + "version": "0.2.2", + "resolved": "http://registry.npm.taobao.org/crc32/download/crc32-0.2.2.tgz", + "integrity": "sha1-etIg1v/c0Rn5/BJ6d3LKzqOQpLo=" + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "http://registry.npm.taobao.org/cross-spawn/download/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, + "cryptiles": { + "version": "3.1.2", + "resolved": "http://registry.npm.taobao.org/cryptiles/download/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "requires": { + "boom": "5.x.x" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "http://registry.npm.taobao.org/boom/download/boom-5.2.0.tgz", + "integrity": "sha1-XdnabuOl8wIHdDYpDLcX0/SlTgI=", + "requires": { + "hoek": "4.x.x" + } + } + } + }, + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" + }, + "csextends": { + "version": "1.1.1", + "resolved": "http://registry.npm.taobao.org/csextends/download/csextends-1.1.1.tgz", + "integrity": "sha1-zFPBNJ+vfwrmzfb2xKTZFW08TsE=", + "requires": { + "coffee-script": "^1.12.5" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "http://registry.npm.taobao.org/dashdash/download/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "dasherize": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/dasherize/download/dasherize-2.0.0.tgz", + "integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg=" + }, + "date-format": { + "version": "1.2.0", + "resolved": "http://registry.npm.taobao.org/date-format/download/date-format-1.2.0.tgz", + "integrity": "sha1-YV6CjiM90aubua4JUODOzPpuytg=" + }, + "debug": { + "version": "3.1.0", + "resolved": "http://registry.npm.taobao.org/debug/download/debug-3.1.0.tgz", + "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "http://registry.npm.taobao.org/decamelize/download/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "http://registry.npm.taobao.org/deep-is/download/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "default-user-agent": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/default-user-agent/download/default-user-agent-1.0.0.tgz", + "integrity": "sha1-FsRu/cq6PtxF8k8r1IaLAbfCrcY=", + "requires": { + "os-name": "~1.0.3" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/delayed-stream/download/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "denque": { + "version": "1.2.3", + "resolved": "http://registry.npm.taobao.org/denque/download/denque-1.2.3.tgz", + "integrity": "sha1-mMUMjdjN+uMYzFhZzI7j2g+bDMI=" + }, + "depd": { + "version": "1.1.2", + "resolved": "http://registry.npm.taobao.org/depd/download/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "http://registry.npm.taobao.org/destroy/download/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "diff": { + "version": "3.2.0", + "resolved": "http://registry.npm.taobao.org/diff/download/diff-3.2.0.tgz", + "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "dev": true + }, + "diff-match-patch": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.1.tgz", + "integrity": "sha512-A0QEhr4PxGUMEtKxd6X+JLnOTFd3BfIPSDpsc4dMvj+CbSaErDwTpoTo/nFJDMSrjxLW4BiNq+FbNisAAHhWeQ==" + }, + "digest-header": { + "version": "0.0.1", + "resolved": "http://registry.npm.taobao.org/digest-header/download/digest-header-0.0.1.tgz", + "integrity": "sha1-Ecz23uxXZqw3l0TZAcEsuklRS+Y=", + "requires": { + "utility": "0.1.11" + } + }, + "dns-prefetch-control": { + "version": "0.1.0", + "resolved": "http://registry.npm.taobao.org/dns-prefetch-control/download/dns-prefetch-control-0.1.0.tgz", + "integrity": "sha1-YN20V3dOF48flBXwyrsOhbCzALI=" + }, + "doctypes": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/doctypes/download/doctypes-1.1.0.tgz", + "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" + }, + "dont-sniff-mimetype": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/dont-sniff-mimetype/download/dont-sniff-mimetype-1.0.0.tgz", + "integrity": "sha1-WTKJDcn04vGeXrAqIAJuXl78j1g=" + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "requires": { + "is-obj": "^1.0.0" + } + }, + "dot-qs": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dot-qs/-/dot-qs-0.2.0.tgz", + "integrity": "sha1-02UX/iS3zaYfznpQJqACSvr1pDk=" + }, + "dottie": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/dottie/download/dottie-2.0.0.tgz", + "integrity": "sha1-2hkZgci41xPKARXViYzzl8Lw3dA=" + }, + "double-ended-queue": { + "version": "2.1.0-0", + "resolved": "http://registry.npm.taobao.org/double-ended-queue/download/double-ended-queue-2.1.0-0.tgz", + "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=" + }, + "eachr": { + "version": "2.0.4", + "resolved": "http://registry.npm.taobao.org/eachr/download/eachr-2.0.4.tgz", + "integrity": "sha1-Rm98qhBwj2EFCeMsgHqv5X/BIr8=", + "requires": { + "typechecker": "^2.0.8" + } + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "http://registry.npm.taobao.org/ecc-jsbn/download/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "~0.1.0" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.9", + "resolved": "http://registry.npm.taobao.org/ecdsa-sig-formatter/download/ecdsa-sig-formatter-1.0.9.tgz", + "integrity": "sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE=", + "requires": { + "base64url": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "editions": { + "version": "1.3.4", + "resolved": "http://registry.npm.taobao.org/editions/download/editions-1.3.4.tgz", + "integrity": "sha1-NmLLWSNHwxaOuOSYoP9zJx1n9Qs=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "http://registry.npm.taobao.org/ee-first/download/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "http://registry.npm.taobao.org/encodeurl/download/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "entities": { + "version": "1.1.1", + "resolved": "http://registry.npm.taobao.org/entities/download/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "http://registry.npm.taobao.org/escape-html/download/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "http://registry.npm.taobao.org/escape-string-regexp/download/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "http://registry.npm.taobao.org/esutils/download/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "etag": { + "version": "1.8.1", + "resolved": "http://registry.npm.taobao.org/etag/download/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "events": { + "version": "1.1.1", + "resolved": "http://registry.npm.taobao.org/events/download/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "execa": { + "version": "0.7.0", + "resolved": "http://registry.npm.taobao.org/execa/download/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "expect-ct": { + "version": "0.1.0", + "resolved": "http://registry.npm.taobao.org/expect-ct/download/expect-ct-0.1.0.tgz", + "integrity": "sha1-UnNWeN4YUwiQ2Ne5XwrGNkCVgJQ=" + }, + "express": { + "version": "4.16.3", + "resolved": "http://registry.npm.taobao.org/express/download/express-4.16.3.tgz", + "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", + "requires": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.2", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.3", + "qs": "6.5.1", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.1", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "http://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "requires": { + "ms": "2.0.0" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/setprototypeof/download/setprototypeof-1.1.0.tgz", + "integrity": "sha1-0L2FU2iHtv58DYGMuWLZ2RxU5lY=" + } + } + }, + "extend": { + "version": "3.0.1", + "resolved": "http://registry.npm.taobao.org/extend/download/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extendr": { + "version": "2.1.0", + "resolved": "http://registry.npm.taobao.org/extendr/download/extendr-2.1.0.tgz", + "integrity": "sha1-MBqgu+pWX00tyPVw8qImEahSe1Y=", + "requires": { + "typechecker": "~2.0.1" + }, + "dependencies": { + "typechecker": { + "version": "2.0.8", + "resolved": "http://registry.npm.taobao.org/typechecker/download/typechecker-2.0.8.tgz", + "integrity": "sha1-6D2oS7ZMWEzLNFg4V2xAsDN9uC4=" + } + } + }, + "extract-opts": { + "version": "2.2.0", + "resolved": "http://registry.npm.taobao.org/extract-opts/download/extract-opts-2.2.0.tgz", + "integrity": "sha1-H6KOunNSxttID4hc63GkaBC+bX0=", + "requires": { + "typechecker": "~2.0.1" + }, + "dependencies": { + "typechecker": { + "version": "2.0.8", + "resolved": "http://registry.npm.taobao.org/typechecker/download/typechecker-2.0.8.tgz", + "integrity": "sha1-6D2oS7ZMWEzLNFg4V2xAsDN9uC4=" + } + } + }, + "extract-zip": { + "version": "1.6.6", + "resolved": "http://registry.npm.taobao.org/extract-zip/download/extract-zip-1.6.6.tgz", + "integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=", + "requires": { + "concat-stream": "1.6.0", + "debug": "2.6.9", + "mkdirp": "0.5.0", + "yauzl": "2.4.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "http://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "http://registry.npm.taobao.org/extsprintf/download/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/fast-deep-equal/download/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/fast-json-stable-stringify/download/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "http://registry.npm.taobao.org/fast-levenshtein/download/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/fd-slicer/download/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "requires": { + "pend": "~1.2.0" + } + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "http://registry.npm.taobao.org/finalhandler/download/finalhandler-1.1.1.tgz", + "integrity": "sha1-7r9O2EAHnIP0JJA4ydcDAIMBsQU=", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "http://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/find-up/download/find-up-3.0.0.tgz", + "integrity": "sha1-SRafHXmTQwZG2mHsxa41XCHJe3M=", + "requires": { + "locate-path": "^3.0.0" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "http://registry.npm.taobao.org/forever-agent/download/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.2", + "resolved": "http://registry.npm.taobao.org/form-data/download/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.1", + "resolved": "http://registry.npm.taobao.org/formidable/download/formidable-1.2.1.tgz", + "integrity": "sha1-cPt8oCkO5v+WEJBBX0s989IIJlk=" + }, + "formstream": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/formstream/download/formstream-1.1.0.tgz", + "integrity": "sha1-UfOXDyYTbrCtRDBN5M67UCB7RHk=", + "requires": { + "destroy": "^1.0.4", + "mime": "^1.3.4", + "pause-stream": "~0.0.11" + }, + "dependencies": { + "mime": { + "version": "1.6.0", + "resolved": "http://registry.npm.taobao.org/mime/download/mime-1.6.0.tgz", + "integrity": "sha1-Ms2eXGRVO9WNGaVor0Uqz/BJgbE=" + } + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "http://registry.npm.taobao.org/forwarded/download/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "frameguard": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/frameguard/download/frameguard-3.0.0.tgz", + "integrity": "sha1-e8rUae57lukdEs6zlZx4I1qScuk=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "http://registry.npm.taobao.org/fresh/download/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs-extra": { + "version": "7.0.0", + "resolved": "http://registry.npm.taobao.org/fs-extra/download/fs-extra-7.0.0.tgz", + "integrity": "sha1-jMP0fOB+97NZOhG5+yRffjTAQdY=", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/fs.realpath/download/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "http://registry.npm.taobao.org/function-bind/download/function-bind-1.1.1.tgz", + "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=" + }, + "generate-function": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/generate-function/download/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=" + }, + "generic-pool": { + "version": "3.4.2", + "resolved": "http://registry.npm.taobao.org/generic-pool/download/generic-pool-3.4.2.tgz", + "integrity": "sha1-kv9xllINZwg5pnMICSoSqt8valk=" + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "http://registry.npm.taobao.org/get-caller-file/download/get-caller-file-1.0.3.tgz", + "integrity": "sha1-+Xj6TJDR3+f/LWvtoqUV5xO9z0o=" + }, + "get-stream": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/get-stream/download/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "getpass": { + "version": "0.1.7", + "resolved": "http://registry.npm.taobao.org/getpass/download/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "5.0.15", + "resolved": "http://registry.npm.taobao.org/glob/download/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "http://registry.npm.taobao.org/graceful-fs/download/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/graceful-readlink/download/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" + }, + "growl": { + "version": "1.9.2", + "resolved": "http://registry.npm.taobao.org/growl/download/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "handlebars": { + "version": "4.0.11", + "resolved": "http://registry.npm.taobao.org/handlebars/download/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "dev": true, + "requires": { + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4", + "uglify-js": "^2.6" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "http://registry.npm.taobao.org/source-map/download/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/har-schema/download/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "http://registry.npm.taobao.org/har-validator/download/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "^5.1.0", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/has/download/has-1.0.1.tgz", + "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", + "requires": { + "function-bind": "^1.0.2" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/has-flag/download/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "hawk": { + "version": "6.0.2", + "resolved": "http://registry.npm.taobao.org/hawk/download/hawk-6.0.2.tgz", + "integrity": "sha1-r02RTrBl+bXOTZ0RwcshJu7MMDg=", + "requires": { + "boom": "4.x.x", + "cryptiles": "3.x.x", + "hoek": "4.x.x", + "sntp": "2.x.x" + } + }, + "he": { + "version": "1.1.1", + "resolved": "http://registry.npm.taobao.org/he/download/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "helmet": { + "version": "3.12.0", + "resolved": "http://registry.npm.taobao.org/helmet/download/helmet-3.12.0.tgz", + "integrity": "sha1-IJjjXPTlHGTC8dOGcLfTgqN32Sw=", + "requires": { + "dns-prefetch-control": "0.1.0", + "dont-sniff-mimetype": "1.0.0", + "expect-ct": "0.1.0", + "frameguard": "3.0.0", + "helmet-csp": "2.7.0", + "hide-powered-by": "1.0.0", + "hpkp": "2.0.0", + "hsts": "2.1.0", + "ienoopen": "1.0.0", + "nocache": "2.0.0", + "referrer-policy": "1.1.0", + "x-xss-protection": "1.1.0" + } + }, + "helmet-csp": { + "version": "2.7.0", + "resolved": "http://registry.npm.taobao.org/helmet-csp/download/helmet-csp-2.7.0.tgz", + "integrity": "sha1-eTQJRhfR/re7LcQ7t9nogw93RxY=", + "requires": { + "camelize": "1.0.0", + "content-security-policy-builder": "2.0.0", + "dasherize": "2.0.0", + "lodash.reduce": "4.6.0", + "platform": "1.3.5" + } + }, + "hide-powered-by": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/hide-powered-by/download/hide-powered-by-1.0.0.tgz", + "integrity": "sha1-SoWtZYgfYoV/xwr3F0oRhNzM4ys=" + }, + "hmacsha1": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hmacsha1/-/hmacsha1-1.0.0.tgz", + "integrity": "sha1-wbeuA6TqEWNICQrxT4FIwSk4qRc=" + }, + "hoek": { + "version": "4.2.1", + "resolved": "http://registry.npm.taobao.org/hoek/download/hoek-4.2.1.tgz", + "integrity": "sha1-ljRQKqEsRF3Vp8VzS1cruHOKrLs=" + }, + "hpkp": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/hpkp/download/hpkp-2.0.0.tgz", + "integrity": "sha1-EOFCJk52IVpdMMROxD3mTe5tFnI=" + }, + "hsts": { + "version": "2.1.0", + "resolved": "http://registry.npm.taobao.org/hsts/download/hsts-2.1.0.tgz", + "integrity": "sha1-y9bJGKI4X+4d1WgL+ys6GUwBIcw=" + }, + "http-errors": { + "version": "1.6.2", + "resolved": "http://registry.npm.taobao.org/http-errors/download/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": ">= 1.3.1 < 2" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "http://registry.npm.taobao.org/depd/download/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "http://registry.npm.taobao.org/http-signature/download/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "http://registry.npm.taobao.org/humanize-ms/download/humanize-ms-1.2.1.tgz", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "requires": { + "ms": "^2.0.0" + } + }, + "i18n": { + "version": "0.8.3", + "resolved": "http://registry.npm.taobao.org/i18n/download/i18n-0.8.3.tgz", + "integrity": "sha1-LYzxwkciYCwgQdAbpq5eqlE4jw4=", + "requires": { + "debug": "*", + "make-plural": "^3.0.3", + "math-interval-parser": "^1.1.0", + "messageformat": "^0.3.1", + "mustache": "*", + "sprintf-js": ">=1.0.3" + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "http://registry.npm.taobao.org/iconv-lite/download/iconv-lite-0.4.19.tgz", + "integrity": "sha1-90aPYBNfXl2tM5nAqBvpoWA6CCs=" + }, + "ieee754": { + "version": "1.1.10", + "resolved": "http://registry.npm.taobao.org/ieee754/download/ieee754-1.1.10.tgz", + "integrity": "sha1-cZpvewJoMeZL24OLDeG7ACm79xY=" + }, + "ienoopen": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/ienoopen/download/ienoopen-1.0.0.tgz", + "integrity": "sha1-NGpCj0dKrI9QzzeE6i0PFvYr2ms=" + }, + "ignorefs": { + "version": "1.2.0", + "resolved": "http://registry.npm.taobao.org/ignorefs/download/ignorefs-1.2.0.tgz", + "integrity": "sha1-2ln7hYl25KXkNwLM0fKC/byeV1Y=", + "requires": { + "editions": "^1.3.3", + "ignorepatterns": "^1.1.0" + } + }, + "ignorepatterns": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/ignorepatterns/download/ignorepatterns-1.1.0.tgz", + "integrity": "sha1-rI9DbyI5td+2bV8NOpBKh6xnzF4=" + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "http://registry.npm.taobao.org/inflight/download/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "http://registry.npm.taobao.org/inherits/download/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/invert-kv/download/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "ipaddr.js": { + "version": "1.6.0", + "resolved": "http://registry.npm.taobao.org/ipaddr.js/download/ipaddr.js-1.6.0.tgz", + "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=" + }, + "is-bluebird": { + "version": "1.0.2", + "resolved": "http://registry.npm.taobao.org/is-bluebird/download/is-bluebird-1.0.2.tgz", + "integrity": "sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI=" + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "http://registry.npm.taobao.org/is-buffer/download/is-buffer-1.1.6.tgz", + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=" + }, + "is-expression": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/is-expression/download/is-expression-3.0.0.tgz", + "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=", + "requires": { + "acorn": "~4.0.2", + "object-assign": "^4.0.1" + }, + "dependencies": { + "acorn": { + "version": "4.0.13", + "resolved": "http://registry.npm.taobao.org/acorn/download/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + } + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/is-fullwidth-code-point/download/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + }, + "is-promise": { + "version": "2.1.0", + "resolved": "http://registry.npm.taobao.org/is-promise/download/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + }, + "is-regex": { + "version": "1.0.4", + "resolved": "http://registry.npm.taobao.org/is-regex/download/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "requires": { + "has": "^1.0.1" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/is-stream/download/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/is-typedarray/download/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/isarray/download/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/isexe/download/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "http://registry.npm.taobao.org/isstream/download/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "istanbul": { + "version": "0.4.5", + "resolved": "http://registry.npm.taobao.org/istanbul/download/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "dev": true, + "requires": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "resolved": "http://registry.npm.taobao.org/abbrev/download/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "resolved": "http://registry.npm.taobao.org/escodegen/download/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "http://registry.npm.taobao.org/esprima/download/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "1.9.3", + "resolved": "http://registry.npm.taobao.org/estraverse/download/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "resolve": { + "version": "1.1.7", + "resolved": "http://registry.npm.taobao.org/resolve/download/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "source-map": { + "version": "0.2.0", + "resolved": "http://registry.npm.taobao.org/source-map/download/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "http://registry.npm.taobao.org/supports-color/download/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "jmespath": { + "version": "0.15.0", + "resolved": "http://registry.npm.taobao.org/jmespath/download/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, + "js-stringify": { + "version": "1.0.2", + "resolved": "http://registry.npm.taobao.org/js-stringify/download/js-stringify-1.0.2.tgz", + "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" + }, + "js-yaml": { + "version": "3.11.0", + "resolved": "http://registry.npm.taobao.org/js-yaml/download/js-yaml-3.11.0.tgz", + "integrity": "sha1-WXwai9VxUvJtYizkEXhRpR9euu8=", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.0", + "resolved": "http://registry.npm.taobao.org/esprima/download/esprima-4.0.0.tgz", + "integrity": "sha1-RJnt3NERDgshi6zy+n9/WfVcqAQ=", + "dev": true + } + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "http://registry.npm.taobao.org/jsbn/download/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "jschardet": { + "version": "1.6.0", + "resolved": "http://registry.npm.taobao.org/jschardet/download/jschardet-1.6.0.tgz", + "integrity": "sha1-x9GnHtz/KDnbL57DD8XV69PBpng=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "http://registry.npm.taobao.org/json-schema/download/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "http://registry.npm.taobao.org/json-schema-traverse/download/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "http://registry.npm.taobao.org/json-stringify-safe/download/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json3": { + "version": "3.3.2", + "resolved": "http://registry.npm.taobao.org/json3/download/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "http://registry.npm.taobao.org/jsonfile/download/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonwebtoken": { + "version": "8.2.0", + "resolved": "http://registry.npm.taobao.org/jsonwebtoken/download/jsonwebtoken-8.2.0.tgz", + "integrity": "sha1-aQ7DqefpXiiENHzj6eudOJqlmLM=", + "requires": { + "jws": "^3.1.4", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "xtend": "^4.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "http://registry.npm.taobao.org/ms/download/ms-2.1.1.tgz", + "integrity": "sha1-MKWGTrPrsKZvLr5tcnrwagnYbgo=" + } + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "http://registry.npm.taobao.org/jsprim/download/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jstransformer": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/jstransformer/download/jstransformer-1.0.0.tgz", + "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=", + "requires": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "jwa": { + "version": "1.1.5", + "resolved": "http://registry.npm.taobao.org/jwa/download/jwa-1.1.5.tgz", + "integrity": "sha1-oFUs4CIHQs1S4VN3SjKQXDDnVuU=", + "requires": { + "base64url": "2.0.0", + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.9", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.1.4", + "resolved": "http://registry.npm.taobao.org/jws/download/jws-3.1.4.tgz", + "integrity": "sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI=", + "requires": { + "base64url": "^2.0.0", + "jwa": "^1.1.4", + "safe-buffer": "^5.0.1" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "http://registry.npm.taobao.org/kind-of/download/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "http://registry.npm.taobao.org/lazy-cache/download/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + }, + "lcid": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/lcid/download/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "^1.0.0" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "http://registry.npm.taobao.org/levn/download/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "linkify-it": { + "version": "2.0.3", + "resolved": "http://registry.npm.taobao.org/linkify-it/download/linkify-it-2.0.3.tgz", + "integrity": "sha1-2UpGSPmxwXnWT6lykSaL22zpQ08=", + "requires": { + "uc.micro": "^1.0.1" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/locate-path/download/locate-path-3.0.0.tgz", + "integrity": "sha1-2+w7OrdZdYBxtY/ln8QYca8hQA4=", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.5", + "resolved": "http://registry.npm.taobao.org/lodash/download/lodash-4.17.5.tgz", + "integrity": "sha1-maktZcAnLevoyWtgV7yPv6O+1RE=" + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "http://registry.npm.taobao.org/lodash._baseassign/download/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "http://registry.npm.taobao.org/lodash._basecopy/download/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basecreate": { + "version": "3.0.3", + "resolved": "http://registry.npm.taobao.org/lodash._basecreate/download/lodash._basecreate-3.0.3.tgz", + "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "http://registry.npm.taobao.org/lodash._getnative/download/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "http://registry.npm.taobao.org/lodash._isiterateecall/download/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash.create": { + "version": "3.1.1", + "resolved": "http://registry.npm.taobao.org/lodash.create/download/lodash.create-3.1.1.tgz", + "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", + "dev": true, + "requires": { + "lodash._baseassign": "^3.0.0", + "lodash._basecreate": "^3.0.0", + "lodash._isiterateecall": "^3.0.0" + } + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "http://registry.npm.taobao.org/lodash.includes/download/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "http://registry.npm.taobao.org/lodash.isarguments/download/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "http://registry.npm.taobao.org/lodash.isarray/download/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "http://registry.npm.taobao.org/lodash.isboolean/download/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "http://registry.npm.taobao.org/lodash.isinteger/download/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "http://registry.npm.taobao.org/lodash.isnumber/download/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "http://registry.npm.taobao.org/lodash.isplainobject/download/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "http://registry.npm.taobao.org/lodash.isstring/download/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "http://registry.npm.taobao.org/lodash.keys/download/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "http://registry.npm.taobao.org/lodash.once/download/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, + "lodash.reduce": { + "version": "4.6.0", + "resolved": "http://registry.npm.taobao.org/lodash.reduce/download/lodash.reduce-4.6.0.tgz", + "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" + }, + "log4js": { + "version": "3.0.5", + "resolved": "http://registry.npm.taobao.org/log4js/download/log4js-3.0.5.tgz", + "integrity": "sha1-uAFGv+utaLQw1PNWlVbYpu3+8wM=", + "requires": { + "circular-json": "^0.5.5", + "date-format": "^1.2.0", + "debug": "^3.1.0", + "rfdc": "^1.1.2", + "streamroller": "0.7.0" + } + }, + "long": { + "version": "2.4.0", + "resolved": "http://registry.npm.taobao.org/long/download/long-2.4.0.tgz", + "integrity": "sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8=" + }, + "longest": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/longest/download/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + }, + "lru-cache": { + "version": "4.1.3", + "resolved": "http://registry.npm.taobao.org/lru-cache/download/lru-cache-4.1.3.tgz", + "integrity": "sha1-oRdc80lt/IQ2wVbDNLSVWZK85pw=", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "requires": { + "pify": "^3.0.0" + } + }, + "make-plural": { + "version": "3.0.6", + "resolved": "http://registry.npm.taobao.org/make-plural/download/make-plural-3.0.6.tgz", + "integrity": "sha1-IDOgO6wpC487uRJY9lud9+iwHKc=", + "requires": { + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npm.taobao.org/minimist/download/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "optional": true + } + } + }, + "markdown-it": { + "version": "8.4.1", + "resolved": "http://registry.npm.taobao.org/markdown-it/download/markdown-it-8.4.1.tgz", + "integrity": "sha1-IG/lmw5OG3inxzJQr5s0pK0Kr0Q=", + "requires": { + "argparse": "^1.0.7", + "entities": "~1.1.1", + "linkify-it": "^2.0.0", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "math-interval-parser": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/math-interval-parser/download/math-interval-parser-1.1.0.tgz", + "integrity": "sha1-2+2lsGsySZc8bfYXD94jhvCv2JM=", + "requires": { + "xregexp": "^2.0.0" + } + }, + "md5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", + "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", + "requires": { + "charenc": "~0.0.1", + "crypt": "~0.0.1", + "is-buffer": "~1.1.1" + } + }, + "mdurl": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/mdurl/download/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "http://registry.npm.taobao.org/media-typer/download/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "mem": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/mem/download/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/merge-descriptors/download/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "messageformat": { + "version": "0.3.1", + "resolved": "http://registry.npm.taobao.org/messageformat/download/messageformat-0.3.1.tgz", + "integrity": "sha1-5Y//gkXps5cXmeW0PbWLPpQX9aI=", + "requires": { + "async": "~1.5.2", + "glob": "~6.0.4", + "make-plural": "~3.0.3", + "nopt": "~3.0.6", + "watchr": "~2.4.13" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "http://registry.npm.taobao.org/glob/download/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "methods": { + "version": "1.1.2", + "resolved": "http://registry.npm.taobao.org/methods/download/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime-db": { + "version": "1.33.0", + "resolved": "http://registry.npm.taobao.org/mime-db/download/mime-db-1.33.0.tgz", + "integrity": "sha1-o0kgUKXLm2NFBUHjnZeI0icng9s=" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "http://registry.npm.taobao.org/mime-types/download/mime-types-2.1.18.tgz", + "integrity": "sha1-bzI/YKg9ERRvgx/xH9ZuL+VQO7g=", + "requires": { + "mime-db": "~1.33.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "http://registry.npm.taobao.org/mimic-fn/download/mimic-fn-1.2.0.tgz", + "integrity": "sha1-ggyGo5M0ZA6ZUWkovQP8qIBX0CI=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "http://registry.npm.taobao.org/minimatch/download/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npm.taobao.org/minimist/download/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.0", + "resolved": "http://registry.npm.taobao.org/mkdirp/download/mkdirp-0.5.0.tgz", + "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "3.5.3", + "resolved": "http://registry.npm.taobao.org/mocha/download/mocha-3.5.3.tgz", + "integrity": "sha1-HgSA/jbS2lhY0etqzDhBiybqog0=", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.9.0", + "debug": "2.6.8", + "diff": "3.2.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.1", + "growl": "1.9.2", + "he": "1.1.1", + "json3": "3.3.2", + "lodash.create": "3.1.1", + "mkdirp": "0.5.1", + "supports-color": "3.1.2" + }, + "dependencies": { + "commander": { + "version": "2.9.0", + "resolved": "http://registry.npm.taobao.org/commander/download/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true, + "requires": { + "graceful-readlink": ">= 1.0.0" + } + }, + "debug": { + "version": "2.6.8", + "resolved": "http://registry.npm.taobao.org/debug/download/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "glob": { + "version": "7.1.1", + "resolved": "http://registry.npm.taobao.org/glob/download/glob-7.1.1.tgz", + "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npm.taobao.org/mkdirp/download/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "supports-color": { + "version": "3.1.2", + "resolved": "http://registry.npm.taobao.org/supports-color/download/supports-color-3.1.2.tgz", + "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "moment": { + "version": "2.21.0", + "resolved": "http://registry.npm.taobao.org/moment/download/moment-2.21.0.tgz", + "integrity": "sha1-KhFLUdKm7J5tg8+AP4OKh42KAjo=" + }, + "moment-timezone": { + "version": "0.5.14", + "resolved": "http://registry.npm.taobao.org/moment-timezone/download/moment-timezone-0.5.14.tgz", + "integrity": "sha1-TrOP+VOLgBCLpGekWPPtQmjM/LE=", + "requires": { + "moment": ">= 2.9.0" + } + }, + "morgan": { + "version": "1.9.0", + "resolved": "http://registry.npm.taobao.org/morgan/download/morgan-1.9.0.tgz", + "integrity": "sha1-0B+mxlhZt2/PMbPLU6OCGjEdgFE=", + "requires": { + "basic-auth": "~2.0.0", + "debug": "2.6.9", + "depd": "~1.1.1", + "on-finished": "~2.3.0", + "on-headers": "~1.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "http://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mustache": { + "version": "2.3.0", + "resolved": "http://registry.npm.taobao.org/mustache/download/mustache-2.3.0.tgz", + "integrity": "sha1-QCj3d4sXcIpImTCm5SrDvKDaQdA=" + }, + "mysql2": { + "version": "1.5.2", + "resolved": "http://registry.npm.taobao.org/mysql2/download/mysql2-1.5.2.tgz", + "integrity": "sha1-5OBzg5uxCXJmmyQOx3Bh1/9Vz1k=", + "requires": { + "cardinal": "1.0.0", + "denque": "^1.1.1", + "generate-function": "^2.0.0", + "iconv-lite": "^0.4.18", + "long": "^4.0.0", + "lru-cache": "^4.1.1", + "named-placeholders": "1.1.1", + "object-assign": "^4.1.1", + "readable-stream": "2.3.2", + "safe-buffer": "^5.0.1", + "seq-queue": "0.0.5", + "sqlstring": "^2.2.0" + }, + "dependencies": { + "long": { + "version": "4.0.0", + "resolved": "http://registry.npm.taobao.org/long/download/long-4.0.0.tgz", + "integrity": "sha1-mntxz7fTYaGU6lVSQckvdGjVvyg=" + }, + "lru-cache": { + "version": "4.1.2", + "resolved": "http://registry.npm.taobao.org/lru-cache/download/lru-cache-4.1.2.tgz", + "integrity": "sha1-RSNLLm4vKzPaElYkxGZJKaAiTD8=", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "http://registry.npm.taobao.org/process-nextick-args/download/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "readable-stream": { + "version": "2.3.2", + "resolved": "http://registry.npm.taobao.org/readable-stream/download/readable-stream-2.3.2.tgz", + "integrity": "sha1-WgTfBeT1f+Pw3Gj90R3FyXx+b00=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "safe-buffer": "~5.1.0", + "string_decoder": "~1.0.0", + "util-deprecate": "~1.0.1" + } + } + } + }, + "named-placeholders": { + "version": "1.1.1", + "resolved": "http://registry.npm.taobao.org/named-placeholders/download/named-placeholders-1.1.1.tgz", + "integrity": "sha1-O3oNJiA910s6nfTJz7gnsvuQfmQ=", + "requires": { + "lru-cache": "2.5.0" + }, + "dependencies": { + "lru-cache": { + "version": "2.5.0", + "resolved": "http://registry.npm.taobao.org/lru-cache/download/lru-cache-2.5.0.tgz", + "integrity": "sha1-2COIrpyWC+y+oMc7uet5tsbOmus=" + } + } + }, + "negotiator": { + "version": "0.6.1", + "resolved": "http://registry.npm.taobao.org/negotiator/download/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "nocache": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/nocache/download/nocache-2.0.0.tgz", + "integrity": "sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA=" + }, + "node_memcached": { + "version": "1.1.3", + "resolved": "http://registry.npm.taobao.org/node_memcached/download/node_memcached-1.1.3.tgz", + "integrity": "sha1-icFSr4itKIF/ANiRyZBFHV1xLqg=", + "requires": { + "debug": "^2.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "http://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "nodemailer": { + "version": "4.6.3", + "resolved": "http://registry.npm.taobao.org/nodemailer/download/nodemailer-4.6.3.tgz", + "integrity": "sha1-w7fpf7cvRtSkdcQGoV7SOkXbzdw=" + }, + "nopt": { + "version": "3.0.6", + "resolved": "http://registry.npm.taobao.org/nopt/download/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "requires": { + "abbrev": "1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "http://registry.npm.taobao.org/npm-run-path/download/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/number-is-nan/download/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "http://registry.npm.taobao.org/oauth-sign/download/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "http://registry.npm.taobao.org/object-assign/download/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "http://registry.npm.taobao.org/on-finished/download/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/on-headers/download/on-headers-1.0.1.tgz", + "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" + }, + "once": { + "version": "1.4.0", + "resolved": "http://registry.npm.taobao.org/once/download/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "http://registry.npm.taobao.org/optimist/download/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.3", + "resolved": "http://registry.npm.taobao.org/wordwrap/download/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "http://registry.npm.taobao.org/optionator/download/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "optjs": { + "version": "3.2.2", + "resolved": "http://registry.npm.taobao.org/optjs/download/optjs-3.2.2.tgz", + "integrity": "sha1-aabOicRCpEQDFBrS+bNwvVu29O4=" + }, + "os-locale": { + "version": "1.4.0", + "resolved": "http://registry.npm.taobao.org/os-locale/download/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "requires": { + "lcid": "^1.0.0" + } + }, + "os-name": { + "version": "1.0.3", + "resolved": "http://registry.npm.taobao.org/os-name/download/os-name-1.0.3.tgz", + "integrity": "sha1-GzefZINa98Wn9JizV8uVIVwVnt8=", + "requires": { + "osx-release": "^1.0.0", + "win-release": "^1.0.0" + } + }, + "osx-release": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/osx-release/download/osx-release-1.1.0.tgz", + "integrity": "sha1-8heRGigTaUmvG/kwiyQeJzfTzWw=", + "requires": { + "minimist": "^1.1.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npm.taobao.org/minimist/download/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/p-finally/download/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-limit": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/p-limit/download/p-limit-2.0.0.tgz", + "integrity": "sha1-5iTtVO6MRgp3izyfNnBJb/ileuw=", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/p-locate/download/p-locate-3.0.0.tgz", + "integrity": "sha1-Mi1poFwCZLJZl9n0DNiokasAZKQ=", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/p-try/download/p-try-2.0.0.tgz", + "integrity": "sha1-hQgLuHxkaI+keZb+j3376CEXYLE=" + }, + "parseurl": { + "version": "1.3.2", + "resolved": "http://registry.npm.taobao.org/parseurl/download/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/path-exists/download/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/path-is-absolute/download/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "http://registry.npm.taobao.org/path-key/download/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "path-parse": { + "version": "1.0.5", + "resolved": "http://registry.npm.taobao.org/path-parse/download/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "http://registry.npm.taobao.org/path-to-regexp/download/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "pause-stream": { + "version": "0.0.11", + "resolved": "http://registry.npm.taobao.org/pause-stream/download/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "requires": { + "through": "~2.3" + } + }, + "pend": { + "version": "1.2.0", + "resolved": "http://registry.npm.taobao.org/pend/download/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "http://registry.npm.taobao.org/performance-now/download/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "platform": { + "version": "1.3.5", + "resolved": "http://registry.npm.taobao.org/platform/download/platform-1.3.5.tgz", + "integrity": "sha1-+2lYxpbgfikY0u7aDwvJRI1zNEQ=" + }, + "pomelo-protobuf": { + "version": "0.4.0", + "resolved": "http://registry.npm.taobao.org/pomelo-protobuf/download/pomelo-protobuf-0.4.0.tgz", + "integrity": "sha1-5F6aCkRusYZn4MbhPutT1Hrdvag=" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "http://registry.npm.taobao.org/prelude-ls/download/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/process-nextick-args/download/process-nextick-args-2.0.0.tgz", + "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=" + }, + "promise": { + "version": "7.3.1", + "resolved": "http://registry.npm.taobao.org/promise/download/promise-7.3.1.tgz", + "integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=", + "requires": { + "asap": "~2.0.3" + } + }, + "protobufjs": { + "version": "4.1.3", + "resolved": "http://registry.npm.taobao.org/protobufjs/download/protobufjs-4.1.3.tgz", + "integrity": "sha1-jjbRsCJsu2jWR+S0TCoUTzfyd54=", + "requires": { + "ascli": "~1", + "bytebuffer": "~4 >=4.1", + "glob": "^5.0.10", + "yargs": "^3.10.0" + }, + "dependencies": { + "yargs": { + "version": "3.32.0", + "resolved": "http://registry.npm.taobao.org/yargs/download/yargs-3.32.0.tgz", + "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", + "requires": { + "camelcase": "^2.0.1", + "cliui": "^3.0.3", + "decamelize": "^1.1.1", + "os-locale": "^1.4.0", + "string-width": "^1.0.1", + "window-size": "^0.1.4", + "y18n": "^3.2.0" + } + } + } + }, + "proxy-addr": { + "version": "2.0.3", + "resolved": "http://registry.npm.taobao.org/proxy-addr/download/proxy-addr-2.0.3.tgz", + "integrity": "sha1-NV8mJQWmIWRrMTCnKOtkfiIFU0E=", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.6.0" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "http://registry.npm.taobao.org/pseudomap/download/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "pug": { + "version": "2.0.1", + "resolved": "http://registry.npm.taobao.org/pug/download/pug-2.0.1.tgz", + "integrity": "sha1-J8FRYStT1ymr6OgoWqxryJNFtdA=", + "requires": { + "pug-code-gen": "^2.0.1", + "pug-filters": "^3.0.1", + "pug-lexer": "^4.0.0", + "pug-linker": "^3.0.5", + "pug-load": "^2.0.11", + "pug-parser": "^5.0.0", + "pug-runtime": "^2.0.4", + "pug-strip-comments": "^1.0.3" + } + }, + "pug-attrs": { + "version": "2.0.3", + "resolved": "http://registry.npm.taobao.org/pug-attrs/download/pug-attrs-2.0.3.tgz", + "integrity": "sha1-owlflw5kFR972tlX7vVftdeQXRU=", + "requires": { + "constantinople": "^3.0.1", + "js-stringify": "^1.0.1", + "pug-runtime": "^2.0.4" + } + }, + "pug-code-gen": { + "version": "2.0.1", + "resolved": "http://registry.npm.taobao.org/pug-code-gen/download/pug-code-gen-2.0.1.tgz", + "integrity": "sha1-CVHsgyJddNjPxHan+Zolm199BQw=", + "requires": { + "constantinople": "^3.0.1", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.1", + "pug-attrs": "^2.0.3", + "pug-error": "^1.3.2", + "pug-runtime": "^2.0.4", + "void-elements": "^2.0.1", + "with": "^5.0.0" + } + }, + "pug-error": { + "version": "1.3.2", + "resolved": "http://registry.npm.taobao.org/pug-error/download/pug-error-1.3.2.tgz", + "integrity": "sha1-U659nSm7A89WRJOgJhCfVMR/XyY=" + }, + "pug-filters": { + "version": "3.0.1", + "resolved": "http://registry.npm.taobao.org/pug-filters/download/pug-filters-3.0.1.tgz", + "integrity": "sha1-Fj73O/ux8VRNAysrQPRRMOtS3Ms=", + "requires": { + "clean-css": "^3.3.0", + "constantinople": "^3.0.1", + "jstransformer": "1.0.0", + "pug-error": "^1.3.2", + "pug-walk": "^1.1.7", + "resolve": "^1.1.6", + "uglify-js": "^2.6.1" + } + }, + "pug-lexer": { + "version": "4.0.0", + "resolved": "http://registry.npm.taobao.org/pug-lexer/download/pug-lexer-4.0.0.tgz", + "integrity": "sha1-IQwYRX7y4XYCQnQMXmR715TOwng=", + "requires": { + "character-parser": "^2.1.1", + "is-expression": "^3.0.0", + "pug-error": "^1.3.2" + } + }, + "pug-linker": { + "version": "3.0.5", + "resolved": "http://registry.npm.taobao.org/pug-linker/download/pug-linker-3.0.5.tgz", + "integrity": "sha1-npp65ABWgtAn3uuWsAD4juuDoC8=", + "requires": { + "pug-error": "^1.3.2", + "pug-walk": "^1.1.7" + } + }, + "pug-load": { + "version": "2.0.11", + "resolved": "http://registry.npm.taobao.org/pug-load/download/pug-load-2.0.11.tgz", + "integrity": "sha1-5kjlftET/iwfRdV4WOorrWvAFSc=", + "requires": { + "object-assign": "^4.1.0", + "pug-walk": "^1.1.7" + } + }, + "pug-parser": { + "version": "5.0.0", + "resolved": "http://registry.npm.taobao.org/pug-parser/download/pug-parser-5.0.0.tgz", + "integrity": "sha1-45Stmz/KkxI5QK/4hcBuRKt+aOQ=", + "requires": { + "pug-error": "^1.3.2", + "token-stream": "0.0.1" + } + }, + "pug-runtime": { + "version": "2.0.4", + "resolved": "http://registry.npm.taobao.org/pug-runtime/download/pug-runtime-2.0.4.tgz", + "integrity": "sha1-4XjhvaaKsujArPybztLFT9iM61g=" + }, + "pug-strip-comments": { + "version": "1.0.3", + "resolved": "http://registry.npm.taobao.org/pug-strip-comments/download/pug-strip-comments-1.0.3.tgz", + "integrity": "sha1-8VWVkiBu3G+FMQ2s9K+0igJa9Z8=", + "requires": { + "pug-error": "^1.3.2" + } + }, + "pug-walk": { + "version": "1.1.7", + "resolved": "http://registry.npm.taobao.org/pug-walk/download/pug-walk-1.1.7.tgz", + "integrity": "sha1-wA1cUSi6xYBr7BXSt+fNq+QlMfM=" + }, + "punycode": { + "version": "1.3.2", + "resolved": "http://registry.npm.taobao.org/punycode/download/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "qcloudapi-sdk": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/qcloudapi-sdk/-/qcloudapi-sdk-0.2.0.tgz", + "integrity": "sha512-Hy6a+95zDheaeMbA/N/7AEuAoOhwjR1POPI9xCwfsPc3b24ZzyH1M4PKgO9sq7Rq62+14ZTessHMdXJ+6NaeQg==", + "requires": { + "dot-qs": "^0.2.0", + "object-assign": "^3.0.0", + "request": ">= 2.33.0" + }, + "dependencies": { + "object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=" + } + } + }, + "qiniu": { + "version": "7.1.3", + "resolved": "http://registry.npm.taobao.org/qiniu/download/qiniu-7.1.3.tgz", + "integrity": "sha1-Zk2FDSvmndSMEldV2O1XTUh/kzI=", + "requires": { + "agentkeepalive": "3.3.0", + "crc32": "0.2.2", + "encodeurl": "^1.0.1", + "formstream": "1.1.0", + "mime": "1.3.6", + "tunnel-agent": "0.6.0", + "urllib": "2.22.0" + }, + "dependencies": { + "mime": { + "version": "1.3.6", + "resolved": "http://registry.npm.taobao.org/mime/download/mime-1.3.6.tgz", + "integrity": "sha1-WR2E02U6awtKO5343lqoEI5y5eA=" + } + } + }, + "qs": { + "version": "6.5.1", + "resolved": "http://registry.npm.taobao.org/qs/download/qs-6.5.1.tgz", + "integrity": "sha1-NJzfbu+J7EXBLX1es/wMhwNDptg=" + }, + "querystring": { + "version": "0.2.0", + "resolved": "http://registry.npm.taobao.org/querystring/download/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "rand-token": { + "version": "0.4.0", + "resolved": "http://registry.npm.taobao.org/rand-token/download/rand-token-0.4.0.tgz", + "integrity": "sha1-GlZbatEtkt1LMMTE5ZReiqJKWzs=" + }, + "range-parser": { + "version": "1.2.0", + "resolved": "http://registry.npm.taobao.org/range-parser/download/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raw-body": { + "version": "2.3.2", + "resolved": "http://registry.npm.taobao.org/raw-body/download/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "2.3.5", + "resolved": "http://registry.npm.taobao.org/readable-stream/download/readable-stream-2.3.5.tgz", + "integrity": "sha1-tPhQA6k4y7bsvOKhJPsQEr0ag40=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" + } + }, + "recursive-readdir": { + "version": "2.2.2", + "resolved": "http://registry.npm.taobao.org/recursive-readdir/download/recursive-readdir-2.2.2.tgz", + "integrity": "sha1-mUb7MnThYo3m42svZxSVO0hFCU8=", + "requires": { + "minimatch": "3.0.4" + } + }, + "redeyed": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/redeyed/download/redeyed-1.0.1.tgz", + "integrity": "sha1-6WwZO0DAgWsArshCaY5hGF5VSYo=", + "requires": { + "esprima": "~3.0.0" + }, + "dependencies": { + "esprima": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/esprima/download/esprima-3.0.0.tgz", + "integrity": "sha1-U88kes2ncxPlUcOqLnM0LT+099k=" + } + } + }, + "redis": { + "version": "2.8.0", + "resolved": "http://registry.npm.taobao.org/redis/download/redis-2.8.0.tgz", + "integrity": "sha1-ICKI4/WMSfYHnZevehDhMDrhSwI=", + "requires": { + "double-ended-queue": "^2.1.0-0", + "redis-commands": "^1.2.0", + "redis-parser": "^2.6.0" + } + }, + "redis-commands": { + "version": "1.3.5", + "resolved": "http://registry.npm.taobao.org/redis-commands/download/redis-commands-1.3.5.tgz", + "integrity": "sha1-RJWIlBTx6IYmEYCxRC5ylWAtg6I=" + }, + "redis-parser": { + "version": "2.6.0", + "resolved": "http://registry.npm.taobao.org/redis-parser/download/redis-parser-2.6.0.tgz", + "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=" + }, + "referrer-policy": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/referrer-policy/download/referrer-policy-1.1.0.tgz", + "integrity": "sha1-NXdOtzW/UPtsB46DM0tHI1AgfXk=" + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "http://registry.npm.taobao.org/regenerator-runtime/download/regenerator-runtime-0.11.1.tgz", + "integrity": "sha1-vgWtf5v30i4Fb5cmzuUBf78Z4uk=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "http://registry.npm.taobao.org/repeat-string/download/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "request": { + "version": "2.85.0", + "resolved": "http://registry.npm.taobao.org/request/download/request-2.85.0.tgz", + "integrity": "sha1-WgNhWkfGFCCz65m326IE+DYD4fo=", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.1", + "forever-agent": "~0.6.1", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "hawk": "~6.0.2", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "stringstream": "~0.0.5", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "http://registry.npm.taobao.org/require-directory/download/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/require-main-filename/download/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "resolve": { + "version": "1.5.0", + "resolved": "http://registry.npm.taobao.org/resolve/download/resolve-1.5.0.tgz", + "integrity": "sha1-HwmsznlsmnYlefMbLBzEw83fnzY=", + "requires": { + "path-parse": "^1.0.5" + } + }, + "retry-as-promised": { + "version": "2.3.2", + "resolved": "http://registry.npm.taobao.org/retry-as-promised/download/retry-as-promised-2.3.2.tgz", + "integrity": "sha1-zZdO5P2bX+A8vzGHHuSCIcB3N7c=", + "requires": { + "bluebird": "^3.4.6", + "debug": "^2.6.9" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "http://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "rfdc": { + "version": "1.1.2", + "resolved": "http://registry.npm.taobao.org/rfdc/download/rfdc-1.1.2.tgz", + "integrity": "sha1-5uctdPXcOd6PU49l4Aw2wYAY40k=" + }, + "right-align": { + "version": "0.1.3", + "resolved": "http://registry.npm.taobao.org/right-align/download/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "requires": { + "align-text": "^0.1.1" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "http://registry.npm.taobao.org/safe-buffer/download/safe-buffer-5.1.1.tgz", + "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=" + }, + "safefs": { + "version": "3.2.2", + "resolved": "http://registry.npm.taobao.org/safefs/download/safefs-3.2.2.tgz", + "integrity": "sha1-gXDBRE1wOOCMrqBaN0+uL6NJ4Vw=", + "requires": { + "graceful-fs": "*" + } + }, + "sax": { + "version": "0.6.1", + "resolved": "http://registry.npm.taobao.org/sax/download/sax-0.6.1.tgz", + "integrity": "sha1-VjsZx8HeiS4Jv8Ty/DDjwn8JUrk=" + }, + "scandirectory": { + "version": "2.5.0", + "resolved": "http://registry.npm.taobao.org/scandirectory/download/scandirectory-2.5.0.tgz", + "integrity": "sha1-bOA/VKCQtmjjy+2/IO354xBZPnI=", + "requires": { + "ignorefs": "^1.0.0", + "safefs": "^3.1.2", + "taskgroup": "^4.0.5" + } + }, + "semver": { + "version": "5.5.0", + "resolved": "http://registry.npm.taobao.org/semver/download/semver-5.5.0.tgz", + "integrity": "sha1-3Eu8emyp2Rbe5dQ1FvAJK1j3uKs=" + }, + "send": { + "version": "0.16.2", + "resolved": "http://registry.npm.taobao.org/send/download/send-0.16.2.tgz", + "integrity": "sha1-bsyh4PjBVtFBWXVZhI32RzCmu8E=", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "http://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "requires": { + "ms": "2.0.0" + } + }, + "mime": { + "version": "1.4.1", + "resolved": "http://registry.npm.taobao.org/mime/download/mime-1.4.1.tgz", + "integrity": "sha1-Eh+evEnjdm8xGnbh+hyAA8SwOqY=" + } + } + }, + "seq-queue": { + "version": "0.0.5", + "resolved": "http://registry.npm.taobao.org/seq-queue/download/seq-queue-0.0.5.tgz", + "integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=" + }, + "sequelize": { + "version": "4.37.1", + "resolved": "http://registry.npm.taobao.org/sequelize/download/sequelize-4.37.1.tgz", + "integrity": "sha1-F6qX8mm3YhAVxz53qmshNPRdp9c=", + "requires": { + "bluebird": "^3.5.0", + "cls-bluebird": "^2.1.0", + "debug": "^3.1.0", + "depd": "^1.1.0", + "dottie": "^2.0.0", + "generic-pool": "^3.4.0", + "inflection": "1.12.0", + "lodash": "^4.17.1", + "moment": "^2.20.0", + "moment-timezone": "^0.5.14", + "retry-as-promised": "^2.3.2", + "semver": "^5.5.0", + "terraformer-wkt-parser": "^1.1.2", + "toposort-class": "^1.0.1", + "uuid": "^3.2.1", + "validator": "^9.4.1", + "wkx": "^0.4.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "http://registry.npm.taobao.org/debug/download/debug-3.1.0.tgz", + "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "requires": { + "ms": "2.0.0" + } + }, + "inflection": { + "version": "1.12.0", + "resolved": "http://registry.npm.taobao.org/inflection/download/inflection-1.12.0.tgz", + "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" + }, + "uuid": { + "version": "3.2.1", + "resolved": "http://registry.npm.taobao.org/uuid/download/uuid-3.2.1.tgz", + "integrity": "sha1-EsUou51Y0LkmXZovbw/ovhf/HxQ=" + }, + "validator": { + "version": "9.4.1", + "resolved": "http://registry.npm.taobao.org/validator/download/validator-9.4.1.tgz", + "integrity": "sha1-q/Rm05i1Yc0kMFARLG/x3mzBJmM=" + } + } + }, + "serve-favicon": { + "version": "2.4.5", + "resolved": "http://registry.npm.taobao.org/serve-favicon/download/serve-favicon-2.4.5.tgz", + "integrity": "sha1-SdmkaGMVOpJAaRyJPSsOfYXW1DY=", + "requires": { + "etag": "~1.8.1", + "fresh": "0.5.2", + "ms": "2.0.0", + "parseurl": "~1.3.2", + "safe-buffer": "5.1.1" + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "http://registry.npm.taobao.org/serve-static/download/serve-static-1.13.2.tgz", + "integrity": "sha1-CV6Ecv1bRiN9tQzkhqQ/S4bGzsE=", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/set-blocking/download/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "http://registry.npm.taobao.org/setprototypeof/download/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "http://registry.npm.taobao.org/shebang-command/download/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/shebang-regex/download/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "shimmer": { + "version": "1.2.0", + "resolved": "http://registry.npm.taobao.org/shimmer/download/shimmer-1.2.0.tgz", + "integrity": "sha1-+Wb3VVeJdj502IQRk2haXnhzZmU=" + }, + "should": { + "version": "11.2.1", + "resolved": "http://registry.npm.taobao.org/should/download/should-11.2.1.tgz", + "integrity": "sha1-kPVRRVUtAc/CAGZuToGKHJZw7aI=", + "dev": true, + "requires": { + "should-equal": "^1.0.0", + "should-format": "^3.0.2", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "should-equal": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/should-equal/download/should-equal-1.0.1.tgz", + "integrity": "sha1-C26VFvJgGp+wuy3MNpr6HH4gCvc=", + "dev": true, + "requires": { + "should-type": "^1.0.0" + } + }, + "should-format": { + "version": "3.0.3", + "resolved": "http://registry.npm.taobao.org/should-format/download/should-format-3.0.3.tgz", + "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "should-type": { + "version": "1.4.0", + "resolved": "http://registry.npm.taobao.org/should-type/download/should-type-1.4.0.tgz", + "integrity": "sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=", + "dev": true + }, + "should-type-adaptors": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/should-type-adaptors/download/should-type-adaptors-1.1.0.tgz", + "integrity": "sha1-QB5/M7VTMDOUTVzYvytlAneS4no=", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "should-util": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/should-util/download/should-util-1.0.0.tgz", + "integrity": "sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "http://registry.npm.taobao.org/signal-exit/download/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "slash": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/slash/download/slash-2.0.0.tgz", + "integrity": "sha1-3lUoUaF1nfOo8gZTVEL17E3eq0Q=" + }, + "sntp": { + "version": "2.1.0", + "resolved": "http://registry.npm.taobao.org/sntp/download/sntp-2.1.0.tgz", + "integrity": "sha1-LGzsFP7cIiJznK+bXD2F0cxaLMg=", + "requires": { + "hoek": "4.x.x" + } + }, + "sprintf-js": { + "version": "1.1.1", + "resolved": "http://registry.npm.taobao.org/sprintf-js/download/sprintf-js-1.1.1.tgz", + "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=" + }, + "sqlstring": { + "version": "2.3.1", + "resolved": "http://registry.npm.taobao.org/sqlstring/download/sqlstring-2.3.1.tgz", + "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=" + }, + "sshpk": { + "version": "1.14.1", + "resolved": "http://registry.npm.taobao.org/sshpk/download/sshpk-1.14.1.tgz", + "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" + } + }, + "statuses": { + "version": "1.4.0", + "resolved": "http://registry.npm.taobao.org/statuses/download/statuses-1.4.0.tgz", + "integrity": "sha1-u3PURtonlhBu/MG2AaJT1sRr0Ic=" + }, + "streamroller": { + "version": "0.7.0", + "resolved": "http://registry.npm.taobao.org/streamroller/download/streamroller-0.7.0.tgz", + "integrity": "sha1-odG3z4PTmvsNYwSaWsv5NJO99ks=", + "requires": { + "date-format": "^1.2.0", + "debug": "^3.1.0", + "mkdirp": "^0.5.1", + "readable-stream": "^2.3.0" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npm.taobao.org/mkdirp/download/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + } + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "http://registry.npm.taobao.org/string-width/download/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "http://registry.npm.taobao.org/string_decoder/download/string_decoder-1.0.3.tgz", + "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "http://registry.npm.taobao.org/stringstream/download/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npm.taobao.org/strip-ansi/download/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/strip-eof/download/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "superagent": { + "version": "3.8.2", + "resolved": "http://registry.npm.taobao.org/superagent/download/superagent-3.8.2.tgz", + "integrity": "sha1-5KEbnQR/fT7+s7vlNtnsACHRZAM=", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.1.1", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.0.5" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "http://registry.npm.taobao.org/debug/download/debug-3.1.0.tgz", + "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "http://registry.npm.taobao.org/mime/download/mime-1.6.0.tgz", + "integrity": "sha1-Ms2eXGRVO9WNGaVor0Uqz/BJgbE=", + "dev": true + } + } + }, + "supertest": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/supertest/download/supertest-3.0.0.tgz", + "integrity": "sha1-jUu2j9GDDuBwM7HFpamkAhyWUpY=", + "dev": true, + "requires": { + "methods": "~1.1.2", + "superagent": "^3.0.0" + } + }, + "supervisor": { + "version": "0.12.0", + "resolved": "http://registry.npm.taobao.org/supervisor/download/supervisor-0.12.0.tgz", + "integrity": "sha1-3n5jNwFbKRhRwQ81OMSn8EkX7ME=", + "dev": true + }, + "taskgroup": { + "version": "4.3.1", + "resolved": "http://registry.npm.taobao.org/taskgroup/download/taskgroup-4.3.1.tgz", + "integrity": "sha1-feGT/r12gnPEV3MElwJNUSwnkVo=", + "requires": { + "ambi": "^2.2.0", + "csextends": "^1.0.3" + } + }, + "terraformer": { + "version": "1.0.8", + "resolved": "http://registry.npm.taobao.org/terraformer/download/terraformer-1.0.8.tgz", + "integrity": "sha1-UeCtiXRvzyFh3G9lqnDkI3fItZM=", + "requires": { + "@types/geojson": "^1.0.0" + } + }, + "terraformer-wkt-parser": { + "version": "1.1.2", + "resolved": "http://registry.npm.taobao.org/terraformer-wkt-parser/download/terraformer-wkt-parser-1.1.2.tgz", + "integrity": "sha1-M2oMj8gglKWv+DKI9prt7NNpvww=", + "requires": { + "terraformer": "~1.0.5" + } + }, + "through": { + "version": "2.3.8", + "resolved": "http://registry.npm.taobao.org/through/download/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "http://registry.npm.taobao.org/to-fast-properties/download/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" + }, + "token-stream": { + "version": "0.0.1", + "resolved": "http://registry.npm.taobao.org/token-stream/download/token-stream-0.0.1.tgz", + "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=" + }, + "toposort-class": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/toposort-class/download/toposort-class-1.0.1.tgz", + "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=" + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "http://registry.npm.taobao.org/tough-cookie/download/tough-cookie-2.3.4.tgz", + "integrity": "sha1-7GDO44rGdQY//JelwYlwV47oNlU=", + "requires": { + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "http://registry.npm.taobao.org/punycode/download/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "http://registry.npm.taobao.org/tunnel-agent/download/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "http://registry.npm.taobao.org/tweetnacl/download/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "http://registry.npm.taobao.org/type-check/download/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-is": { + "version": "1.6.16", + "resolved": "http://registry.npm.taobao.org/type-is/download/type-is-1.6.16.tgz", + "integrity": "sha1-+JzjQVQcZysl7nrjxz3uOyvlAZQ=", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } + }, + "typechecker": { + "version": "2.1.0", + "resolved": "http://registry.npm.taobao.org/typechecker/download/typechecker-2.1.0.tgz", + "integrity": "sha1-0cIJOlT/ihn1jP+HfuqlTyJC04M=" + }, + "typedarray": { + "version": "0.0.6", + "resolved": "http://registry.npm.taobao.org/typedarray/download/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "uc.micro": { + "version": "1.0.5", + "resolved": "http://registry.npm.taobao.org/uc.micro/download/uc.micro-1.0.5.tgz", + "integrity": "sha1-DGXxX4FaoItWCmHOi023/8P0U3Y=" + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "http://registry.npm.taobao.org/uglify-js/download/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "camelcase": { + "version": "1.2.1", + "resolved": "http://registry.npm.taobao.org/camelcase/download/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + }, + "cliui": { + "version": "2.1.0", + "resolved": "http://registry.npm.taobao.org/cliui/download/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "http://registry.npm.taobao.org/source-map/download/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "window-size": { + "version": "0.1.0", + "resolved": "http://registry.npm.taobao.org/window-size/download/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "http://registry.npm.taobao.org/wordwrap/download/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + }, + "yargs": { + "version": "3.10.0", + "resolved": "http://registry.npm.taobao.org/yargs/download/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "http://registry.npm.taobao.org/uglify-to-browserify/download/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "optional": true + }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "requires": { + "crypto-random-string": "^1.0.0" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "http://registry.npm.taobao.org/universalify/download/universalify-0.1.2.tgz", + "integrity": "sha1-tkb2m+OULavOzJ1mOcgNwQXvqmY=" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/unpipe/download/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "upyun": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/upyun/-/upyun-3.3.9.tgz", + "integrity": "sha512-UqvSKvvFgbQwLI+yjTO0WUnmS2i2KIvX4N2Je6ZTRN4cja17DHX7ARd13DO6h65/JOQJPs/Nua2zlnixNFIdWA==", + "requires": { + "axios": "^0.18.0", + "base-64": "^0.1.0", + "form-data": "^2.1.4", + "hmacsha1": "^1.0.0", + "is-promise": "^2.1.0", + "md5": "^2.2.1", + "mime-types": "^2.1.15" + } + }, + "url": { + "version": "0.10.3", + "resolved": "http://registry.npm.taobao.org/url/download/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "urllib": { + "version": "2.22.0", + "resolved": "http://registry.npm.taobao.org/urllib/download/urllib-2.22.0.tgz", + "integrity": "sha1-KWXcSuEnpvtpW32yfTGE8X2Cy0I=", + "requires": { + "any-promise": "^1.3.0", + "content-type": "^1.0.2", + "debug": "^2.6.0", + "default-user-agent": "^1.0.0", + "digest-header": "^0.0.1", + "ee-first": "~1.1.1", + "humanize-ms": "^1.2.0", + "iconv-lite": "^0.4.15", + "qs": "^6.4.0", + "statuses": "^1.3.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "http://registry.npm.taobao.org/debug/download/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "http://registry.npm.taobao.org/util-deprecate/download/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utility": { + "version": "0.1.11", + "resolved": "http://registry.npm.taobao.org/utility/download/utility-0.1.11.tgz", + "integrity": "sha1-/eYM+bTkdRlHoM9dEEzik2ciZxU=", + "requires": { + "address": ">=0.0.1" + } + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/utils-merge/download/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "3.1.0", + "resolved": "http://registry.npm.taobao.org/uuid/download/uuid-3.1.0.tgz", + "integrity": "sha1-PdPT55Crwk17DToDT/q6vijrvAQ=" + }, + "validator": { + "version": "10.6.0", + "resolved": "http://registry.npm.taobao.org/validator/download/validator-10.6.0.tgz", + "integrity": "sha1-qb3OaFs8PoSA5+u7nrlcVM2XM7A=" + }, + "vary": { + "version": "1.1.2", + "resolved": "http://registry.npm.taobao.org/vary/download/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "verror": { + "version": "1.10.0", + "resolved": "http://registry.npm.taobao.org/verror/download/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "void-elements": { + "version": "2.0.1", + "resolved": "http://registry.npm.taobao.org/void-elements/download/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + }, + "watchr": { + "version": "2.4.13", + "resolved": "http://registry.npm.taobao.org/watchr/download/watchr-2.4.13.tgz", + "integrity": "sha1-10hHu01vkPYf4sdPn2hmKqDgdgE=", + "requires": { + "eachr": "^2.0.2", + "extendr": "^2.1.0", + "extract-opts": "^2.2.0", + "ignorefs": "^1.0.0", + "safefs": "^3.1.2", + "scandirectory": "^2.5.0", + "taskgroup": "^4.2.0", + "typechecker": "^2.0.8" + } + }, + "which": { + "version": "1.3.0", + "resolved": "http://registry.npm.taobao.org/which/download/which-1.3.0.tgz", + "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/which-module/download/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "win-release": { + "version": "1.1.1", + "resolved": "http://registry.npm.taobao.org/win-release/download/win-release-1.1.1.tgz", + "integrity": "sha1-X6VeAr58qTTt/BJmVjLoSbcuUgk=", + "requires": { + "semver": "^5.0.1" + } + }, + "window-size": { + "version": "0.1.4", + "resolved": "http://registry.npm.taobao.org/window-size/download/window-size-0.1.4.tgz", + "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=" + }, + "with": { + "version": "5.1.1", + "resolved": "http://registry.npm.taobao.org/with/download/with-5.1.1.tgz", + "integrity": "sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4=", + "requires": { + "acorn": "^3.1.0", + "acorn-globals": "^3.0.0" + } + }, + "wkx": { + "version": "0.4.4", + "resolved": "http://registry.npm.taobao.org/wkx/download/wkx-0.4.4.tgz", + "integrity": "sha1-z3UbZy5LReFi+f0wEkh45z2WybI=", + "requires": { + "@types/node": "*" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/wordwrap/download/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "http://registry.npm.taobao.org/wrap-ansi/download/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "http://registry.npm.taobao.org/wrappy/download/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write-file-atomic": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", + "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "x-xss-protection": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/x-xss-protection/download/x-xss-protection-1.1.0.tgz", + "integrity": "sha1-TxiYwzLesefyvhKA77PixT1pwac=" + }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=" + }, + "xml2js": { + "version": "0.4.4", + "resolved": "http://registry.npm.taobao.org/xml2js/download/xml2js-0.4.4.tgz", + "integrity": "sha1-MREBAAMAiuGSQOuhdJe1fHKcVV0=", + "requires": { + "sax": "0.6.x", + "xmlbuilder": ">=1.0.0" + } + }, + "xmlbuilder": { + "version": "2.6.5", + "resolved": "http://registry.npm.taobao.org/xmlbuilder/download/xmlbuilder-2.6.5.tgz", + "integrity": "sha1-b/etYPty0idk8AehZLd/K/FABSY=", + "requires": { + "lodash": "^3.5.0" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "http://registry.npm.taobao.org/lodash/download/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + } + } + }, + "xregexp": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/xregexp/download/xregexp-2.0.0.tgz", + "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "http://registry.npm.taobao.org/xtend/download/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "y18n": { + "version": "3.2.1", + "resolved": "http://registry.npm.taobao.org/y18n/download/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yallist": { + "version": "2.1.2", + "resolved": "http://registry.npm.taobao.org/yallist/download/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "yargs": { + "version": "12.0.1", + "resolved": "http://registry.npm.taobao.org/yargs/download/yargs-12.0.1.tgz", + "integrity": "sha1-ZDLlYSO7Tnw1YhFUAemDdAYCYcI=", + "requires": { + "cliui": "^4.0.0", + "decamelize": "^2.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^10.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/ansi-regex/download/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "cliui": { + "version": "4.1.0", + "resolved": "http://registry.npm.taobao.org/cliui/download/cliui-4.1.0.tgz", + "integrity": "sha1-NIQi2+gtgAswIu709qwQvy5NG0k=", + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "decamelize": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/decamelize/download/decamelize-2.0.0.tgz", + "integrity": "sha1-ZW17vICUxMeI6lPFhAkIycfQY8c=", + "requires": { + "xregexp": "4.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/is-fullwidth-code-point/download/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "os-locale": { + "version": "2.1.0", + "resolved": "http://registry.npm.taobao.org/os-locale/download/os-locale-2.1.0.tgz", + "integrity": "sha1-QrwpAKa1uL0XN2yOiCtlr8zyS/I=", + "requires": { + "execa": "^0.7.0", + "lcid": "^1.0.0", + "mem": "^1.1.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "http://registry.npm.taobao.org/string-width/download/string-width-2.1.1.tgz", + "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "http://registry.npm.taobao.org/strip-ansi/download/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "xregexp": { + "version": "4.0.0", + "resolved": "http://registry.npm.taobao.org/xregexp/download/xregexp-4.0.0.tgz", + "integrity": "sha1-5pgYneSd0qGMxWh7BeF8jkOUMCA=" + } + } + }, + "yargs-parser": { + "version": "10.1.0", + "resolved": "http://registry.npm.taobao.org/yargs-parser/download/yargs-parser-10.1.0.tgz", + "integrity": "sha1-cgImW4n36eny5XZeD+c1qQXtuqg=", + "requires": { + "camelcase": "^4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "http://registry.npm.taobao.org/camelcase/download/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + } + } + }, + "yauzl": { + "version": "2.4.1", + "resolved": "http://registry.npm.taobao.org/yauzl/download/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "requires": { + "fd-slicer": "~1.0.1" + } + }, + "yazl": { + "version": "2.4.3", + "resolved": "http://registry.npm.taobao.org/yazl/download/yazl-2.4.3.tgz", + "integrity": "sha1-7CblzIfVYBud+EMtvdPNLlFzoHE=", + "requires": { + "buffer-crc32": "~0.2.3" + } + } + } +} diff --git a/package.json b/package.json index d1b0605d..1e3fc19a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-push-server", "description": "CodePush service is hotupdate services which adapter react-native-code-push and cordova-plugin-code-push", - "version": "0.2.19", + "version": "0.5.4", "license": "MIT", "repository": { "type": "git", @@ -34,43 +34,47 @@ "init": "node ./bin/db init", "upgrade": "node ./bin/db upgrade", "test": "make test", - "test-win": "mocha test/api/init test/api/users test/api/auth test/api/account test/api/accessKeys test/api/sessions test/api/apps test/api/index --recursive --timeout 15000", + "test-win": "mocha test/api/init test/api/users test/api/auth test/api/account test/api/accessKeys test/api/apps test/api/index --recursive --timeout 15000", "coverage": "make coverage" }, "dependencies": { "aliyun-oss-upload-stream": "^1.3.0", - "aliyun-sdk": "^1.9.17", + "aliyun-sdk": "^1.11.10", "aws-sdk": "^2.7.0", "bcryptjs": "^2.3.0", "bluebird": "^3.4.1", "body-parser": "^1.15.2", "cookie-parser": "^1.4.3", - "debug": "^2.3.3", + "cos-nodejs-sdk-v5": "^2.4.10", + "debug": "^3.1.0", + "diff-match-patch": "^1.0.1", "express": "^4.14.0", "extract-zip": "^1.6.0", - "formidable": "^1.0.17", - "fs-extra": "^2.0.0", + "formidable": "^1.2.1", + "fs-extra": "^7.0.0", "helmet": "^3.1.0", "i18n": "^0.8.3", - "jsonwebtoken": "^7.1.7", - "lodash": "^4.5.1", - "log4js": "^1.1.0", + "jschardet": "^1.6.0", + "jsonwebtoken": "^8.2.0", + "lodash": "^4.17.5", + "log4js": "^3.0.5", "markdown-it": "^8.0.1", "moment": "^2.14.1", "morgan": "^1.7.0", - "mysql": "^2.10.2", - "nodemailer": "^3.1.3", - "pug": "^2.0.0-beta6", - "qiniu": "^6.1.10", - "rand-token": "^0.3.0", - "recursive-fs": "^1.0.0", + "mysql2": "^1.3.5", + "nodemailer": "^4.0.1", + "pug": "^2.0.1", + "qiniu": "^7.1.3", + "upyun": "^3.3.9", + "rand-token": "^0.4.0", + "recursive-readdir": "^2.1.1", "redis": "^2.6.2", "request": "^2.72.0", - "sequelize": "^3.24.5", + "sequelize": "^4.37.1", "serve-favicon": "^2.4.0", - "slash": "^1.0.0", - "validator": "^7.0.0", - "yargs": "^6.2.0", + "slash": "^2.0.0", + "validator": "^10.6.0", + "yargs": "^12.0.1", "yazl": "^2.3.0" }, "devDependencies": { @@ -85,6 +89,7 @@ "config", "core", "docs", + "docker", "models", "public", "routes", diff --git a/routes/accessKeys.js b/routes/accessKeys.js index 835ac8ff..b7239b41 100644 --- a/routes/accessKeys.js +++ b/routes/accessKeys.js @@ -19,12 +19,7 @@ router.get('/', middleware.checkToken, (req, res, next) => { res.send({accessKeys: accessKeys}); }) .catch((e) => { - if (e instanceof AppError.AppError) { - log.debug('request get acceesKeys AppError', e) - res.status(406).send(e.message); - } else { - next(e); - } + next(e); }); }); @@ -33,9 +28,9 @@ router.post('/', middleware.checkToken, (req, res, next) => { var identical = req.users.identical; var createdBy = _.trim(req.body.createdBy); var friendlyName = _.trim(req.body.friendlyName); - var isSession = req.body.isSession; var ttl = parseInt(req.body.ttl); var description = _.trim(req.body.description); + log.debug(req.body) var newAccessKey = security.randToken(28).concat(identical); return accountManager.isExsitAccessKeyName(uid, friendlyName) .then((data) => { @@ -44,7 +39,7 @@ router.post('/', middleware.checkToken, (req, res, next) => { } }) .then(() => { - return accountManager.createAccessKey(uid, newAccessKey, isSession, ttl, friendlyName, createdBy, description); + return accountManager.createAccessKey(uid, newAccessKey, ttl, friendlyName, createdBy, description); }) .then((newToken) => { var moment = require("moment"); @@ -53,14 +48,15 @@ router.post('/', middleware.checkToken, (req, res, next) => { createdTime : parseInt(moment(newToken.created_at).format('x')), createdBy : newToken.created_by, expires : parseInt(moment(newToken.expires_at).format('x')), - isSession: newToken.is_session == 1 ? true :false, description : newToken.description, friendlyName: newToken.name, }; + log.debug(info); res.send({accessKey:info}); }) .catch((e) => { if (e instanceof AppError.AppError) { + log.debug(e) res.status(406).send(e.message); } else { next(e); @@ -73,66 +69,16 @@ router.delete('/:name', middleware.checkToken, (req, res, next) => { var uid = req.users.id; return models.UserTokens.destroy({where: {name:name, uid: uid}}) .then((rowNum) => { + log.debug('delete acceesKey:', name) res.send({friendlyName:name}); }) .catch((e) => { if (e instanceof AppError.AppError) { + log.debug(e) res.status(406).send(e.message); } else { next(e); } }); }); - -router.patch('/:name', middleware.checkToken, (req, res, next) => { - var name = _.trim(decodeURI(req.params.name)); - var friendlyName = _.trim(req.body.friendlyName); - var ttl = _.trim(req.body.ttl); - var uid = req.users.id; - return Promise.all([ - models.UserTokens.findOne({where: {name:name, uid: uid}}), - accountManager.isExsitAccessKeyName(uid, friendlyName), - ]) - .spread((token, token2) => { - if (_.isEmpty(token)) { - throw new AppError.AppError(`The access key "${name}" does not exist.`); - } - if (!_.isEmpty(token2)) { - throw new AppError.AppError(`The access key "${friendlyName}" already exists.`); - } - return token; - }) - .then((token) => { - var moment = require('moment'); - if (ttl > 0 || ttl < 0) { - var newExp = moment(token.get('expires_at')).add(ttl/1000, 'seconds').format('YYYY-MM-DD HH:mm:ss'); - token.set('expires_at', newExp); - } - if (friendlyName.length > 0) { - token.set('name', friendlyName); - } - return token.save(); - }) - .then((token) => { - var info = { - name : '(hidden)', - isSession: token.is_session == 1 ? true :false, - createdTime : token.created_at, - createdBy : token.created_by, - description : token.description, - expires : token.expires_at, - friendlyName: token.name, - id: token.id + "" - }; - res.send({accessKey: info}); - }) - .catch((e) => { - if (e instanceof AppError.AppError) { - res.status(406).send(e.message); - } else { - next(e); - } - }); -}); - module.exports = router; diff --git a/routes/account.js b/routes/account.js index c022af4d..edae56f1 100644 --- a/routes/account.js +++ b/routes/account.js @@ -4,14 +4,16 @@ var models = require('../models'); var _ = require('lodash'); var security = require('../core/utils/security'); var middleware = require('../core/middleware'); +var log4js = require('log4js'); +var log = log4js.getLogger("cps:account"); router.get('/', middleware.checkToken, (req, res) => { var userInfo = { email:req.users.email, - id:req.users.identical, linkedProviders: [], name:req.users.username, }; + log.debug(userInfo); res.send({account:userInfo}); }); diff --git a/routes/apps.js b/routes/apps.js index 8f634bec..8fd7d832 100644 --- a/routes/apps.js +++ b/routes/apps.js @@ -15,13 +15,10 @@ var config = require('../core/config'); const REGEX = /^(\w+)(-android|-ios)$/; const REGEX_ANDROID = /^(\w+)(-android)$/; const REGEX_IOS = /^(\w+)(-ios)$/; -const OLD_REGEX_ANDROID = /^(android_)/; -const OLD_REGEX_IOS = /^(ios_)/; var log4js = require('log4js'); var log = log4js.getLogger("cps:apps"); -router.get('/', - middleware.checkToken, (req, res, next) => { +router.get('/', middleware.checkToken, (req, res, next) => { var uid = req.users.id; var appManager = new AppManager(); appManager.listApps(uid) @@ -73,6 +70,7 @@ router.get('/:appName/deployments/:deploymentName', throw new AppError.AppError("does not find the deployment"); } res.send({deployment: deployments.listDeloyment(deploymentInfo)}); + return true; }) .catch((e) => { if (e instanceof AppError.AppError) { @@ -173,7 +171,7 @@ router.get('/:appName/deployments/:deploymentName/history', return deployments.getDeploymentHistory(deploymentInfo.id); }) .then((rs) => { - res.send({history: rs}); + res.send({history: _.pullAll(rs, [null, false])}); }) .catch((e) => { if (e instanceof AppError.AppError) { @@ -269,17 +267,7 @@ router.post('/:appName/deployments/:deploymentName/release', var packageManager = new PackageManager(); accountManager.collaboratorCan(uid, appName) .then((col) => { - var pubType = ''; - log.debug(`check publish type`); - if (REGEX_ANDROID.test(appName)) { - pubType = 'android'; - } else if (REGEX_IOS.test(appName)) { - pubType = 'ios'; - } else { - log.debug(`you have to rename app name, eg. Demo-android Demo-ios`); - throw new AppError.AppError(`you have to rename app name, eg. Demo-android Demo-ios`); - } - log.debug(`publish type is ${pubType}`); + log.debug(col); return deployments.findDeloymentByName(deploymentName, col.appid) .then((deploymentInfo) => { if (_.isEmpty(deploymentInfo)) { @@ -288,18 +276,23 @@ router.post('/:appName/deployments/:deploymentName/release', } return packageManager.parseReqFile(req) .then((data) => { - return packageManager.releasePackage(deploymentInfo.id, data.packageInfo, data.package.type, data.package.path, uid, pubType) + if (data.package.type != "application/zip") { + log.debug(`upload file type is invlidate`, data.package); + throw new AppError.AppError("upload file type is invalidate"); + } + log.debug('packageInfo:', data.packageInfo); + return packageManager.releasePackage(deploymentInfo.appid, deploymentInfo.id, data.packageInfo, data.package.path, uid) .finally(() => { common.deleteFolderSync(data.package.path); }); }) .then((packages) => { if (packages) { - Promise.delay(2000) + Promise.delay(1000) .then(() => { - packageManager.createDiffPackagesByLastNums(packages.id, _.get(config, 'common.diffNums', 1)) + packageManager.createDiffPackagesByLastNums(deploymentInfo.appid, packages, _.get(config, 'common.diffNums', 1)) .catch((e) => { - console.error(e); + log.error(e); }); }); } @@ -316,8 +309,8 @@ router.post('/:appName/deployments/:deploymentName/release', }); }); }) - .then((data) => { - res.send(""); + .then(() => { + res.send('{"msg": "succeed"}'); }) .catch((e) => { if (e instanceof AppError.AppError) { @@ -330,12 +323,13 @@ router.post('/:appName/deployments/:deploymentName/release', router.patch('/:appName/deployments/:deploymentName/release', middleware.checkToken, (req, res, next) => { - return res.status(406).send('Not supported currently'); + log.debug('req.body', req.body); var appName = _.trim(req.params.appName); var deploymentName = _.trim(req.params.deploymentName); var uid = req.users.id; var deployments = new Deployments(); var packageManager = new PackageManager(); + var label = _.get(req, 'body.packageInfo.label'); accountManager.collaboratorCan(uid, appName) .then((col) => { return deployments.findDeloymentByName(deploymentName, col.appid) @@ -343,9 +337,35 @@ router.patch('/:appName/deployments/:deploymentName/release', if (_.isEmpty(deploymentInfo)) { throw new AppError.AppError("does not find the deployment"); } - var label = deploymentInfo.label; - var deploymentVersionId = deploymentInfo.last_deployment_version_id; - return packageManager.modifyReleasePackage(deploymentInfo.id, deploymentVersionId, _.get(req, 'body.packageInfo')); + if (label) { + return packageManager.findPackageInfoByDeploymentIdAndLabel(deploymentInfo.id, label) + .then((data)=>{ + return [deploymentInfo, data]; + }); + } else { + var deploymentVersionId = deploymentInfo.last_deployment_version_id; + return packageManager.findLatestPackageInfoByDeployVersion(deploymentVersionId) + .then((data)=>{ + return [deploymentInfo, data]; + });; + } + }) + .spread((deploymentInfo, packageInfo)=>{ + if (!packageInfo) { + throw new AppError.AppError("does not find the packageInfo"); + } + return packageManager.modifyReleasePackage(packageInfo.id, _.get(req, 'body.packageInfo')) + .then(()=>{ + //clear cache if exists. + if (_.get(config, 'common.updateCheckCache', false) !== false) { + Promise.delay(2500) + .then(() => { + var ClientManager = require('../core/services/client-manager'); + var clientManager = new ClientManager(); + clientManager.clearUpdateCheckCache(deploymentInfo.deployment_key, '*', '*', '*'); + }); + } + }); }); }).then((data) => { res.send(""); @@ -359,8 +379,10 @@ router.patch('/:appName/deployments/:deploymentName/release', }); }); + router.post('/:appName/deployments/:sourceDeploymentName/promote/:destDeploymentName', middleware.checkToken, (req, res, next) => { + log.debug('req.body:', req.body); var appName = _.trim(req.params.appName); var sourceDeploymentName = _.trim(req.params.sourceDeploymentName); var destDeploymentName = _.trim(req.params.destDeploymentName); @@ -381,6 +403,23 @@ router.post('/:appName/deployments/:sourceDeploymentName/promote/:destDeployment if (!destDeploymentInfo) { throw new AppError.AppError(`${destDeploymentName} does not exist.`); } + return [sourceDeploymentInfo, destDeploymentInfo]; + }) + .spread((sourceDeploymentInfo, destDeploymentInfo) => { + var params = _.get(req.body, 'packageInfo', {}); + _.set(params, 'promoteUid', uid); + return [packageManager.promotePackage(sourceDeploymentInfo, destDeploymentInfo, params),destDeploymentInfo]; + }) + .spread((packages, destDeploymentInfo) => { + if (packages) { + Promise.delay(1000) + .then(() => { + packageManager.createDiffPackagesByLastNums(destDeploymentInfo.appid, packages, _.get(config, 'common.diffNums', 1)) + .catch((e) => { + log.error(e); + }); + }); + } //clear cache if exists. if (_.get(config, 'common.updateCheckCache', false) !== false) { Promise.delay(2500) @@ -390,26 +429,11 @@ router.post('/:appName/deployments/:sourceDeploymentName/promote/:destDeployment clientManager.clearUpdateCheckCache(destDeploymentInfo.deployment_key, '*', '*', '*'); }); } - return [sourceDeploymentInfo.id, destDeploymentInfo.id]; + return packages; }) - .spread((sourceDeploymentId, destDeploymentId) => { - return packageManager.promotePackage(sourceDeploymentId, destDeploymentId, uid); - }); }) .then((packages) => { - if (packages) { - Promise.delay(2000) - .then(() => { - packageManager.createDiffPackagesByLastNums(packages.id, _.get(config, 'common.diffNums', 1)) - .catch((e) => { - console.log(e); - }); - }); - } - return null; - }) - .then(() => { - res.send('ok'); + res.send({package:packages}); }) .catch((e) => { if (e instanceof AppError.AppError) { @@ -432,16 +456,28 @@ var rollbackCb = function (req, res, next) { return deployments.findDeloymentByName(deploymentName, col.appid); }) .then((dep) => { - //clear cache if exists. - if (_.get(config, 'common.updateCheckCache', false) !== false) { - Promise.delay(2500) - .then(() => { - var ClientManager = require('../core/services/client-manager'); - var clientManager = new ClientManager(); - clientManager.clearUpdateCheckCache(dep.deployment_key, '*', '*', '*'); - }); - } - return packageManager.rollbackPackage(dep.last_deployment_version_id, targetLabel, uid); + return packageManager.rollbackPackage(dep.last_deployment_version_id, targetLabel, uid) + .then((packageInfo)=>{ + if (packageInfo) { + Promise.delay(1000) + .then(() => { + packageManager.createDiffPackagesByLastNums(dep.appid, packageInfo, 1) + .catch((e) => { + log.error(e); + }); + }); + } + //clear cache if exists. + if (_.get(config, 'common.updateCheckCache', false) !== false) { + Promise.delay(2500) + .then(() => { + var ClientManager = require('../core/services/client-manager'); + var clientManager = new ClientManager(); + clientManager.clearUpdateCheckCache(dep.deployment_key, '*', '*', '*'); + }); + } + return packageInfo; + }); }) .then(() => { res.send('ok'); @@ -583,17 +619,6 @@ router.patch('/:appName', var appManager = new AppManager(); return accountManager.ownerCan(uid, appName) .then((col) => { - if (REGEX_ANDROID.test(appName) || OLD_REGEX_ANDROID.test(appName)) { - if (!REGEX_ANDROID.test(newAppName)) { - throw new AppError.AppError(`new appName have to point -android suffix! eg. Demo-android`); - } - } else if (REGEX_IOS.test(appName) || OLD_REGEX_IOS.test(appName)) { - if (!REGEX_IOS.test(newAppName)) { - throw new AppError.AppError(`new appName have to point -ios suffix! eg. Demo-ios`); - } - } else { - throw new AppError.AppError(`appName have to point -android or -ios suffix! eg. ${appName}-android ${appName}-ios`); - } return appManager.findAppByName(uid, newAppName) .then((appInfo) => { if (!_.isEmpty(appInfo)){ @@ -647,21 +672,42 @@ router.post('/:appName/transfer/:email', }); router.post('/', middleware.checkToken, (req, res, next) => { + log.debug("addApp params:",req.body); + var constName = require('../core/const'); var appName = req.body.name; - var uid = req.users.id; - var appManager = new AppManager(); if (_.isEmpty(appName)) { return res.status(406).send("Please input name!"); } + var osName = _.toLower(req.body.os); + var os; + if (osName == _.toLower(constName.IOS_NAME)) { + os = constName.IOS; + } else if (osName == _.toLower(constName.ANDROID_NAME)) { + os = constName.ANDROID; + } else if (osName == _.toLower(constName.WINDOWS_NAME)) { + os = constName.WINDOWS; + } else { + return res.status(406).send("Please input os [iOS|Android|Windows]!"); + } + var platformName = _.toLower(req.body.platform); + var platform; + if (platformName == _.toLower(constName.REACT_NATIVE_NAME)) { + platform = constName.REACT_NATIVE; + } else if (platformName == _.toLower(constName.CORDOVA_NAME)) { + platform = constName.CORDOVA; + } else { + return res.status(406).send("Please input platform [React-Native|Cordova]!"); + } + var manuallyProvisionDeployments = req.body.manuallyProvisionDeployments; + var uid = req.users.id; + var appManager = new AppManager(); + appManager.findAppByName(uid, appName) .then((appInfo) => { if (!_.isEmpty(appInfo)){ throw new AppError.AppError(appName + " Exist!"); } - if (!REGEX.test(appName)) { - throw new AppError.AppError(`appName have to point -android or -ios suffix! eg. ${appName}-android ${appName}-ios`); - } - return appManager.addApp(uid, appName, req.users.identical) + return appManager.addApp(uid, appName, os, platform, req.users.identical) .then(() => { return {name: appName, collaborators: {[req.users.email]: {permission: "Owner"}}}; }); diff --git a/routes/auth.js b/routes/auth.js index e9e0ccea..7bdd96c2 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -1,21 +1,19 @@ var express = require('express'); var router = express.Router(); var _ = require('lodash'); -var security = require('../core/utils/security'); -var accountManager = require('../core/services/account-manager')(); -var AppError = require('../core/app-error'); +var config = require('../core/config'); +var validator = require('validator'); +var log4js = require('log4js'); +var log = log4js.getLogger("cps:auth"); + +router.get('/password', (req, res) => { + res.render('auth/password', { title: 'CodePushServer' }); +}); router.get('/login', (req, res) => { - var config = require('../core/config'); var codePushWebUrl = _.get(config, 'common.codePushWebUrl'); - var isRedirect = false; - if (codePushWebUrl) { - var validator = require('validator'); - if (validator.isURL(codePushWebUrl)){ - isRedirect = true; - } - } - if (isRedirect) { + if (codePushWebUrl && validator.isURL(codePushWebUrl)) { + log.debug(`login redirect:${codePushWebUrl}`); res.redirect(`${codePushWebUrl}/login`); } else { res.render('auth/login', { title: 'CodePushServer' }); @@ -27,16 +25,10 @@ router.get('/link', (req, res) => { }); router.get('/register', (req, res) => { - var config = require('../core/config'); var codePushWebUrl = _.get(config, 'common.codePushWebUrl'); var isRedirect = false; - if (codePushWebUrl) { - var validator = require('validator'); - if (validator.isURL(codePushWebUrl)){ - isRedirect = true; - } - } - if (isRedirect) { + if (codePushWebUrl && validator.isURL(codePushWebUrl)) { + log.debug(`register redirect:${codePushWebUrl}`); res.redirect(`${codePushWebUrl}/register`); } else { res.render('auth/login', { title: 'CodePushServer' }); @@ -48,20 +40,25 @@ router.post('/logout', (req, res) => { }); router.post('/login', (req, res, next) => { + var AppError = require('../core/app-error'); + var accountManager = require('../core/services/account-manager')(); + var security = require('../core/utils/security'); var account = _.trim(req.body.account); var password = _.trim(req.body.password); - var config = require('../core/config'); var tokenSecret = _.get(config, 'jwt.tokenSecret'); + log.debug(`login:${account}`); accountManager.login(account, password) .then((users) => { var jwt = require('jsonwebtoken'); return jwt.sign({ uid: users.id, hash: security.md5(users.ack_code), expiredIn: 7200 }, tokenSecret); }) .then((token) => { + log.debug(token); res.send({status:'OK', results: {tokens: token}}); }) .catch((e) => { if (e instanceof AppError.AppError) { + log.debug(e); res.send({status:'ERROR', errorMessage: e.message}); } else { next(e); diff --git a/routes/index.js b/routes/index.js index 9b1eb15c..d070b551 100644 --- a/routes/index.js +++ b/routes/index.js @@ -5,6 +5,8 @@ var AppError = require('../core/app-error'); var middleware = require('../core/middleware'); var ClientManager = require('../core/services/client-manager'); var _ = require('lodash'); +var log4js = require('log4js'); +var log = log4js.getLogger("cps:index"); router.get('/', (req, res, next) => { res.render('index', { title: 'CodePushServer' }); @@ -39,9 +41,24 @@ router.get('/updateCheck', (req, res, next) => { var appVersion = _.get(req, "query.appVersion"); var label = _.get(req, "query.label"); var packageHash = _.get(req, "query.packageHash") + var clientUniqueId = _.get(req, "query.clientUniqueId") var clientManager = new ClientManager(); - clientManager.updateCheckFromCache(deploymentKey, appVersion, label, packageHash) + log.debug('req.query', req.query); + clientManager.updateCheckFromCache(deploymentKey, appVersion, label, packageHash, clientUniqueId) .then((rs) => { + //灰度检测 + return clientManager.chosenMan(rs.packageId, rs.rollout, clientUniqueId) + .then((data)=>{ + if (!data) { + rs.isAvailable = false; + return rs; + } + return rs; + }); + }) + .then((rs) => { + delete rs.packageId; + delete rs.rollout; res.send({"updateInfo":rs}); }) .catch((e) => { @@ -54,6 +71,7 @@ router.get('/updateCheck', (req, res, next) => { }); router.post('/reportStatus/download', (req, res) => { + log.debug('req.body', req.body); var clientUniqueId = _.get(req, "body.clientUniqueId"); var label = _.get(req, "body.label"); var deploymentKey = _.get(req, "body.deploymentKey"); @@ -68,6 +86,7 @@ router.post('/reportStatus/download', (req, res) => { }); router.post('/reportStatus/deploy', (req, res) => { + log.debug('req.body', req.body); var clientUniqueId = _.get(req, "body.clientUniqueId"); var label = _.get(req, "body.label"); var deploymentKey = _.get(req, "body.deploymentKey"); diff --git a/routes/indexV1.js b/routes/indexV1.js new file mode 100644 index 00000000..1720f1a8 --- /dev/null +++ b/routes/indexV1.js @@ -0,0 +1,89 @@ +var express = require('express'); +var router = express.Router(); +var Promise = require('bluebird'); +var AppError = require('../core/app-error'); +var middleware = require('../core/middleware'); +var ClientManager = require('../core/services/client-manager'); +var _ = require('lodash'); +var log4js = require('log4js'); +var log = log4js.getLogger("cps:indexV1"); + +router.get('/update_check', (req, res, next) => { + var deploymentKey = _.get(req, "query.deployment_key"); + var appVersion = _.get(req, "query.app_version"); + var label = _.get(req, "query.label"); + var packageHash = _.get(req, "query.package_hash") + var isCompanion = _.get(req, "query.is_companion") + var clientUniqueId = _.get(req, "query.client_unique_id") + var clientManager = new ClientManager(); + log.debug('req.query', req.query); + clientManager.updateCheckFromCache(deploymentKey, appVersion, label, packageHash, clientUniqueId) + .then((rs) => { + //灰度检测 + return clientManager.chosenMan(rs.packageId, rs.rollout, clientUniqueId) + .then((data)=>{ + if (!data) { + rs.isAvailable = false; + return rs; + } + return rs; + }); + }) + .then((rs) => { + delete rs.packageId; + delete rs.rollout; + var update_info = { + download_url : rs.downloadUrl, + description : rs.description, + is_available : rs.isAvailable, + is_disabled : rs.isDisabled, + target_binary_range: rs.targetBinaryRange, + label: rs.label, + package_hash: rs.packageHash, + package_size: rs.packageSize, + should_run_binary_version: rs.shouldRunBinaryVersion, + update_app_version: rs.updateAppVersion, + is_mandatory: rs.isMandatory, + }; + res.send({"update_info": update_info}); + }) + .catch((e) => { + if (e instanceof AppError.AppError) { + res.status(404).send(e.message); + } else { + next(e); + } + }); +}); + +router.post('/report_status/download', (req, res) => { + log.debug('req.body', req.body); + var clientUniqueId = _.get(req, "body.client_unique_id"); + var label = _.get(req, "body.label"); + var deploymentKey = _.get(req, "body.deployment_key"); + var clientManager = new ClientManager(); + clientManager.reportStatusDownload(deploymentKey, label, clientUniqueId) + .catch((err) => { + if (!err instanceof AppError.AppError) { + console.error(err.stack) + } + }); + res.send('OK'); +}); + +router.post('/report_status/deploy', (req, res) => { + log.debug('req.body', req.body); + var clientUniqueId = _.get(req, "body.client_unique_id"); + var label = _.get(req, "body.label"); + var deploymentKey = _.get(req, "body.deployment_key"); + var clientManager = new ClientManager(); + clientManager.reportStatusDeploy(deploymentKey, label, clientUniqueId, req.body) + .catch((err) => { + if (!err instanceof AppError.AppError) { + console.error(err.stack) + } + }); + res.send('OK'); +}); + +module.exports = router; diff --git a/routes/sessions.js b/routes/sessions.js deleted file mode 100644 index a7b4ce18..00000000 --- a/routes/sessions.js +++ /dev/null @@ -1,24 +0,0 @@ -var express = require('express'); -var router = express.Router(); -var _ = require('lodash'); -var models = require('../models'); -var middleware = require('../core/middleware'); -var AppError = require('../core/app-error'); - -router.delete('/:machineName', middleware.checkToken, (req, res, next) => { - var machineName = _.trim(decodeURI(req.params.machineName)); - var uid = req.users.id; - models.UserTokens.destroy({where: {created_by:machineName, uid: uid}}) - .then((rowNum) => { - res.send(""); - }) - .catch(e => { - if (e instanceof AppError.AppError) { - res.status(406).send(e.message); - } else { - next(e); - } - }); -}); - -module.exports = router; diff --git a/sql/codepush-all.sql b/sql/codepush-all.sql index e20dfe06..7e2d8776 100644 --- a/sql/codepush-all.sql +++ b/sql/codepush-all.sql @@ -1,17 +1,18 @@ -DROP TABLE IF EXISTS `apps`; -CREATE TABLE `apps` ( +CREATE TABLE IF NOT EXISTS `apps` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL DEFAULT '', `uid` bigint(20) unsigned NOT NULL DEFAULT '0', + `os` tinyint(3) unsigned NOT NULL DEFAULT '0', + `platform` tinyint(3) unsigned NOT NULL DEFAULT '0', + `is_use_diff_text` tinyint(3) unsigned NOT NULL DEFAULT '0', `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `created_at` timestamp NULL DEFAULT NULL, `deleted_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_name` (`name`(12)) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `collaborators`; -CREATE TABLE `collaborators` ( +CREATE TABLE IF NOT EXISTS `collaborators` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `appid` int(10) unsigned NOT NULL DEFAULT '0', `uid` bigint(20) unsigned NOT NULL DEFAULT '0', @@ -24,9 +25,7 @@ CREATE TABLE `collaborators` ( KEY `idx_uid` (`uid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -DROP TABLE IF EXISTS `deployments`; -CREATE TABLE `deployments` ( +CREATE TABLE IF NOT EXISTS `deployments` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `appid` int(10) unsigned NOT NULL DEFAULT '0', `name` varchar(20) NOT NULL DEFAULT '', @@ -42,8 +41,7 @@ CREATE TABLE `deployments` ( KEY `idx_deploymentkey` (`deployment_key`(40)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `deployments_history`; -CREATE TABLE `deployments_history` ( +CREATE TABLE IF NOT EXISTS `deployments_history` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `deployment_id` int(11) unsigned NOT NULL DEFAULT '0', `package_id` int(10) unsigned NOT NULL DEFAULT '0', @@ -53,22 +51,23 @@ CREATE TABLE `deployments_history` ( KEY `idx_deployment_id` (`deployment_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -DROP TABLE IF EXISTS `deployments_versions`; -CREATE TABLE `deployments_versions` ( +CREATE TABLE IF NOT EXISTS `deployments_versions` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `deployment_id` int(11) unsigned NOT NULL DEFAULT '0', - `app_version` varchar(14) NOT NULL DEFAULT '', + `app_version` varchar(100) NOT NULL DEFAULT '', `current_package_id` int(10) unsigned NOT NULL DEFAULT '0', `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `created_at` timestamp NULL DEFAULT NULL, `deleted_at` timestamp NULL DEFAULT NULL, + `min_version` bigint(20) unsigned NOT NULL DEFAULT '0', + `max_version` bigint(20) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), - KEY `idx_did_appversion` (`deployment_id`,`app_version`) + KEY `idx_did_minversion` (`deployment_id`,`min_version`), + KEY `idx_did_maxversion` (`deployment_id`,`max_version`), + KEY `idx_did_appversion` (`deployment_id`,`app_version`(30)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `packages`; -CREATE TABLE `packages` ( +CREATE TABLE IF NOT EXISTS `packages` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `deployment_version_id` int(10) unsigned NOT NULL DEFAULT '0', `deployment_id` int(10) unsigned NOT NULL DEFAULT '0', @@ -83,16 +82,17 @@ CREATE TABLE `packages` ( `original_deployment` varchar(20) NOT NULL DEFAULT '', `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `created_at` timestamp NULL DEFAULT NULL, - `released_by` bigint(20) unsigned NOT NULL, + `released_by` bigint(20) unsigned NOT NULL DEFAULT '0', `is_mandatory` tinyint(3) unsigned NOT NULL DEFAULT '0', + `is_disabled` tinyint(3) unsigned NOT NULL DEFAULT '0', + `rollout` tinyint(3) unsigned NOT NULL DEFAULT '0', `deleted_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_deploymentid_label` (`deployment_id`,`label`(8)), KEY `idx_versions_id` (`deployment_version_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `packages_diff`; -CREATE TABLE `packages_diff` ( +CREATE TABLE IF NOT EXISTS `packages_diff` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `package_id` int(11) unsigned NOT NULL DEFAULT '0', `diff_against_package_hash` varchar(64) NOT NULL DEFAULT '', @@ -105,8 +105,7 @@ CREATE TABLE `packages_diff` ( KEY `idx_packageid_hash` (`package_id`,`diff_against_package_hash`(40)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `packages_metrics`; -CREATE TABLE `packages_metrics` ( +CREATE TABLE IF NOT EXISTS `packages_metrics` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `package_id` int(10) unsigned NOT NULL DEFAULT '0', `active` int(10) unsigned NOT NULL DEFAULT '0', @@ -120,8 +119,7 @@ CREATE TABLE `packages_metrics` ( KEY `idx_packageid` (`package_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `user_tokens`; -CREATE TABLE `user_tokens` ( +CREATE TABLE IF NOT EXISTS `user_tokens` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `uid` bigint(20) unsigned NOT NULL DEFAULT '0', `name` varchar(50) NOT NULL DEFAULT '', @@ -137,8 +135,7 @@ CREATE TABLE `user_tokens` ( KEY `idx_tokens` (`tokens`) KEY_BLOCK_SIZE=16 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -DROP TABLE IF EXISTS `users`; -CREATE TABLE `users` ( +CREATE TABLE IF NOT EXISTS `users` ( `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL DEFAULT '', `password` varchar(255) NOT NULL DEFAULT '', @@ -157,9 +154,7 @@ INSERT INTO `users` (`id`, `username`, `password`, `email`, `identical`, `ack_co VALUES (1,'admin','$2a$12$mvUY9kTqW4kSoGuZFDW0sOSgKmNY8SPHVyVrSckBTLtXKf6vKX3W.','lisong2010@gmail.com','4ksvOXqog','oZmGE','2016-11-14 10:46:55','2016-02-29 21:24:49'); - -DROP TABLE IF EXISTS `versions`; -CREATE TABLE `versions` ( +CREATE TABLE IF NOT EXISTS `versions` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `type` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '1.DBversion', `version` varchar(10) NOT NULL DEFAULT '', @@ -170,5 +165,24 @@ CREATE TABLE `versions` ( LOCK TABLES `versions` WRITE; INSERT INTO `versions` (`id`, `type`, `version`) VALUES - (1,1,'0.2.15'); -UNLOCK TABLES; \ No newline at end of file + (1,1,'0.5.0'); +UNLOCK TABLES; + +CREATE TABLE IF NOT EXISTS `log_report_deploy` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `status` tinyint(3) unsigned NOT NULL DEFAULT '0', + `package_id` int(10) unsigned NOT NULL DEFAULT '0', + `client_unique_id` varchar(100) NOT NULL DEFAULT '', + `previous_label` varchar(20) NOT NULL DEFAULT '', + `previous_deployment_key` varchar(64) NOT NULL DEFAULT '', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `log_report_download` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `package_id` int(10) unsigned NOT NULL DEFAULT '0', + `client_unique_id` varchar(100) NOT NULL DEFAULT '', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/sql/codepush-v0.2.14.sql b/sql/codepush-v0.2.14-patch.sql similarity index 100% rename from sql/codepush-v0.2.14.sql rename to sql/codepush-v0.2.14-patch.sql diff --git a/sql/codepush-v0.2.15.sql b/sql/codepush-v0.2.15-patch.sql similarity index 100% rename from sql/codepush-v0.2.15.sql rename to sql/codepush-v0.2.15-patch.sql diff --git a/sql/codepush-v0.3.0-patch.sql b/sql/codepush-v0.3.0-patch.sql new file mode 100644 index 00000000..bd672fe5 --- /dev/null +++ b/sql/codepush-v0.3.0-patch.sql @@ -0,0 +1,25 @@ +ALTER TABLE `apps` ADD `os` TINYINT UNSIGNED NOT NULL DEFAULT 0; +ALTER TABLE `apps` ADD `platform` TINYINT UNSIGNED NOT NULL DEFAULT 0; +ALTER TABLE `packages` ADD `is_disabled` TINYINT UNSIGNED NOT NULL DEFAULT 0; +ALTER TABLE `packages` ADD `rollout` TINYINT UNSIGNED NOT NULL DEFAULT 100; + +CREATE TABLE `log_report_deploy` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `status` tinyint(3) unsigned NOT NULL DEFAULT '0', + `package_id` int(10) unsigned NOT NULL DEFAULT '0', + `client_unique_id` varchar(100) NOT NULL DEFAULT '', + `previous_label` varchar(20) NOT NULL DEFAULT '', + `previous_deployment_key` varchar(64) NOT NULL DEFAULT '', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `log_report_download` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `package_id` int(10) unsigned NOT NULL DEFAULT '0', + `client_unique_id` varchar(100) NOT NULL DEFAULT '', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +UPDATE `versions` SET `version` = '0.3.0' WHERE `type` = '1'; \ No newline at end of file diff --git a/sql/codepush-v0.4.0-patch.sql b/sql/codepush-v0.4.0-patch.sql new file mode 100644 index 00000000..a8933f94 --- /dev/null +++ b/sql/codepush-v0.4.0-patch.sql @@ -0,0 +1,9 @@ +ALTER TABLE `deployments_versions` ADD `min_version` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0'; +ALTER TABLE `deployments_versions` ADD `max_version` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0'; +ALTER TABLE `deployments_versions` ADD INDEX `idx_did_min_version` (`deployment_id`, `min_version`); +ALTER TABLE `deployments_versions` ADD INDEX `idx_did_maxversion` (`deployment_id`, `max_version`); +ALTER TABLE `deployments_versions` CHANGE `app_version` `app_version` VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT ''; +ALTER TABLE `deployments_versions` DROP INDEX `idx_did_appversion`; +ALTER TABLE `deployments_versions` ADD INDEX `idx_did_appversion` (`deployment_id`, `app_version` (30)); + +UPDATE `versions` SET `version` = '0.4.0' WHERE `type` = '1'; \ No newline at end of file diff --git a/sql/codepush-v0.5.0-patch.sql b/sql/codepush-v0.5.0-patch.sql new file mode 100644 index 00000000..09b822d3 --- /dev/null +++ b/sql/codepush-v0.5.0-patch.sql @@ -0,0 +1,3 @@ +ALTER TABLE `apps` ADD `is_use_diff_text` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0'; + +UPDATE `versions` SET `version` = '0.5.0' WHERE `type` = '1'; \ No newline at end of file diff --git a/test/api/accessKeys/accessKeys.test.js b/test/api/accessKeys/accessKeys.test.js index 83122f8d..69dc2fd6 100644 --- a/test/api/accessKeys/accessKeys.test.js +++ b/test/api/accessKeys/accessKeys.test.js @@ -30,14 +30,14 @@ describe('api/accessKeys/accessKeys.test.js', function() { it('should create accessKeys successful', function(done) { request.post(`/accessKeys`) .set('Authorization', `Basic ${authToken}`) - .send({createdBy: 'tablee', friendlyName: friendlyName, isSession: false, ttl: 30*24*60*60}) + .send({createdBy: 'tablee', friendlyName: friendlyName, ttl: 30*24*60*60}) .end(function(err, res) { should.not.exist(err); res.status.should.equal(200); var rs = JSON.parse(res.text); rs.should.have.properties('accessKey'); rs.accessKey.should.have.properties(['name', 'createdTime', 'createdBy', - 'expires', 'isSession', 'description', 'friendlyName']); + 'expires', 'description', 'friendlyName']); done(); }); }); @@ -45,7 +45,7 @@ describe('api/accessKeys/accessKeys.test.js', function() { it('should not create accessKeys successful when friendlyName exist', function(done) { request.post(`/accessKeys`) .set('Authorization', `Basic ${authToken}`) - .send({createdBy: 'tablee', friendlyName: friendlyName, isSession: true, ttl: 30*24*60*60}) + .send({createdBy: 'tablee', friendlyName: friendlyName, ttl: 30*24*60*60}) .end(function(err, res) { should.not.exist(err); res.status.should.equal(406); @@ -68,84 +68,13 @@ describe('api/accessKeys/accessKeys.test.js', function() { rs.accessKeys.should.be.an.instanceOf(Array); rs.accessKeys.should.matchEach(function(it) { return it.should.have.properties(['name', 'createdTime', 'createdBy', - 'expires', 'isSession', 'description', 'friendlyName']); + 'expires', 'description', 'friendlyName']); }); done(); }); }); }); - describe('modify accessKeys', function(done) { - it('should modify accessKeys add ttl successful', function(done) { - request.patch(`/accessKeys/${encodeURI(friendlyName)}`) - .set('Authorization', `Basic ${authToken}`) - .send({ttl: 7*24*60*60*1000}) - .end(function(err, res) { - should.not.exist(err); - res.status.should.equal(200); - var rs = JSON.parse(res.text); - rs.should.have.properties('accessKey'); - rs.accessKey.should.have.properties(['name', 'createdTime', 'createdBy', - 'expires', 'isSession', 'description', 'friendlyName']); - done(); - }); - }); - - it('should modify accessKeys substact ttl successful', function(done) { - request.patch(`/accessKeys/${encodeURI(friendlyName)}`) - .set('Authorization', `Basic ${authToken}`) - .send({ttl: -7*24*60*60*1000}) - .end(function(err, res) { - should.not.exist(err); - res.status.should.equal(200); - var rs = JSON.parse(res.text); - rs.should.have.properties('accessKey'); - rs.accessKey.should.have.properties(['name', 'createdTime', 'createdBy', - 'expires', 'isSession', 'description', 'friendlyName']); - done(); - }); - }); - - it('should not modify accessKeys friendlyName successful when friendlyName exists', function(done) { - request.patch(`/accessKeys/${encodeURI(friendlyName)}`) - .set('Authorization', `Basic ${authToken}`) - .send({friendlyName: friendlyName}) - .end(function(err, res) { - should.not.exist(err); - res.status.should.equal(406); - res.text.should.equal(`The access key "${friendlyName}" already exists.`); - done(); - }); - }); - - it('should not modify accessKeys friendlyName successful when friendlyName invalid', function(done) { - request.patch(`/accessKeys/${encodeURI(newFriendlyName)}`) - .set('Authorization', `Basic ${authToken}`) - .send({friendlyName: newFriendlyName}) - .end(function(err, res) { - should.not.exist(err); - res.status.should.equal(406); - res.text.should.equal(`The access key "${newFriendlyName}" does not exist.`); - done(); - }); - }); - - it('should modify accessKeys friendlyName successful', function(done) { - request.patch(`/accessKeys/${encodeURI(friendlyName)}`) - .set('Authorization', `Basic ${authToken}`) - .send({friendlyName: newFriendlyName}) - .end(function(err, res) { - should.not.exist(err); - res.status.should.equal(200); - var rs = JSON.parse(res.text); - rs.should.have.properties('accessKey'); - rs.accessKey.should.have.properties(['name', 'createdTime', 'createdBy', - 'expires', 'isSession', 'description', 'friendlyName']); - done(); - }); - }); - }); - describe('delete accessKeys', function(done) { it('should delete accessKeys successful', function(done) { request.delete(`/accessKeys/${encodeURI(newFriendlyName)}`) diff --git a/test/api/account/account.test.js b/test/api/account/account.test.js index 83ebb2ce..62e262a3 100644 --- a/test/api/account/account.test.js +++ b/test/api/account/account.test.js @@ -35,7 +35,7 @@ describe('api/account/account.test.js', function() { res.status.should.equal(200); var rs = JSON.parse(res.text); rs.should.have.properties('account'); - rs.account.should.have.properties(['email', 'id', 'linkedProviders', 'name']); + rs.account.should.have.properties(['email', 'linkedProviders', 'name']); done(); }); }); diff --git a/test/api/apps/apps.test.js b/test/api/apps/apps.test.js index 4628b7a9..d393a42f 100644 --- a/test/api/apps/apps.test.js +++ b/test/api/apps/apps.test.js @@ -13,8 +13,8 @@ describe('api/apps/apps.test.js', function() { var authToken; var machineName = `Login-${Math.random()}`; var friendlyName = `Login-${Math.random()}`; - var appName = 'test-ios'; - var newAppName = 'newtest-ios'; + var appName = 'test'; + var newAppName = 'newtest'; var bearerToken; var testDeployment = 'test'; var newTestDeployment = 'newtest'; @@ -38,14 +38,14 @@ describe('api/apps/apps.test.js', function() { it('should create accessKeys successful', function(done) { request.post(`/accessKeys`) .set('Authorization', `Basic ${authToken}`) - .send({createdBy: machineName, friendlyName: friendlyName, isSession: true, ttl: 30*24*60*60}) + .send({createdBy: machineName, friendlyName: friendlyName, ttl: 30*24*60*60}) .end(function(err, res) { should.not.exist(err); res.status.should.equal(200); var rs = JSON.parse(res.text); rs.should.have.properties('accessKey'); rs.accessKey.should.have.properties(['name', 'createdTime', 'createdBy', - 'expires', 'isSession', 'description', 'friendlyName']); + 'expires', 'description', 'friendlyName']); bearerToken = _.get(rs, 'accessKey.name'); done(); }); @@ -65,23 +65,10 @@ describe('api/apps/apps.test.js', function() { }); }); - it('should not add apps successful when appName is invalid', function(done) { - var appName = 'test'; - request.post(`/apps`) - .set('Authorization', `Bearer ${bearerToken}`) - .send({name: appName}) - .end(function(err, res) { - should.not.exist(err); - res.status.should.equal(406); - res.text.should.equal(`appName have to point -android or -ios suffix! eg. ${appName}-android ${appName}-ios`); - done(); - }); - }); - it('should add apps successful', function(done) { request.post(`/apps`) .set('Authorization', `Bearer ${bearerToken}`) - .send({name: appName}) + .send({name: appName, os:'iOS', platform:'React-Native'}) .end(function(err, res) { should.not.exist(err); res.status.should.equal(200); @@ -95,7 +82,7 @@ describe('api/apps/apps.test.js', function() { it('should not add apps successful when appName exists', function(done) { request.post(`/apps`) .set('Authorization', `Bearer ${bearerToken}`) - .send({name: appName}) + .send({name: appName, os:'iOS', platform:'React-Native'}) .end(function(err, res) { should.not.exist(err); res.status.should.equal(406); @@ -429,19 +416,6 @@ describe('api/apps/apps.test.js', function() { }); }); - it(`should not rename apps successful when new name invalid`, function(done) { - var newAppName = 'newtest'; - request.patch(`/apps/${appName}`) - .set('Authorization', `Bearer ${bearerToken}`) - .send({name: newAppName}) - .end(function(err, res) { - should.not.exist(err); - res.status.should.equal(406); - res.text.should.equal(`new appName have to point -ios suffix! eg. Demo-ios`); - done(); - }); - }); - it(`should rename apps successful`, function(done) { request.patch(`/apps/${appName}`) .set('Authorization', `Bearer ${bearerToken}`) diff --git a/test/api/apps/release.test.js b/test/api/apps/release.test.js index dd6bdd9c..00eb698d 100644 --- a/test/api/apps/release.test.js +++ b/test/api/apps/release.test.js @@ -35,14 +35,14 @@ describe('api/apps/release.test.js', function() { it('should create accessKeys successful', function(done) { request.post(`/accessKeys`) .set('Authorization', `Basic ${authToken}`) - .send({createdBy: machineName, friendlyName: friendlyName, isSession: true, ttl: 30*24*60*60}) + .send({createdBy: machineName, friendlyName: friendlyName, ttl: 30*24*60*60}) .end(function(err, res) { should.not.exist(err); res.status.should.equal(200); var rs = JSON.parse(res.text); rs.should.have.properties('accessKey'); rs.accessKey.should.have.properties(['name', 'createdTime', 'createdBy', - 'expires', 'isSession', 'description', 'friendlyName']); + 'expires', 'description', 'friendlyName']); bearerToken = _.get(rs, 'accessKey.name'); done(); }); @@ -53,7 +53,7 @@ describe('api/apps/release.test.js', function() { it('should add apps successful', function(done) { request.post(`/apps`) .set('Authorization', `Bearer ${bearerToken}`) - .send({name: appName}) + .send({name: appName, os:'iOS', platform:'React-Native'}) .end(function(err, res) { should.not.exist(err); res.status.should.equal(200); @@ -99,7 +99,6 @@ describe('api/apps/release.test.js', function() { .set('Authorization', `Bearer ${bearerToken}`) .send() .end(function(err, res) { - should.not.exist(err); res.status.should.equal(200); done(); }); diff --git a/test/api/auth/auth.test.js b/test/api/auth/auth.test.js index 06311a30..0263639a 100644 --- a/test/api/auth/auth.test.js +++ b/test/api/auth/auth.test.js @@ -10,7 +10,7 @@ describe('api/auth/test.js', function() { describe('sign in view', function(done) { it('should show sign in redirect view successful', function(done) { - _.set(config, 'common.codePushWebUrl', 'http://localhost:3001') + _.set(config, 'common.codePushWebUrl', 'http://127.0.0.1:3001') request.get('/auth/login') .send() .end(function(err, res) { @@ -34,7 +34,7 @@ describe('api/auth/test.js', function() { describe('sign up view', function(done) { it('should show sign up redirect view successful', function(done) { - _.set(config, 'common.codePushWebUrl', 'http://localhost:3001') + _.set(config, 'common.codePushWebUrl', 'http://127.0.0.1:3001') request.get('/auth/register') .send() .end(function(err, res) { diff --git a/test/api/index/index.test.js b/test/api/index/index.test.js index 0f8576a6..51f51289 100644 --- a/test/api/index/index.test.js +++ b/test/api/index/index.test.js @@ -117,7 +117,7 @@ describe('api/index/index.test.js', function() { .end(function(err, res) { should.not.exist(err); res.status.should.equal(404); - res.text.should.equal(`does not found deployment`); + res.text.should.equal(`Not found deployment, check deployment key is right.`); done(); }); }); diff --git a/test/api/init/database.js b/test/api/init/database.js index 90b7238e..a32c0aea 100644 --- a/test/api/init/database.js +++ b/test/api/init/database.js @@ -1,5 +1,5 @@ var config = require('../../../core/config'); -var mysql = require('mysql'); +var mysql = require('mysql2'); var redis = require('redis'); var should = require('should'); var fs = require('fs'); @@ -12,10 +12,11 @@ describe('api/init/database.js', function() { var connection = mysql.createConnection({ host: config.db.host, user: config.db.username, - password: config.db.password + password: config.db.password, + multipleStatements: true }); connection.connect(); - connection.query(`CREATE DATABASE IF NOT EXISTS ${config.db.database}`, function(err, rows, fields) { + connection.query(`DROP DATABASE IF EXISTS ${config.db.database};CREATE DATABASE IF NOT EXISTS ${config.db.database}`, function(err, rows, fields) { should.not.exist(err); done(); }); diff --git a/test/api/sessions/sessions.test.js b/test/api/sessions/sessions.test.js deleted file mode 100644 index 3e7821a6..00000000 --- a/test/api/sessions/sessions.test.js +++ /dev/null @@ -1,58 +0,0 @@ -var app = require('../../../app'); -var request = require('supertest')(app); -var should = require("should"); -var security = require('../../../core/utils/security'); -var factory = require('../../../core/utils/factory'); -var _ = require('lodash'); - -describe('api/sessions/sessions.test.js', function() { - var account = '522539441@qq.com'; - var password = '123456'; - var authToken; - var friendlyName = 'sessions'; - var machineName = 'tablee.hosts'; - before(function(done){ - request.post('/auth/login') - .send({ - account: account, - password: password - }) - .end(function(err, res) { - should.not.exist(err); - var rs = JSON.parse(res.text); - rs.should.containEql({status:"OK"}); - authToken = (new Buffer(`auth:${_.get(rs, 'results.tokens')}`)).toString('base64'); - done(); - }); - }); - - describe('create accessKeys', function() { - it('should create accessKeys successful', function(done) { - request.post(`/accessKeys`) - .set('Authorization', `Basic ${authToken}`) - .send({createdBy: machineName, friendlyName: friendlyName, isSession: true, ttl: 30*24*60*60}) - .end(function(err, res) { - should.not.exist(err); - res.status.should.equal(200); - var rs = JSON.parse(res.text); - rs.should.have.properties('accessKey'); - rs.accessKey.should.have.properties(['name', 'createdTime', 'createdBy', - 'expires', 'isSession', 'description', 'friendlyName']); - done(); - }); - }); - }); - - describe('delete sessions', function() { - it('should delete sessions successful', function(done) { - request.delete(`/sessions/${encodeURI(machineName)}`) - .set('Authorization', `Basic ${authToken}`) - .send() - .end(function(err, res) { - should.not.exist(err); - res.status.should.equal(200); - done(); - }); - }); - }); -}); diff --git a/test/api/users/users.test.js b/test/api/users/users.test.js index 57d87109..2b936dbf 100644 --- a/test/api/users/users.test.js +++ b/test/api/users/users.test.js @@ -78,7 +78,8 @@ describe('api/users/users.test.js', function() { .then(function (t) { storageToken = t; done(); - }); + }) + .finally(() => client.quit()); }); it('should not check register code successful when email already exists', function(done) { @@ -131,7 +132,8 @@ describe('api/users/users.test.js', function() { .then(function (t) { storageToken = t; done(); - }); + }) + .finally(() => client.quit()); }); it('should not sign up successful when password length invalid', function(done) { @@ -174,10 +176,13 @@ describe('api/users/users.test.js', function() { it('should not change password successful when authToken invalid', function(done) { request.patch(`/users/password`) + .set('Authorization', `Basic 11345`) .send({oldPassword: password, newPassword: newPassword}) .end(function(err, res) { should.not.exist(err); - res.status.should.equal(401); + var rs = JSON.parse(res.text); + res.status.should.equal(200); + rs.should.containEql({status:401}); done(); }); }); diff --git a/views/auth/password.pug b/views/auth/password.pug new file mode 100644 index 00000000..47fa302c --- /dev/null +++ b/views/auth/password.pug @@ -0,0 +1,57 @@ +extends ../layout + +block content + .container(style="margin-top:30px;") + form#form.col-md-5.col-md-offset-3(method="post") + .form-group + label.sr-only(for="inputEmail") 邮箱地址/用户名 + input#inputEmail.form-control(type="text" name="account" placeholder="邮箱地址/用户名" required autofocus) + .form-group + .col-md-10(style="margin-left:-15px;") + label.sr-only(for="inputToken") token + input#inputToken.form-control(type="text" name="token" placeholder="token" required) + .col-md-2 + a.form-control.btn.btn-link(style="margin-bottom:15px;" target="_blank" href="/auth/login") 获取token + .form-group + label.sr-only(for="inputPassword") 原密码 + input#inputPassword.form-control(type="password" name="oldPassword" placeholder="原密码" required) + .form-group + label.sr-only(for="inputNewPassword") 新密码 + input#inputNewPassword.form-control(type="password" name="newPassword" placeholder="新密码" required) + .form-group + a#submitBtn.btn.btn-lg.btn-primary.btn-block 修改密码 + +block js + script(). + var submit = false; + $('#submitBtn').on('click', function () { + if (submit) { + return ; + } + var token = $('#inputToken').val(); + var oldPassword = $('#inputPassword').val(); + var newPassword = $('#inputNewPassword').val(); + submit = true; + $.ajax({ + type: 'patch', + data: JSON.stringify({oldPassword:oldPassword,newPassword:newPassword}), + contentType: 'application/json;charset=utf-8', + headers: { + Authorization : 'Bearer '+token + }, + url: '/users/password', + dataType: 'json', + success: function (data) { + if (data.status == "OK") { + alert("修改成功"); + location.href = '/auth/login'; + } else if (data.status == 401) { + alert('token invalid'); + } else { + alert(data.message); + } + submit = false; + } + }); + }); + diff --git a/views/index.pug b/views/index.pug index c97cb43a..800da9bd 100644 --- a/views/index.pug +++ b/views/index.pug @@ -7,4 +7,5 @@ block content h1(style="text-align: center;")= title p(style="text-align: center;") Welcome to #{title} .site-notice - a.btn.btn-primary(href="/auth/login" type="button") 登录 \ No newline at end of file + a.btn.btn-primary(href="/auth/login" type="button") 登录 + a.btn.btn-primary.col-md-offset-1(href="/auth/password" type="button") 修改密码 \ No newline at end of file diff --git a/views/layout.pug b/views/layout.pug index a0ba1682..85d88861 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -2,6 +2,8 @@ doctype html html head title= title + meta(name="keywords" content="code-push-server,code-push,react-native,cordova") + meta(name="description" content="CodePush service is hotupdate services which adapter react-native-code-push and cordova-plugin-code-push") link(rel='stylesheet', href='/js/bootstrap-3.3.7/css/bootstrap.min.css') block css body diff --git a/views/tokens.pug b/views/tokens.pug index 81829751..ad770b56 100644 --- a/views/tokens.pug +++ b/views/tokens.pug @@ -53,7 +53,10 @@ block js $.ajax({ type: 'post', data: postParams, - url: '/accessKeys?access_token='+access_token, + headers: { + Authorization : 'Bearer '+access_token + }, + url: '/accessKeys', dataType: 'json', success: function (data) { submit = false;