├── .gitignore
├── .ignore
├── LICENSE
├── README.md
├── bin
└── client
├── docs
├── architecture.md
├── config.md
├── history.md
├── linkFront2Backend.md
├── modules.md
├── projecName.md
├── server.md
├── swift.md
└── version.md
├── npm-shrinkwrap.json
├── package.json
└── src
└── js
├── annotation
├── Command.js
├── Factory.js
└── Flow
│ ├── Done.js
│ ├── Flow.js
│ └── Step.js
├── app
├── app.js
├── code
│ ├── annotation
│ │ ├── Factory.js
│ │ └── ResponseBodyDeal.js
│ ├── cache
│ │ ├── cache.js
│ │ ├── index.js
│ │ └── zkCache.js
│ ├── controller
│ │ ├── cacheStrategy.js
│ │ ├── index.js
│ │ └── manage.js
│ ├── dao
│ │ ├── index.js
│ │ ├── packageList.js
│ │ └── zkPackageList.js
│ └── storage
│ │ ├── index.js
│ │ ├── localfile.js
│ │ └── swift.js
├── favicon.ico
├── healthcheck.html
├── index.js
├── public
│ ├── scripts
│ │ └── strategy
│ │ │ └── index.js
│ ├── styles
│ │ └── strategy.css
│ └── tree
│ │ ├── directory.html
│ │ ├── icons
│ │ ├── box.png
│ │ ├── drive.png
│ │ └── folder.png
│ │ ├── info.html
│ │ └── style.css
├── views
│ └── strategy.ejs
└── widget
│ └── render.js
├── command
├── clean.js
├── config.js
├── download.js
├── help.js
├── install.js
├── publish.js
├── qdownload.js
├── qupload.js
├── server.js
├── swift.js
└── upload.js
├── common
├── checkUtils.js
├── compressUtils.js
├── console.js
├── constant.js
├── f2bConfigUtils.js
├── installUtils.js
├── manifestUtils.js
├── npmUtils.js
├── shellUtils.js
├── swiftUtils.js
├── utils.js
└── zkClient.js
├── lib
└── swiftClient
│ ├── index.js
│ └── lib
│ ├── multipart.js
│ └── swift.js
└── registry
├── nexus.js
└── node.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # kdiff3 ignore
2 | *.orig
3 |
4 | # maven ignore
5 | target/
6 |
7 | # eclipse ignore
8 | .settings/
9 | .project
10 | .classpath
11 |
12 | # idea ignore
13 | .idea/
14 | *.ipr
15 | *.iml
16 | *.iws
17 |
18 | # temp ignore
19 | *.log
20 | *.cache
21 | *.diff
22 | *.patch
23 | *.tmp
24 |
25 | # system ignore
26 | .DS_Store
27 | Thumbs.db
28 |
29 | # package ignore (optional)
30 | # *.jar
31 | # *.war
32 | # *.zip
33 | # *.tar
34 | # *.tar.gz
35 |
36 | #project
37 | /node_modules
38 | .npm-cache-share
39 | /npm_cache_share
40 | /test
41 | token.json
42 | .vscode/
43 |
--------------------------------------------------------------------------------
/.ignore:
--------------------------------------------------------------------------------
1 | .gitignore
2 | .git
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Robinlim
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # npm_cache_share
2 | ## 介绍
3 | 该工程是用于发布时安装模块依赖的,强制依赖npm-shrinkwrap.json文件,如果工程没有该文件,请执行npm shrinkwrap来生成(否则走正常的npm install)。工程主要是以模块为缓存单元来进行安装的,有的缓存的做法是将整个工程的node_modules来生成缓存,只要有新增的模块就要生成新的,在空间和时间上都不是最细粒度的。该方案涉及两个阶段缓存,第一阶段是本地缓存,第二阶段是公共缓存(存在于其他机器上,需要在该机器上启动缓存服务)。
4 |
5 | ## 产生背景
6 | 为了避免将node_modules入到工程版本库里(这种方式对于有环境依赖的模块是有问题的,比如node-sass,fibers等必须重新build才能运行),需要在发布过程中动态安装,虽然npm自身也有cache,但即使开启了(通过 *--cache-min 时间* 来开启)还是会发起请求询问是否更新,当然这个不是主要问题,主要还是依赖node-gyp的模块,有的编译时间耗费较长,故为了解决这些问题而产生该项目。可以通过docker来模拟目标机器的环境来执行编译操作。
7 |
8 | ## 缓存服务搭建工作
9 |
10 | #### client端
11 |
12 | 1. 安装node.js环境,可以上官网自行安装,版本 ≥0.12
13 | 2. 安装npm_cache_share,`npm install -g npm_cache_share`
14 | 3. 根据场景执行 `ncs install [options]`,可参见 [指令](#command)
15 |
16 | #### 中央公共服务
17 |
18 | 1. 安装node.js环境,可以上官网自行安装,版本 ≥0.12
19 | 2. 安装npm_cache_share,`npm install -g npm_cache_share`
20 | 3. 启动服务 `ncs server [options]`, 可参见 [中央公共服务](./docs/server.md)
21 |
22 | ## 文档
23 | - [中央公共服务](./docs/server.md)
24 | - [前后端关联](./docs/linkFront2Backend.md)
25 | - [包发布与安装](./docs/modules.md)
26 | - [架构](./docs/architecture.md)
27 |
28 | ## 工程命名
29 | 在pacakage.json中针对name的格式进行约束,只有满足了该格式才允许继续,可参见[工程命名](./docs/projecName.md),注意需要符合npm的命名要求
30 |
31 | ## SNAPSHOT和RELEASE
32 | 区分SNAPSHOT和RELEASE版本,根据文件名来自动识别是NAPSHOT,还是RELEASE,可参加[SNAPSHOT和RELEASE](./docs/version.md)
33 |
34 | ## [配置信息](./docs/config.md)
35 |
36 | ## 注意
37 | - 针对optionalDependecise里的包,明确知道系统环境不符合,可以在配置文件中配置npmPlatBinds来过滤这些模块。
38 | - 例子:安装fsevents包,需要OS是darwin,但当前环境是linux,此时可以设置 npmPlatBinds = {"fsevents": 1}
39 | - 如果存在依赖的模块需要额外执行安装后操作,或者有需要强制更新指定模块的,请挪驾[中央公共服务](./docs/server.md)查看
40 | - 如果遇到域名解析失败,getaddrinfo这种的,可以在服务端配置storageConfig的host时直接使用IP
41 | - 由于历史迭代版本,yarn安装会产生莫名错误,可以考虑清空本地缓存,对于运行效率首次会受影响,后续正常
42 | - 在centos6系统下node8以上版本gyp编译的时候会失败,可以看看本地gcc的版本,默认是4.4.7,需要升级到4.7版本以上才行,升级gcc要万分小心
43 | - ncs 1.1.0版本不再区分node版本,删除对fibers的依赖
44 | - ~~node版本 > 6之后请安装ncs 1.0.55之后(不含1.0.55)的版本~~
45 | - ~~Mac系统的用户需要注意下,node版本最低要大于等于4,因为fibers对Xcode有要求。如果非要使用,可查阅[fibers issues](https://github.com/nodejs/node-gyp/issues/1160)里的方式解决~~
46 |
47 |
指令
48 |
49 | ```
50 | Usage: [options]
51 |
52 | Commands:
53 |
54 | server 将会启动公共缓存服务,一般用做多台机器共享缓存模块,会在指令执行路径下生成npm_cache_share文件夹来存放缓存
55 | install 进行依赖安装,会依赖npm-shrinkwrap.json文件来安装模块(如果不存在则使用npm install)
56 | clean 清除缓存,需要指定是客户端,还是服务端,默认是清除客户端缓存目录
57 | config 配置选项默认值,可set,get,list大部分option,也可以通过~/.npm_cache_share_config.json手动修改这些配置,会影响所有指令
58 | publish 发布一个包到中央缓存
59 | upload 上传一个静态资源到swift仓库
60 | download 从swift仓库下载一个静态资源
61 | swift swift仓库操作,目前支持查询对象是否存在,删除对象功能。
62 | help 帮助说明
63 |
64 | Options:
65 | -V,--version 打印当前版本信息
66 | -d,--debug 打印所有调试信息
67 | -h,--help 显示该条命令的帮助信息
68 | -g,--config 读取指定的配置文件
69 |
70 | of 'server'
71 | -s,--storage 指定中央缓存类型,目前有localfile和swift
72 | -c --storageConfig 指定中央缓存配置,localfile为"[存储目录路径]",swift为"[HOST\|USER\|TOKEN]"
73 | -p,--port 指定公共缓存服务的端口,使用如 npm_cache_share server --port 9999
74 | -f,--useFork 强制使用fork方式启动(如果检测到本地pm2会自动使用pm2启动服务)
75 | -t,--token 指定服务的令牌token,用于校验用户上传权限
76 | -i,--i 适用于pm2启动服务,指定进程数目
77 | -n,--name 适用于pm2启动服务,指定服务名称
78 |
79 | of 'install'
80 | -e,--repository 指定公共缓存服务仓库,由HOST:PORT/NAME构成
81 | -r,--registry 指定安装的源, 使用如 npm_cache_share install --registry 源
82 | -t,--token 仅type=node,指定公共服务上传所需要校验的token
83 | -a,--auth 仅type=nexus,指定nexus账户(username:password)
84 | -n,--npm [npm] 可指定npm的安装路径来执行npm指令,用于指定特定版本的npm
85 | -p,--production,--noOptional,--save,--save-dev 同npm安装
86 | -f,--forcenpm 安装模块时强制使用npm,即使本地安装了yarn
87 | --ignoreBlackList 安装时忽略服务器的黑名单
88 | --checkSnapshotDeps 安装时检查依赖中是否存在SNAPSHOT版本的模块
89 |
90 | of 'clean'
91 | -s,--forServer 指定当前运行环境是在公共缓存服务上,使用如 npm_cache_share clean --forServer
92 |
93 | of 'publish'
94 | -c,--type 指定公共缓存服务类型,目前有node(默认)与npm(需要提前npm login)
95 | -e,--repository 指定公共缓存服务仓库,由HOST:PORT/NAME构成
96 | -t,--token 仅type=node,指定公共服务上传所需要校验的token
97 | -p,--password 每个包上传时可以设置一个密码,覆盖该包时必须使用该密码
98 | -s, --snapshot 作为快照版本上传,发布始终覆盖,安装始终更新,忽略本地缓存
99 | -u, --alwaysUpdate 覆盖服务器上同名版本(在单机服务的情况下安装会始终更新,忽略本地缓存),version内容不变
100 | -o, --override 如果指定-s参数,则会将新的version更新文件信息,默认不更新
101 | -v, --moduleVersion 指定发布版本号
102 | --checkSnapshotDeps 发布时检查依赖中是否存在SNAPSHOT版本的模块
103 |
104 | of 'upload','download','qupload','qdownload'
105 | -h, --host swift仓库的地址
106 | -u, --user swift仓库的账户
107 | -w, --pass swift仓库的密码
108 | -c, --container 需要上传/下载的目标容器
109 | -f, --forceUpdate 该参数只有qupload和upload使用,会覆盖服务器上同名版本
110 | -a, --auto 该参数只有qupload和qdownload使用,为true则通过package.json里的信息来指定container,否则就通过container参数或者全局配置文件里resourceSwift来指定
111 | -z, --compressType 指定压缩方式,zip或者tar,默认为tar
112 |
113 | of 'swift' 提供简单的swift操作,目前支持query和delete
114 | -h, --host swift仓库的地址
115 | -u, --user swift仓库的账户
116 | -w, --pass swift仓库的密码
117 | -c, --container 需要上传/下载的目标容器
118 | ```
119 | ## 涉及第三方存储
120 | [Swift](./docs/swift.md)
121 |
122 | ## 历史版本
123 | [历史版本](./docs/history.md)
124 |
--------------------------------------------------------------------------------
/bin/client:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var nodeAnnotation = require('node-annotation');
4 | var path = require('path');
5 |
6 | require('../src/js/common/console');
7 |
8 | /**
9 | * 配置全局错误处理
10 | * [Function] 错误处理函数
11 | */
12 | nodeAnnotation.setGlobalErrorHandler(function(err){
13 | console.error(err.stack || JSON.stringify(err));
14 | process.exit(1);
15 | });
16 |
17 | /**
18 | * 配置node-annotation内的日志流出
19 | * [Boolean] 是否开启日志,默认true
20 | * [String] "error/warn/info/log" 输出日至级别,默认warn
21 | * [Function/LoggerObject] 日志处理函数或对象(类似log4js的Logger对象),默认为console
22 | */
23 | nodeAnnotation.setLogger(true, 'error', function(str, level) {
24 | console.error('[ERROR]', str);
25 | });
26 |
27 | var ignoreDirs = {};
28 | ignoreDirs[path.resolve(__dirname, '../src/js/app')] = true;
29 | nodeAnnotation.start(path.resolve(__dirname,'..', 'src'), ignoreDirs, function(){
30 | global.run();
31 | });
32 |
--------------------------------------------------------------------------------
/docs/architecture.md:
--------------------------------------------------------------------------------
1 | # 工程结构
2 |
3 | 使用[node-annotation](https://www.npmjs.com/package/node-annotation),由bin/client为入口解析全部注解启动。
4 |
5 | src/js下的代码结构及功能如下:
6 |
7 | ├── annotation 自定义注解
8 | │ ├── Command.js 命令注解,所有命令被此注解注册
9 | │ ├── Factory.js 注册registry的工厂模式注解
10 | │ └── Flow 流程注解,用于代码中的流程控制(类似async库里的流程函数)
11 | ├── app 中央缓存服务(对应registry中的node类型)
12 | │ ├── app.js server服务app
13 | │ ├── code server代码
14 | │ │ ├── annotation 自定义注解
15 | │ │ │ └── Factory.js 注册storage的工厂模式注解
16 | │ │ ├── cache 缓存所有仓库和包的索引信息
17 | │ │ │ └── index.js
18 | │ │ │ └── cache.js
19 | │ │ │ └── zkCache.js
20 | │ │ ├── controller 接口控制器
21 | │ │ │ ├── index.js 包含上传、下载、依赖版本check等接口
22 | │ │ │ └── manage.js 包含列出包与相关管理接口
23 | │ │ ├── dao 私有模块信息
24 | │ │ │ └── index.js
25 | │ │ │ └── packageList.js
26 | │ │ │ └── zkPackageList.js
27 | │ │ └── storage 包存储适配器
28 | │ │ ├── index.js 入口
29 | │ │ ├── localfile.js 对接本地文件系统的包存储
30 | │ │ └── swift.js 对接swift的包存储
31 | │ ├── index.js server启动入口
32 | │ ├── public 包管理界面浏览器代码(html、js、css)
33 | │ └── widget
34 | ├── command 客户端命令
35 | │ ├── clean.js
36 | │ ├── config.js
37 | │ ├── download.js
38 | │ ├── install.js
39 | │ ├── publish.js
40 | │ ├── server.js
41 | │ └── ...
42 | ├── common 公共模块
43 | │ ├── checkUtils.js 检查npm-srhinkwrap.json与package.json一致性工具
44 | │ ├── console.js 打印着色工具
45 | │ ├── constant.js 公共常量
46 | │ ├── f2bConfigUtils.js 执行shell命令工具
47 | │ ├── installUtils.js 安装过程工具
48 | │ ├── manifestUtils.js 解析资源(npm-shrinkwrap.json等)工具
49 | │ ├── npmUtils.js 执行npm命令工具
50 | │ ├── shellUtils.js 执行shell命令工具
51 | │ └── utils.js
52 | ├── lib 外部依赖
53 | │ └── swiftClient 修改过的swift客户端
54 | └── registry 客户端对应的服务端接口
55 | ├── nexus.js 对接nexus服务端(已废弃)
56 | └── node.js 对接nodejs服务端
57 |
58 | # 调试说明
59 |
60 | - -d选项可以打印更加详细的过程信息,可以看到命令执行过程中的更多细节。
61 | - 配置文件会以json存储到~/.npm_cache_share_config.json,你可以查看该文件获得配置内容。
62 | - 本地缓存(默认存储到~/.npm_cache_share目录下)中会按照“包名称+版本号+可能出现的环境量(node版本、系统类型等)”的文件夹命名存储每个依赖包,可以直接进去看到每个包的具体内容。
63 |
64 |
65 |
66 | # 技术细节
67 |
68 | - 上传下载中多次采用流处理(即将文件读取流、打包流和上传流串联在一起)来提升效率,依赖了一些流式包fstream和request等。
69 | - 采用了Factory注解做了一些封装,可以理解为对外统一的interface,对内实现各种对接的adapter,从功能上抽象出了一个实体。
70 | - 在命令解析上做了一些trick,将config配置内的内容在命令解析时直接混入了命令的附加选项上(即默认附加选项是配置文件里面的配置)。所以要求所有命令不能有冲突(相同全名)的附加选项。
71 | - server和client均可以使用config命令进行永久性配置,用clean命令清除本地缓存(对server来说是localfile的storage)。
72 |
73 | ## server细节
74 |
75 | server用ncs server命令启动(分为直接fork和调用PM2启动两种方式),由于采用swift仓库的话需要统一管理目录缓存所以仅能单进程启动。
76 |
77 | ### storage
78 | storage用Factory注解抽象出了一层适配层(/app/code/storage/index.js),定义了有关包操作的一些接口,在具体的每个适配器实现中实现具体接口(/app/code/storage/localfile.js和/app/code/storage/swift.js)。
79 |
80 | storage存储部分目前实现了两种,localfile是直接使用服务器本地的文件系统存储包,swift使用ops统一维护的swift仓库存储包。
81 |
82 | 由于本地存储的包的下载并发存在瓶颈,并且为了便于分布式部署,目前均采用swift方式存储包。
83 |
84 | cache部分是一个包的目录缓存,在每次服务启动时会从swift同步一份当前包的列表,在每次有包上传是更新当前列表。这个目录列表用于判断哪些包是中央缓存所存在的,以及判断包的类型(是node-gyp编译包还是无需编译的包)。
85 |
86 | ### dao
87 |
88 | dao层在本地用json文件简单的存储了私有包(即由用户手动publish的包)的相关信息,包括包的名称、是否需要每次同步以及包的密钥。
89 |
90 | 发布一个私有包时会配置它是否需要每次同步(即每次安装均从中央缓存下载而不使用本地缓存,主要用于频繁改动而不升级版本的开发阶段以及非功能改动的bug修复)。同时会配置或校验它的密钥(指定包在初次上传时会需要配置密钥,之后的每次覆盖上传需要带上这个密钥,防止无关人员误操作覆盖)。
91 |
92 | ### controller
93 |
94 | controller是中央服务队外提供的所有接口api和界面管理地址。
95 |
96 | index中是服务基础的上传、下载、校验接口。manage中是管理界面(可用查看当前服务缓存的包目录与包信息)。均适用controller和requestMapping注解完成。
97 |
98 |
99 |
100 | ## client细节
101 |
102 | client的功能包括安装(install)、发布包(publish)、上传/下载静态资源(upload/download)。
103 |
104 | ### install
105 |
106 | 安装流程是整个工程的核心。分为以下几步:
107 |
108 | - 判断参数,不带参数认为是初始化安装,参数为带版本的包名称则直接按之后的安装流程安装该包,参数为不带版本的包名称则先用npm view获取最新版本号进行安装。
109 | - 判断项目当前状态,存在依赖固化文件(npm-shrinkwrap.json或者yarn.lock)则可以取到每个包所需求的版本,进行之后的安装。否则直接调用npm install。
110 | - 安装。至此,所有待安装的包都获取到了待安装的版本号,全称通过请求server端得到(包含包名、版本号、需要node-gyp编译的还有node版本、系统类型等环境量)。
111 | - 判断是否是需要每次同步的包,如是跳过取本地缓存。如果不是,且本地缓存有则跳过下载、安装。
112 | - 判断中央服务是否有,有则下载,跳过本地安装。
113 | - 本地通过npm install 批量安装缺失的包(仅会发生在初次安装中的少量新增包)并上传这些包到中央缓存。
114 | - 将所有安装/下载完的包按照依赖固化文件的树形结构复制到当前项目目录下。
115 | - 收尾。安装单个包会自动追加入package.json并自动执行npm shrinkwrap重写依赖固化文件。
116 |
117 | ### publish
118 |
119 | 发布当前目录为一个私有包(类似npm的publish,但是不会上传到任何npm仓库,可以覆盖同名的npm包)。
120 |
121 | ### upload/download
122 |
123 | 上传/下载一个静态资源,其不会关联到中央缓存服务,直接与一个特定的静态资源swift仓库进行交互。初步用于前端资源版本号文件与前端资源模版文件等需要用于前后端关联的静态资源文件的跨工程关联与发布。
124 |
125 | ### qupload/qdownload
126 | 根据package.json的描述来打包上传静态资源,作用同 upload/download。
127 |
--------------------------------------------------------------------------------
/docs/config.md:
--------------------------------------------------------------------------------
1 | # 配置文件
2 | 该文件里的配置项会影响指令的默认参数,也可以通过指令执行的时候增加参数来覆盖,所以下面的配置都是可选配置。配置文件会优先保存到环境变量 NPM_CACHE_DIR 所指定的路径(同级目录)下,文件名为 `.npm_cache_share_config.json`,如果没有设置该环境变量,则会保存到用户目录下。
3 |
4 | ## Client端
5 | ```json
6 | {
7 | "type": "node",
8 | "repository": "中央缓存服务地址,格式为 host:port/release容器名称-snapshot容器名称",//如果没有写snapshot容器名称,则release和snapshot容器同名
9 | "token": "中央缓存服务的token,权限使用",
10 | "registry": "指定npm源,可以为内部源",
11 | "npmPlatBinds": { "darwin": [ "fsevents" ] },
12 | "resourceSwift": "指定静态资源上传下载RELEASE版本的swift源,格式为 host|user|pass",
13 | "resourceSnapshotSwift": "指定静态资源上传下载SNAPSHOT版本的swift源,格式为 host|user|pass",
14 | "compressType": "指定静态资源的压缩方式",
15 | "auto": "上传下载使用project或key来做容器名",
16 | "ceph": "如果仓库是ceph,值需要为true,虽然兼容swift,但资源的链接有所不一样",
17 | "nameReg": "package.json中name的规则,只有在publish和qupload中有效"
18 | }
19 |
20 | ```
21 | - npmPlatBinds为在某个环境下才会进行安装的模块
22 | - 需要注意模块发布,swift的配置在Server端设置
23 |
24 | ## Server端
25 | ```json
26 | {
27 | "port": "指定服务的端口",
28 | "storage": "swift",
29 | "storageConfig": "指定模块(组件)RELEASE版本的swift源,格式为 host|user|pass",
30 | "storageSnapshotConfig": "指定模块(组件)SNAPSHOT版本的swift源,格式为 host|user|pass",
31 | "zookeeper": "启用zookeeper服务,格式为 host:port",
32 | "zkRoot": "zookeeper上根节点路径的名称",
33 | "ceph": "如果仓库是ceph,值需要为true,虽然兼容swift,但资源的链接有所不一样",
34 | "swiftTokenTimeout": "swift的实效性,比如24小时,以毫秒记录",
35 | }
36 |
37 | ```
38 |
--------------------------------------------------------------------------------
/docs/history.md:
--------------------------------------------------------------------------------
1 | # 历史版本
2 | 1.1.16
3 | - 安装node_modules下bin路径为相对路径
4 |
5 | 1.1.15
6 | - 解决没有忽略package.json不存在的情况
7 |
8 | 1.1.14
9 | - 解决yarn.lock出现重复版本模块的场景,package.json通过链接的方式描述模块和依赖中存在同样版本模块的引用(通过semver来引用)
10 |
11 | 1.1.13
12 | - 解决yarn.lock解析循环问题
13 |
14 | 1.1.12
15 | - 删除解析yarn.lock时输出结构
16 |
17 | 1.1.11
18 | - 解决上传失败问题
19 |
20 | 1.1.10
21 | - 解决release和snapshot容器不一样的场景
22 |
23 | 1.1.9
24 | - 解决unzip没有回调问题
25 | - 兼容在ceph存储下资源链接处理
26 | - 解决swift容器下对象最大10000的问题
27 |
28 | 1.1.8
29 | - 兼容npm-shrinkwrap.json中存在不带version的模块描述
30 |
31 | 1.1.7
32 | - 修复qdonwload和download,在仓库是ceph时获取资源失败的问题
33 |
34 | 1.1.6
35 | - 支持固化版本中非x.x.x格式的模块版本安装,会通过安装得到具体版本的缓存,后续如果切成版本的方式安装会读取该版本,而不会直接安装,可以通过策略配置来强制安装
36 | - 支持ceph
37 |
38 | 1.1.5
39 | - node8以下版本安装会卡住(loadRequestedDeps时),修改npm-shrinkwrap.json文件
40 |
41 | 1.1.4
42 | - swift token 认证频繁修复
43 | - 修复当zookeeper节点非常多时导致UnhandledPromiseRejectionWarning错误
44 | - 修复snapshot版本执行安装问题
45 |
46 | 1.1.3
47 | - 安装单个私有模块时,package.json不记录url,只记录version,对于没有同步到内部npm源的,用npm安装会失败
48 | - zk支持根节点配置
49 | - swift连接授权token失效重连获取token机制,增加swiftTokenTimeout设置
50 | - 兼容git+ssh://git@....#xxx格式,取#后面为版本信息
51 |
52 | 1.1.2
53 | - 解决async.everySeries导致安装缺失
54 |
55 | 1.1.1
56 | - 修复解析yarn.lock时只生成两个层级问题
57 | - 解决qdownload多个依赖下载问题
58 |
59 | 1.1.0
60 | - 升级node-annotation,废弃async,以及对fibers的依赖
61 |
62 | 1.0.57
63 | - 解决单模块安装会导致模块被删
64 |
65 | 1.0.56
66 | - 升级fibers版本,支持node7以上版本
67 |
68 | 1.0.54
69 | - 增加模块黑名单,如果存在则终止安装
70 |
71 | 1.0.53
72 | - 支持模块package.json里的bin在工程目录中可执行
73 |
74 | 1.0.52
75 | - 可强制指定npm
76 |
77 | 1.0.51
78 | - publish支持版本重命名
79 | - 暴露获取模块最新版本接口
80 |
81 | 1.0.50
82 | - gyp模块snapshot包模块支持
83 | - 包发布自动检测是否要进行gyp编译
84 | - 支持参数checkSnapshotDeps参数,安装时检查是否有依赖SNAPSHOT模块
85 |
86 | 1.0.49
87 | - 日志时间纠正
88 |
89 | 1.0.48
90 | - 解决node0.12.7安装时出现ls: no such file or directory的问题
91 |
92 | 1.0.47
93 | - fix npm-shrinkwrap.json里源统一成淘宝源
94 |
95 | 1.0.46
96 | - 增加日志信息
97 |
98 | 1.0.45
99 | - 单台机器多个进程同时安装时安装目录进行隔离
100 |
101 | 1.0.44
102 | - 修复单独安装模块时会误删工程node_modules
103 | - qupload上传时自动获取时version为文件名,不再和project拼装
104 | - 修复zk模式下,同步触发失效问题以及非版本模块的支持
105 |
106 | 1.0.43
107 | - 解决启用zookeeper时,zkCache加载有时会丢失的问题
108 |
109 | 1.0.42
110 | - 回滚npm5.3.0关于npm-shrinkwrap.json对require的支持,理解有出入
111 |
112 | 1.0.41
113 | - 支持npm5.3.0 npm-shrinkwrap.json格式,requires的描述
114 |
115 | 1.0.40
116 | - read-package-json版本2.0.11依赖json-parse-better-errors,会导致node低版本语法错误
117 |
118 | 1.0.39 需要同时更新Client和Server
119 | - 兼容npm5中无版本号的问题
120 | - 安装遇到getaddrinfo时结束安装
121 | - 模块策略管理,当前支持三种策略
122 | * 强制安装(ignoreCache): 会忽略本地缓存以及公共缓存,每次都会重新安装
123 | * 忽略本地缓存(alwaysUpdate): 会忽略本地缓存,每次都从公共缓存获取,比如SNAPSHOT版本默认会采用这种方式
124 | * 安装后执行(postInstall): 会在安装完成之后,执行对应模块package.json里scripts配置的脚本,比如postinstall,多个值以逗号分隔
125 |
126 | 1.0.38
127 | - 修复npm-shrinkwrap中模块值为空对象的情景
128 | - 修复yarn.lock中dependencies模块带双引号的情景
129 |
130 | 1.0.37
131 | - 解决服务端查看包大小出错问题
132 | - 解决qupload无法上传问题
133 |
134 | 1.0.36
135 | - 支持npm5生成npm-shrinkwrap.json中version的格式
136 |
137 | 1.0.35
138 | - 修复服务端缓存与swift不同步的问题
139 |
140 | 1.0.34
141 | - repository对于带协议头的支持
142 | - server端支持zookeeper,解决多进程同步的问题
143 |
144 | 1.0.33
145 |
146 | - 增加config配置说明
147 | - 命名约束只针对package.json中name属性,不再限制前后端关联的场景
148 | - 解决公共缓存服务和swift源不一致问题,优化下载
149 |
150 | 1.0.32
151 |
152 | - 针对包发布、前后端关联增加对工程命名的约束
153 | - download和qdownload不需要权限限制,直接下载
154 | - 增加snapshot和release的区分
155 |
156 | 1.0.31
157 |
158 | - 增加全局错误捕获,错误退出码为1
159 | - qdonwload时文件类型识别错误
160 |
161 | 1.0.30
162 |
163 | - 增加文件名后缀
164 | - 废除上传下载里notTar参数,支持zip压缩
165 | - 单文件上传时也会压缩
166 |
167 | 1.0.29
168 |
169 | - 被快速过度了,忽略
170 |
171 | 1.0.28
172 |
173 | - 修复install时读取yarn.lock文件时丢失尾部依赖
174 |
175 | 1.0.27
176 |
177 | - config指令支持指定配置文件
178 | - download指令对带/的文件名下载能生成对应目录路径
179 |
180 | 1.0.26
181 |
182 | - 可指定配置文件
183 | - qupload和qdownload文件名拼接为name + "/" + version
184 | - 支持单个文件上传和下载
185 |
186 | 1.0.25
187 |
188 | - fix bug for command annotation
189 |
190 | 1.0.24
191 |
192 | - 补充文档
193 |
194 | 1.0.23
195 |
196 | - 增加swift便捷操作
197 |
198 | 1.0.22
199 |
200 | - qupload和qdownload支持auto参数
201 |
202 | 1.0.21
203 |
204 | - qupload和qdownload支持根据配置信息动态创建swift的container
205 |
206 | 1.0.20
207 |
208 | - 新增前后端资源关联,前端上传qupload,后端下载qdownload
209 |
210 | 1.0.19
211 |
212 | - upload功能重命名为publish
213 |
214 | - 追加上传下载静态资源(从swfit)的功能:upload/download
215 |
216 | 1.0.18
217 |
218 | - 追加upload功能,可以本地直接上传一个私有包到中央缓存
219 |
220 | - 私有包可以使用alwasySync(相当于snapshot)功能,每次安装都会去中央缓存上最新代码
221 |
222 | - 优化了安装单个模块的流程
223 |
224 | 1.0.17
225 |
226 | - fix
227 |
228 | 1.0.16
229 |
230 | - 安装时使用指定npm路径,参数-n或者--npm
231 |
232 | 1.0.15
233 |
234 | - 增加快捷指令ncs
235 |
236 | 1.0.14
237 |
238 | - 添加安装前置的对npm-shrinkwrap.json和package.json一致性的校验
239 |
240 | 1.0.13
241 |
242 | - 添加本地开发与对yarn.lock的支持
243 |
244 | 1.0.12
245 |
246 | - 修复拷贝隐藏文件的问题
247 |
248 | 1.0.11
249 |
250 | - 修复强依赖平台的包的过滤
251 |
--------------------------------------------------------------------------------
/docs/linkFront2Backend.md:
--------------------------------------------------------------------------------
1 | # 前后端关联
2 | 在现有的团队协作模式中,前后端工程一般都会分开管理,便于不同开发角色开发和维护,由于游览器有缓存机制,所以会通过版本来控制更新,也就会涉及到前端生成的版本号如何和后端进行关联(当然可以不仅限于版本信息),这里通过swift分布式存储来作为存储服务,区别于包发布(publish指令,走公共缓存服务来上传至存储服务),直接通过client直接上传至swift,不走公共缓存服务,也不走本地缓存。
3 |
4 | # SNAPSHOT
5 | 如果f2b下的version属性里含有 **SNAPSHOT** 字样的都将被作为快照版本对待,上传始终会覆盖服务器上的。可通过resourceSnapshotSwift变量指定swift信息,格式为 **host|user:user|pass**,同resourceSwift。
6 |
7 | # 指令
8 | - qupload 根据配置信息进行压缩上传,区别于upload指令(只做上传操作),swift的配置信息可以通过指令参数指定,也可以在config文件中通过resourceSwift变量指定。
9 | >Usage: qupload|qu [options]
10 | >
11 | >Upload a static source to repository by package.json, you can set the swift setting by parameters or use command 'ncs config set resourceSwift "host|user|pass|container"'
12 | >
13 | >Options:
14 | >
15 | >+ -h, --host [host] host of swift
16 | >+ -u, --user [user] user of swift
17 | >+ -p, --pass [pass] pass of swift
18 | >+ -c, --container [container] container in swift
19 | >+ -f, --forceUpdate if exists, module on the server will be overrided
20 | >+ -a, --auto create container automatically according to package.json,use project parameter in f2b, it will ignore container parameter in command
21 | ** 如果设定了auto参数,会忽略指令的container参数以及resourceSwift中container的配置,会根据package.json里f2b的project属性值来动态创建container **
22 |
23 | - qdownload 根据配置信息进行下载,区别于download指令(只做下载操作),swift的配置信息同qupload。
24 | >Usage: qdownload|qu [options]
25 | >
26 | >Download a static source to repository, you can set the swift setting by parameters or use command 'ncs config set resourceSwift "host|user|pass|container"'
27 | >
28 | >Options:
29 | >
30 | >+ -h, --host [host] host of swift
31 | >+ -u, --user [user] user of swift
32 | >+ -p, --pass [pass] pass of swift
33 | >+ -c, --container [container] container in swift
34 | >+ -a, --auto according to package.json,use project parameter in f2b to set the value of container, it will ignore container parameter in command
35 | ** 如果设定了auto参数,会忽略指令的container参数以及resourceSwift中container的配置,会将package.json里的f2b下的key值作为container来下载对象,所有上传资源都会进行压缩 **
36 |
37 | # 实现
38 | 读取工程根目录下package.json文件,并根据配置进行资源压缩上传,文件名为 **project + @@@ + version**, 后端可通过此文件名来获取资源。如果指定-a参数,则文件名为 **version**。
39 |
40 | ## for frontend
41 |
42 | > in package.json
43 | >
44 | > f2b
45 | >
46 | >> project:项目名称,不存在会取package.json里的name属性
47 | >> version:版本号
48 | >> path:相对于工程目录的待上传目录
49 | >> type: 压缩类型,目前支持zip和tar
50 |
51 | ```json
52 | example package.json
53 | {
54 | ...
55 | "f2b": {
56 | "project": "sample_project",
57 | "version": "btag-1234567",
58 | "path": "./ver",
59 | "type": "zip"
60 | }
61 | ...
62 | }
63 |
64 | ```
65 |
66 |
67 |
68 | ## for backend
69 |
70 | > in package.json
71 | >
72 | > f2b
73 | >
74 | >> key 需要的前端工程名(会和version拼接)
75 | >>> version:前端版本号
76 | >>> path:下载的前端资源需要存放的位置
77 | >>> type: 压缩类型,目前支持zip和tar
78 |
79 |
80 | ```json
81 | {
82 | ...
83 | "f2b": {
84 | "sample_project": {
85 | "version": "btag-1234567",
86 | "path": "./static/",
87 | "type": "zip"
88 | },
89 | "another_sample_project": {
90 | "version": "btag-1234567",
91 | "path": "./",
92 | "type": "tar"
93 | }
94 | }
95 | }
96 | ```
97 | # 注意
98 | > 压缩文件命名时不要以.properties结尾,会导致目录追踪至顶级。
99 |
--------------------------------------------------------------------------------
/docs/modules.md:
--------------------------------------------------------------------------------
1 | # 包发布与使用
2 |
3 | # 发布
4 | 通过ncs publish会将当前工程进行打包,上传至中央服务,中央服务再存储至仓库,当前仓库支持swift和localfile,推荐使用swift,不会和npm源进行同步,所以这些包只能通过ncs来进行安装。
5 |
6 | 此指令一般针对包工程,在工程根目录下执行该命令,会将该工程打包,并通过中央服务上传至仓库。推荐在发布前执行npm shrinkwrap固化版本,我们会按照npm-shrinkwrap.json文件描述在该包下安装依赖包。
7 |
8 | # 文件忽略
9 | 可以在.ignore或者.gitignore里指定需要忽略的目录和文件。
10 |
11 | # SNAPSHOT
12 | 在package.json里version属性里含有 **SNAPSHOT** 字样的都将被作为快照版本对待,可在配置文件里配置分隔符(SNAPSHOTLINKE属性),默认为-。行为描述如下:
13 | - 发布:始终会覆盖同名模块
14 | - 安装:始终会从服务器端更新,忽略本地缓存
15 | 注意:包发布是通过公共缓存服务来上传的,所以SNAPSHOT和RELEASE的配置由服务端来决定,请参见[公共服务](./server.md)
16 |
17 | # 安装
18 | 在初次新增依赖时,通过ncs install (包名称)[@包版本];或者在相关依赖固化之后,直接使用ncs install,都会优先安装通过ncs publish发布到仓库的包。如果未找到,才会尝试从npm源安装。
19 |
20 | # 指令
21 | - publish 发布包, 公共服务需要启动,参见[公共服务](./server.md)。
22 |
23 | >Usage: publish|p [options]
24 | >
25 | > Publish a dir as a package to center cache server
26 | >
27 | > Options:
28 | >
29 | >- -c, --type [type] server type node/npm, default is node
30 | >- -e, --repository [repository] specify the repository, format as HOST:PORT/REPOSITORY-NAME
31 | >- -t, --token [token] use the token to access the npm_cache_share server
32 | >- -p, --password [password] use the password to access certain package
33 | >- -r, --registry [registry] specify the npm registry
34 | >- -s, --snapshot specify this is a snapshot version
35 | >- -v, --moduleVersion [moduleVersion] specify the module version
36 | >- -u, --alwaysUpdate this module will publish overwrite the same version on the server, and will always update when install, if -s not specify, the version remain unchanged"],
37 | >- -o, --overwrite if -s exist, it will overwrite the version into package.json"
38 | >- --checkSnapshotDeps check if or not dependend on the snapshot module, default is ignore check
39 |
40 |
41 | # 参数额外说明
42 | - repository参数包含两部分,一部分是服务域名和端口,另一部分是repository name,对于swift就是container,对于localfile就是文件名
43 | - token参数是中央公共服务的权限认证,简单的值比较
44 | - dependOnEnv参数指明该包是否需要node-gyp编译,如果是的话,包名的组成会和环境相关
45 | - cancelAlwaysSync参数会忽略本地缓存,始终从中央公共服务获取
46 |
47 | # 注意
48 | 作为同名包我们会以npm_cache_share中的为主,没有才会从npm源上进行安装。
49 |
--------------------------------------------------------------------------------
/docs/projecName.md:
--------------------------------------------------------------------------------
1 | # 工程命名
2 | 由于模块的发布是以package.json里的name属性来区分的,前后端都有各自的模块,很可能会存在命名冲突的问题,所以在工程命名的时候可以增加区分的纬度,通过正则表达来校验命名的合法性,如果不存在该规则,则忽略命名校验。
3 |
4 | # 配置
5 | 在配置文件中配置nameReg属性值来指定,该值为正则表达。可以通过 `ncs config set nameReg "正则规则"`
6 |
7 | # 发生场景
8 | - 包发布和安装
9 |
--------------------------------------------------------------------------------
/docs/server.md:
--------------------------------------------------------------------------------
1 | # 中央公共服务
2 | 在安装模块的时候,一般在本地机器上会存有模块的缓存,但这样缓存信息只能单台机器享有,为了使多台机器共享模块缓存,需要搭建中央公共服务来提供服务,该服务还涉及权限认证,避免普通用户修改影响到公共资源。
3 |
4 | # SNAPSHOT
5 | 启动的时候默认会有SNAPSHOT和RELEASE的区分,可通过配置storageSnapshotConfig和storageConfig来分别指定,当storageSnapshotConfig不存在时,会默认等同于storageConfigs。
6 |
7 | # 权限
8 | 避免任何人都能修改缓存信息,增加了token校验,如果设置了token参数,在更新的时候会校验请求头里token的值。
9 |
10 | # zookeeper
11 | zookeeper是一个分布式应用程序协调服务,可以在配置文件里配置zookeeper对应的服务地址来开启,格式为 * host:port * , 如果涉及到多进程或者多机部署时一定要设置,否则模块状态会有问题。
12 |
13 | # 指令
14 | > Usage: server|s [options] [command] [name]
15 | >
16 | > Start a server to store the npm module cache, command is for pm2, like start、stop、restart,and so on, name mean the name of application for pm2.
17 | >
18 | > Options:
19 | >
20 | >- -s, --storage [storage] specify the type of storage, could be localfile or swift
21 | >- -c, --storageConfig [storageConfig] specify the config of storage, serveral arguments joined with '|', the format of swift is 'host|user|pass', localfile is 'cache path'"
22 | >- -p, --port [port] specify the port of the service, default is 8888
23 | >- -f, --useFork start with fork
24 | >- -t, --token [token] control the auth to access the server
25 | >- -i, --i [i] thread count only for pm2
26 | >- -n --name [name] app name only for pm2
27 |
28 | ## 注意
29 | - 如果storage选择swift,只能启动单机单进程,由于要记录swift的信息,并维护ncs操作的更新,需要通过zookeeper来保证多进程或者多机之间模块信息的同步,所以需要开启zookeeper
30 | - 服务启动请使用sudo权限
31 |
32 | # 策略控制台
33 | ## /strategy 访问路径
34 | ## 策略分类
35 | - 强制安装(ignoreCache): 会忽略本地缓存以及公共缓存,每次都会重新安装
36 | - 忽略本地缓存(alwaysUpdate): 会忽略本地缓存,每次都从公共缓存获取,比如SNAPSHOT版本默认会采用这种方式
37 | - 安装后执行(postInstall): 会在安装完成之后,执行对应模块package.json里scripts配置的脚本,比如postinstall,多个值以逗号分隔
--------------------------------------------------------------------------------
/docs/swift.md:
--------------------------------------------------------------------------------
1 | # Swift
2 | Swift 最初是由 Rackspace 公司开发的高可用分布式对象存储服务 (非水果公司编程语言),并于 2010 年贡献给 OpenStack 开源社区作为其最初的核心子项目之一,为其 Nova 子项目提供虚机镜像存储服务。Swift 构筑在比较便宜的标准硬件存储基础设施之上,无需采用 RAID(磁盘冗余阵列),通过在软件层面引入一致性散列技术和数据冗余性,牺牲一定程度的数据一致性来达到高可用性和可伸缩性,支持多租户模式、容器和对象读写操作,适合解决互联网的应用场景下非结构化数据存储问题,组织结构为 ** Account/Container/Object ** ,其他swift相关知识大家可以去问度娘。
3 |
--------------------------------------------------------------------------------
/docs/version.md:
--------------------------------------------------------------------------------
1 | # SNAPSHOT和RELEASE
2 | ## 背景
3 | 不同环境场景下是否要更新包(模块)的要求不一样,像开发环境下,模块的频繁修改发布是存在的,如果仅仅是通过改变版本号来控制更新就显得非常繁琐,故有
4 |
5 | - SNAPSHOT版本,始终获取当前版本下最新的SNAPSHOT版本,格式为 ** module + version + 分隔符 + SNAPSHOT ** ,上传和发布操作都将覆盖同名版本,下载和安装操作都将始终从服务器更新
6 | - RELEASE版本,顾名思义就是正式发布的版本,只能通过变更版本号才会更新,格式为 ** module + version ** ,也可通过指令中的参数来强制更新服务器上的当前版本,具体参见具体使用指令
7 |
8 | ## 配置
9 |
10 | - 分隔符:可以在配置文件里配置 SNAPSHOTLINKE 来指定,默认为-,仅在 publish 指令中生效
11 |
12 | ## 配置
13 | 目前涉及SNAPSHOT和RELEASE的共有两组参数:
14 |
15 | - storageSnapshotConfig和storageConfig,该组参数server指令里会使用,install和publish指令会受影响,主要是模块发布和安装时会受影响
16 | - resourceSnapshotSwift和resourceSwift,该组参数upload、download、qupload、qdownload里会使用,主要影响静态资源的上传和下载
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "npm_cache_share",
3 | "version": "1.1.16",
4 | "description": "npm cache by npm-shrinkwrap.json",
5 | "license": "MIT",
6 | "main": "index.js",
7 | "bin": {
8 | "npm_cache_share": "./bin/client",
9 | "ncs": "./bin/client"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/Robinlim/npm_cache_share.git"
14 | },
15 | "keywords": [
16 | "npm cache",
17 | "npm_cache",
18 | "npm-cache",
19 | "module cache"
20 | ],
21 | "author": {
22 | "name": "robin",
23 | "email": "robinlim9@aliyun.com"
24 | },
25 | "engines": [
26 | "node"
27 | ],
28 | "bugs": {
29 | "url": "https://github.com/Robinlim/npm_cache_share/issues",
30 | "email": "robinlim9@aliyun.com"
31 | },
32 | "dependencies": {
33 | "ansi-styles": "^2.2.1",
34 | "archiver": "^1.3.0",
35 | "async": "^2.0.1",
36 | "body-parser": "~1.15.2",
37 | "commander": "^2.9.0",
38 | "compression": "~1.5.0",
39 | "cookie-parser": "~1.4.3",
40 | "ejs": "^2.5.7",
41 | "escape-html": "^1.0.3",
42 | "express": "~4.13.3",
43 | "fs-extra": "^0.30.0",
44 | "fstream": "^1.0.10",
45 | "fstream-ignore": "^1.0.5",
46 | "json-parse-helpfulerror": "^1.0.3",
47 | "lodash": "^4.14.2",
48 | "multiparty": "^4.1.2",
49 | "node-annotation": "^1.0.0",
50 | "node-zookeeper-client": "^0.2.2",
51 | "os-homedir": "^1.0.2",
52 | "read-package-json": "^2.0.4",
53 | "read-package-tree": "^5.1.5",
54 | "request": "^2.74.0",
55 | "semver": "^5.3.0",
56 | "serve-favicon": "~2.3.0",
57 | "shelljs": "^0.7.3",
58 | "tar": "^2.2.1",
59 | "unzipper": "^0.8.8"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/js/annotation/Command.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 执行指令
3 | * @Author: robin
4 | * @Date: 2016-08-08 17:29:59
5 | * @Email: xin.lin@qunar.com
6 | * @Last modified by: robin
7 | * @Last modified time: 2016-09-14 18:48:06
8 | */
9 |
10 | 'use strict';
11 | var _ = require('lodash'),
12 | path = require("path"),
13 | fsExtra = require("fs-extra"),
14 | utils = require('../common/utils');
15 |
16 | var programe = require("commander")
17 | .version(fsExtra.readJsonSync(path.resolve(__dirname,'../../../package.json')).version || '0.0.1')
18 | .usage(' [options]');
19 |
20 | var config = fsExtra.readJsonSync(utils.getConfigPath());
21 |
22 | /*@AutoLoad*/
23 | var Command = module.exports = require('node-annotation').Annotation.extend({
24 | /**
25 | * compile the model
26 | * @param {[Model]} model [annotation data]
27 | * @return
28 | */
29 | compile: function(model) {
30 | var ops = model.po();
31 | if(!ops.name){
32 | return;
33 | }
34 | var cmd = programe
35 | .command(ops.name)
36 | .usage(ops.usage)
37 | .alias(ops.alias)
38 | .description(ops.des)
39 | .option('-d, --debug', 'print all information for debug')
40 | .option('-g, --config [config]', 'use specific config path')
41 | .action(function(){
42 | try{
43 | var opts = arguments[arguments.length-1];
44 | // 设置全局debug选项
45 | if(opts && opts.debug){
46 | global.DEBUG = true;
47 | console.debug('In debug mode, will print all information for debug');
48 | }
49 | opts = filter(opts);
50 | var instance = model.instance();
51 | if(opts.config){
52 | config = fsExtra.readJsonSync(path.resolve(opts.config));
53 | }
54 | arguments[arguments.length-1] = _.extend(config, opts);
55 | instance[model.vo()].apply(instance, arguments);
56 | }catch(err){
57 | if(err){
58 | console.error(err.stack || err);
59 | process.exit(1);
60 | }
61 | }
62 | });
63 | (ops.options || []).forEach(function(v){
64 | cmd.option(v[0], v[1], v[2]);
65 | });
66 | //过滤command options的自有属性和方法,会和指令参数冲突
67 | function filter(options){
68 | var opts = {};
69 | _.map(options, function(v, k){
70 | if(k[0] !== '_' && typeof v != 'function'){
71 | opts[k] = v;
72 | }
73 | });
74 | return opts;
75 | }
76 | }
77 | }, {
78 | name: 'Command'
79 | });
80 |
81 | global.run = function() {
82 | // 默认打印帮助信息
83 | if (!process.argv.slice(2).length) {
84 | programe.help();
85 | }
86 | programe.parse(process.argv);
87 | }
88 |
--------------------------------------------------------------------------------
/src/js/annotation/Factory.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 工厂模式
3 | * @Author: robin
4 | * @Date: 2016-09-14 17:29:59
5 | * @Email: xin.lin@qunar.com
6 | * @Last modified by: robin
7 | * @Last modified time: 2016-09-14 19:35:18
8 | */
9 |
10 | 'use strict';
11 | /*@AutoLoad*/
12 | var Factory = module.exports = require('node-annotation').Annotation.extend({
13 | /**
14 | * compile the model
15 | * @param {[Model]} model [annotation data]
16 | * @return
17 | */
18 | compile: function(model) {
19 | Factory.register(model.po(), model);
20 | }
21 | }, {
22 | name: 'Factory',
23 | _items: {},
24 | /**
25 | * 注册
26 | * @param {String} k key
27 | * @param {Object} v value
28 | * @return {void}
29 | */
30 | register: function(k, v) {
31 | this._items[k] = v;
32 | },
33 | /**
34 | * 根据key获取值
35 | * @param {String} k key value
36 | * @param {Object} ops instance option
37 | * @return {Object}
38 | */
39 | instance: function(k, ops){
40 | if(!this._items[k])return null;
41 | var classType = this._items[k].instance();
42 | return new classType(ops);
43 | }
44 | });
45 |
--------------------------------------------------------------------------------
/src/js/annotation/Flow/Done.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 流程结束
3 | * @Author: robin
4 | * @Date: 2016-08-08 17:29:59
5 | * @Email: xin.lin@qunar.com
6 | * @Last modified by: robin
7 | * @Last modified time: 2016-08-09 10:18:55
8 | */
9 |
10 | 'use strict';
11 |
12 | /*@AutoLoad*/
13 | module.exports = require('./Flow').extend({
14 | /**
15 | * business realization
16 | * @return
17 | */
18 | execute: function() {
19 | //do nothing
20 | }
21 | }, {
22 | name: 'Done'
23 | });
24 |
--------------------------------------------------------------------------------
/src/js/annotation/Flow/Flow.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 流程控制
3 | * @Author: robin
4 | * @Date: 2016-08-08 17:29:59
5 | * @Email: xin.lin@qunar.com
6 | * @Last modified by: robin
7 | * @Last modified time: 2016-08-19 17:54:10
8 | */
9 |
10 | 'use strict';
11 | var async = require('async');
12 | var _ = require('lodash');
13 |
14 | /*@AutoLoad()*/
15 | var Flow = module.exports = require('node-annotation').Annotation.extend({
16 | /**
17 | * business realization
18 | * @return
19 | */
20 | execute: function() {
21 | var steps = {},
22 | model,
23 | funct,
24 | dependency,
25 | done = [],
26 | instance = this.model.instance();
27 | //获取流程步骤和结束行为
28 | this.traverse(function(i, item){
29 | model = item.model;
30 | funct = _.bind(instance[model.vo()], instance);
31 | if(model.name() == "Done"){
32 | done.push(funct);
33 | return;
34 | }
35 | if(dependency = model.po()){
36 | if(typeof dependency == 'string'){
37 | dependency = [dependency];
38 | }
39 | dependency.push(funct);
40 | }
41 | steps[model.vo()] = dependency ? dependency : funct;
42 | });
43 | //添加启动方法
44 | instance.start = function(){
45 | async.auto(steps, function(err, results){
46 | _.each(done, function(cb){
47 | cb.call(instance, err, results);
48 | });
49 | });
50 | }
51 | }
52 | }, {
53 | name: 'Flow'
54 | });
55 |
--------------------------------------------------------------------------------
/src/js/annotation/Flow/Step.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 流程步骤
3 | * @Author: robin
4 | * @Date: 2016-08-08 17:29:59
5 | * @Email: xin.lin@qunar.com
6 | * @Last modified by: robin
7 | * @Last modified time: 2016-08-09 14:06:22
8 | * @example
9 | * \/*Flow(["",""])*\/
10 | * \/*Flow("")*\/
11 | */
12 |
13 | 'use strict';
14 |
15 | /*@AutoLoad*/
16 | module.exports = require('./Flow').extend({
17 | /**
18 | * business realization
19 | * @return
20 | */
21 | execute: function() {
22 | //do nothing
23 | }
24 | }, {
25 | name: 'Step'
26 | });
27 |
--------------------------------------------------------------------------------
/src/js/app/app.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: robin
3 | * @Date: 2016-08-17 19:03:55
4 | * @Email: xin.lin@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2016-09-19 15:13:46
7 | */
8 |
9 | var express = require('express');
10 | var path = require('path');
11 | var fs = require('fs');
12 | var favicon = require('serve-favicon');
13 | var cookieParser = require('cookie-parser');
14 | var bodyParser = require('body-parser');
15 | var compression = require('compression');
16 | var utils = require('../common/utils');
17 | // 初始化express
18 | var app = express();
19 | var env = process.env;
20 | // 同步client的debug方法
21 | require('../common/console');
22 | global.DEBUG = env.DEBUG == 'true';
23 |
24 | // view engine setup
25 | app.set('views', path.join(__dirname, 'views'));
26 | app.set('view engine', 'ejs');
27 | app.use(favicon(__dirname + '/favicon.ico'));
28 | app.enable('trust proxy');
29 |
30 | // 让post请求实体增大
31 | app.use(bodyParser.json({limit: '200mb'}));
32 | app.use(bodyParser.urlencoded({limit: '200mb', extended: true, parameterLimit: 20000}));
33 | app.use(cookieParser());
34 | app.use(express.static(path.join(__dirname, 'public')));
35 | app.use(compression());
36 |
37 | // healthcheck
38 | app.get('/healthcheck.html', function(req, res) {
39 | /* 使用healthcheck.html文件,由发布系统通过写入/删除该文件决定上下线 */
40 | var healthcheck = path.join(__dirname, 'healthcheck.html');
41 | try {
42 | fs.accessSync(healthcheck, fs.R_OK);
43 | } catch(e) {
44 | // 不存在或不可读返回404
45 | res.statusCode = 404;
46 | res.end('');
47 | return;
48 | }
49 | // 存在返回healthcheck.html内容
50 | var cont = fs.readFileSync(healthcheck).toString();
51 | res.end(cont);
52 | });
53 |
54 | //初始化缓存
55 | require('./code/cache').ready().then(function(){
56 | //初始化存储
57 | require('./code/storage').init(
58 | env.storage, env.storageConfig,
59 | env.storageSnapshotConfig || env.storageConfig,
60 | env.swiftTokenTimeout,
61 | env);
62 | //加载缓存策略
63 | require('./code/dao').load();
64 | });
65 |
66 | module.exports = app;
67 |
--------------------------------------------------------------------------------
/src/js/app/code/annotation/Factory.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 工厂模式
3 | * @Author: robin
4 | * @Date: 2016-09-14 17:29:59
5 | * @Email: xin.lin@qunar.com
6 | * @Last modified by: robin
7 | * @Last modified time: 2016-09-14 19:35:18
8 | */
9 |
10 | 'use strict';
11 | /*@AutoLoad(1)*/
12 | var Factory = module.exports = require('node-annotation').Annotation.extend({
13 | /**
14 | * compile the model
15 | * @param {[Model]} model [annotation data]
16 | * @return
17 | */
18 | compile: function(model) {
19 | Factory.register(model.po(), model);
20 | }
21 | }, {
22 | name: 'Factory',
23 | _items: {},
24 | /**
25 | * 注册
26 | * @param {String} k key
27 | * @param {Object} v value
28 | * @return {void}
29 | */
30 | register: function(k, v) {
31 | this._items[k] = v;
32 | },
33 | /**
34 | * 根据key获取值
35 | * @param {String} k key value
36 | * @param {Object} ops instance option
37 | * @param {Object} snapshotOpts instance option
38 | * @return {Object}
39 | */
40 | instance: function(k, ops, snapshotOpts){
41 | if(!this._items[k]){
42 | throw new Error('Unrecongnize storage type:'+k);
43 | };
44 | var classType = this._items[k].instance();
45 | return new classType(ops, snapshotOpts);
46 | }
47 | });
48 |
--------------------------------------------------------------------------------
/src/js/app/code/annotation/ResponseBodyDeal.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 响应处理
3 | * User: xin.lin
4 | * Date: 17-8-22
5 | * control the response
6 | */
7 | /*@AutoLoad*/
8 | var na = require('node-annotation');
9 |
10 | module.exports = na.Annotation.extend({
11 | /**
12 | * automatic transfer json to string
13 | * @return {[type]} [description]
14 | */
15 | execute: function() {
16 | var model = this.model,
17 | voParam = model.voParam(),
18 | resIndex;
19 | voParam.some(function(item, i) {
20 | if (item == 'res' || item == 'response') {
21 | resIndex = i;
22 | return true;
23 | }
24 | });
25 |
26 | model.exports().addMethodInterceptor(model.vo(), na.PROXYCYCLE.BEFORE, function() {
27 | if (typeof resIndex !== 'undefined') {
28 | var res = arguments[resIndex],
29 | old = res.end;
30 | res.writeHead(200, {'Content-Type': 'application/json;charset=UTF-8'});
31 | res.end = function() {
32 | if (typeof arguments[0] == 'object') {
33 | arguments[0] = JSON.stringify(arguments[0]);
34 | }
35 | old.apply(res, arguments);
36 | };
37 | }
38 | });
39 | },
40 | /**
41 | * compile the model
42 | * @param {[Model]} model [annotation data]
43 | * @return {[type]} [description]
44 | */
45 | compile: function(model) {
46 | model.exports();
47 | }
48 | }, {
49 | //annotation name
50 | name: "ResponseBodyDeal"
51 | });
52 |
--------------------------------------------------------------------------------
/src/js/app/code/cache/cache.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: wyw.wang
3 | * @Date: 2016-10-14 14:07
4 | * @Email: wyw.wang@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2017-05-08 10:37
7 | */
8 |
9 |
10 |
11 | var _ = require('lodash'),
12 | utils = require('../../../common/utils'),
13 | CACHESTRATEGY = require('../../../common/constant').CACHESTRATEGY;
14 |
15 | /**
16 | * 缓存所有仓库和包的索引信息
17 | * @type {Object}
18 | */
19 | function Cache(){
20 | this._cache = {};
21 | this._snapshotCache = {};
22 | this.storage = null;
23 |
24 | }
25 |
26 | Cache.prototype = {
27 | /**
28 | * 缓存就绪后执行
29 | * @return {Promise}
30 | */
31 | ready: function(){
32 | return new Promise(function(resolve, reject){
33 | resolve();
34 | });
35 | },
36 | /**
37 | * RELEASE和SNAPSHOT是一致的
38 | * @return {[type]} [description]
39 | */
40 | same: function(){
41 | //因为zkCache优先渲染RELEASE,再渲染SNAPSHOT
42 | this._snapshotCache = this._cache;
43 | },
44 | /**
45 | * 清空缓存
46 | * @return {[type]} [description]
47 | */
48 | clear: function(){
49 | console.info('清除本地缓存');
50 | this._cache = {};
51 | this._snapshotCache = {};
52 | return new Promise(function(resolve, reject){
53 | resolve();
54 | });
55 | },
56 | /**
57 | * 增加仓库
58 | * @param {Boolean} isSnapshot 是否是snapshot
59 | * @param {String} name 仓库名称
60 | * @param {Object} stat 仓库状态
61 | */
62 | addRepository: function(isSnapshot, name, stat){
63 | var cache = this.listAll(isSnapshot);
64 | if(cache[name]){
65 | cache[name].stat = stat;
66 | return;
67 | }
68 | cache[name] = {
69 | name: name,
70 | stat: stat,
71 | modules: {}
72 | }
73 | },
74 | /**
75 | * 删除仓库
76 | * @param {Boolean} isSnapshot 是否是snapshot
77 | * @param {String} name 仓库名称
78 | * @return {boolean} 是否删除成功
79 | */
80 | delRepository: function(isSnapshot, name) {
81 | if(isSnapshot){
82 | return delete this._snapshotCache[name];
83 | }
84 | return delete this._cache[name];
85 | },
86 | /**
87 | * 追加包到仓库
88 | * @param {Boolean} isSnapshot 是否是snapshot
89 | * @param {String} repository 仓库名称
90 | * @param {String} name 包名称,形如“five@0.0.1”
91 | */
92 | addPackage: function(isSnapshot, repository, name){
93 | var cache = this.listAll(isSnapshot);
94 | if(!cache[repository]){
95 | return false;
96 | }
97 | var modules = cache[repository].modules,
98 | moduleName = utils.isModuleVersion(name) ? utils.splitModuleName(name) : name;
99 | if(!modules[moduleName]){
100 | modules[moduleName] = [];
101 | }
102 | if(modules[moduleName].indexOf(name) < 0){
103 | modules[moduleName].push(name);
104 | }
105 | return true;
106 | },
107 | /**
108 | * 从仓库中删除包
109 | * @param {Boolean} isSnapshot 是否是snapshot
110 | * @param {String} repository 仓库名称
111 | * @param {String} name 包名称
112 | * @return {boolean} 是否删除成功
113 | */
114 | delPackage: function(isSnapshot, repository, name) {
115 | var cache = this.listAll(isSnapshot);
116 | if(!cache[repository]){
117 | return false;
118 | }
119 | var modules = cache[repository].modules;
120 | if(utils.isModuleVersion(name)){
121 | var moduleName = utils.splitModuleName(name),
122 | index;
123 | if(modules[moduleName]
124 | && (index = modules[moduleName].indexOf(name)) > -1){
125 | modules[moduleName].splice(index,1);
126 | if(modules[moduleName].length === 0){
127 | modules[moduleName] = null;
128 | delete modules[moduleName];
129 | }
130 | }
131 | }else{
132 | modules[name] = null;
133 | delete modules[name];
134 | }
135 | },
136 | /**
137 | * 从仓库中删除模块
138 | * @param {Boolean} isSnapshot 是否是snapshot
139 | * @param {String} repository 仓库名称
140 | * @param {String} moduleName 模块名称
141 | * @return {void}
142 | */
143 | delModule: function(isSnapshot, repository, moduleName) {
144 | var cache = this.listAll(isSnapshot);
145 | if(!cache[repository]){
146 | return false;
147 | }
148 | var modules = cache[repository].modules;
149 | if(cache[repository].modules[moduleName]){
150 | delete cache[repository].modules[moduleName];
151 | }
152 | },
153 | /**
154 | * 返回缓存全部内容
155 | * @param {Boolean} isSnapshot 是否是snapshot
156 | * @return {Object} 缓存对象
157 | */
158 | listAll: function(isSnapshot) {
159 | return isSnapshot ? this._snapshotCache : this._cache;
160 | },
161 | /**
162 | * 返回仓库列表
163 | * @param {Boolean} isSnapshot 是否是snapshot
164 | * @return {Array} 数组每项包含name,stat
165 | */
166 | listRepository: function(isSnapshot){
167 | var cache = this.listAll(isSnapshot);
168 | return _.map(cache, function(v, k){
169 | return {name: k, stat: v.stat};
170 | });
171 | },
172 | /**
173 | * 返回模块列表
174 | * @param {Boolean} isSnapshot 是否是snapshot
175 | * @param {String} repository 仓库名称
176 | * @return {Array} 数组每项为模块名(不含版本号以及环境)
177 | */
178 | listModules: function(isSnapshot, repository){
179 | var cache = this.listAll(isSnapshot);
180 | return cache[repository] ? _.keys(cache[repository].modules) : [];
181 | },
182 | /**
183 | * 返回模块下的包列表
184 | * @param {Boolean} isSnapshot 是否是snapshot
185 | * @param {String} repository 仓库名称
186 | * @param {String} name 模块名
187 | * @return {Array} 数组每项为包名称(含版本号以及环境)
188 | */
189 | listPackages: function(isSnapshot, repository, name){
190 | var cache = this.listAll(isSnapshot);
191 | return cache[repository].modules[name];
192 | },
193 | /**
194 | * 比较需要的模块与缓存内容,返回缓存中存在的包名称
195 | * @param {String} repos 仓库名称
196 | * @param {Array} list 所需的模块列表(包含版本号,不含环境)
197 | * @param {Array} userLocals 用户本地缓存
198 | * @param {String} platform 环境信息
199 | * @param {Object} strategies 模块策略
200 | * @return {HashMap} 缓存存在的模块列表(包含版本号和环境)
201 | */
202 | diffPackages: function(repos, list, userLocals, platform, strategies){
203 | if(!this._cache[repos.release] && !this._snapshotCache[repos.snapshot]){
204 | return {};
205 | }
206 | var repository = repos.release,
207 | snapRepo = repos.snapshot,
208 | modules = (this._cache[repository]||{}).modules,
209 | snapshotModules = (this._snapshotCache[snapRepo]||{}).modules,
210 | storage = this.storage,
211 | fileExt = utils.getFileExt(),
212 | downloads = {},
213 | alwaysUpdates = {}, //有alwaysUpdates为1时,downloads也会存在
214 | installs = {},
215 | postinstalls = {},
216 | rebuilds = {},
217 | blacks = [],
218 | strategy;
219 |
220 | //服务端缓存
221 | _.forEach(list, function(name){
222 | var moduleName = utils.splitModuleName(name),
223 | isSnapshot = utils.isSnapshot(name),
224 | repo = isSnapshot ? snapRepo : repository,
225 | packages = isSnapshot ? snapshotModules[moduleName] || snapshotModules[name + fileExt] : modules[moduleName] || modules[name + fileExt];
226 | if(strategy = strategies[name] || strategies[moduleName]){
227 | if(strategy[CACHESTRATEGY.BLACKLIST]){
228 | blacks.push(name);
229 | }
230 | }
231 | if(!packages){
232 | return;
233 | }
234 | if(strategy){
235 | //如果有忽略缓存策略,则忽略其他策略,相当于真实安装
236 | if(strategy[CACHESTRATEGY.IGNORECACHE]){
237 | installs[moduleName] = name;
238 | return;
239 | }
240 | //如果有强制同步服务端策略,则本地缓存失效
241 | if(strategy[CACHESTRATEGY.ALWAYSUPDATE]){
242 | alwaysUpdates[moduleName] = 1;
243 | }
244 | //安装后执行策略
245 | if(strategy[CACHESTRATEGY.POSTINSTALL]){
246 | postinstalls[moduleName] = strategy[CACHESTRATEGY.POSTINSTALL];
247 | }
248 | }
249 | var packageNameForPlatform = utils.joinPackageName(name, platform),
250 | packageName = name;
251 | if(packages.indexOf(packageNameForPlatform + fileExt) > -1){
252 | downloads[packageNameForPlatform] = {url: storage.get(repo, packageNameForPlatform + fileExt)};
253 | } else if (packages.indexOf(packageName + fileExt) > -1){
254 | downloads[packageName] = {url: storage.get(repo, packageName + fileExt)};
255 | }
256 | //gyp模块,需要执行编译,一般只有包发布SNAPSHOT版本的时需要考虑
257 | if(isSnapshot && strategy && strategy.isGyp){
258 | rebuilds[packageName] = 1;
259 | }
260 | });
261 |
262 | //客户端缓存
263 | _.forEach(userLocals, function(name){
264 | var moduleName = utils.splitModuleName(name),
265 | isSnapshot = utils.isSnapshot(name),
266 | repo = isSnapshot ? snapRepo : repository;
267 | //由于是以SNAPSHOT为依据,该标示只出现在版本号,则会影响SNAPSHOT对应的版本
268 | if(isSnapshot){
269 | alwaysUpdates[name] = 1;
270 | }
271 | if(!(strategy = strategies[name] || strategies[moduleName])){
272 | isSnapshot && downloadDeal();
273 | return;
274 | }
275 | //如果存在黑名单,则忽略其他策略
276 | if(strategy[CACHESTRATEGY.BLACKLIST]){
277 | blacks.push(name);
278 | return;
279 | }
280 | //如果有忽略缓存策略,则忽略其他策略,相当于真实安装
281 | if(strategy[CACHESTRATEGY.IGNORECACHE]){
282 | installs[moduleName] = name;
283 | return;
284 | }
285 | //gyp模块,需要执行编译,一般只有包发布SNAPSHOT版本的时需要考虑
286 | if(isSnapshot && strategy && strategy.isGyp){
287 | rebuilds[name] = 1;
288 | }
289 | //如果有强制同步服务端策略,则本地缓存失效
290 | if(strategy[CACHESTRATEGY.ALWAYSUPDATE] || isSnapshot){
291 | downloadDeal();
292 | !isSnapshot && (alwaysUpdates[moduleName] = 1);
293 | }
294 | if(strategy[CACHESTRATEGY.POSTINSTALL]){
295 | postinstalls[moduleName] = strategy[CACHESTRATEGY.POSTINSTALL];
296 | }
297 | function downloadDeal(){
298 | var packages = isSnapshot ? snapshotModules[moduleName] || snapshotModules[name + fileExt] : modules[moduleName] || modules[name + fileExt];
299 | if(!packages){
300 | installs[moduleName] = name;
301 | return;
302 | }
303 | var packageNameForPlatform = utils.joinPackageName(name, platform);
304 | if(packages.indexOf(packageNameForPlatform + fileExt) > -1){
305 | downloads[packageNameForPlatform] = {url: storage.get(repo, packageNameForPlatform + fileExt)};
306 | } else if (packages.indexOf(name + fileExt) > -1){
307 | downloads[name] = {url: storage.get(repo, name + fileExt)};
308 | }
309 | }
310 | });
311 | return {
312 | downloads: downloads,
313 | alwaysUpdates: alwaysUpdates,
314 | installs: installs,
315 | postinstall: postinstalls,
316 | rebuilds: rebuilds,
317 | blacks: blacks
318 | };
319 | },
320 | setStorage: function(st){
321 | this.storage = st;
322 | },
323 | getStorage: function() {
324 | return this.storage;
325 | }
326 | };
327 |
328 | module.exports = Cache;
329 |
--------------------------------------------------------------------------------
/src/js/app/code/cache/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: robin
3 | * @Date: 2017-05-08 10:37
4 | * @Email: xin.lin@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2017-05-08 10:37
7 | */
8 |
9 |
10 | var Factory = require('../annotation/Factory');
11 |
12 | module.exports = (function(opts) {
13 | var cache = opts.zookeeper && opts.zookeeper != 'undefined' ? require('./zkCache') : require('./cache');
14 | return new cache(opts);
15 | })(process.env);
16 |
--------------------------------------------------------------------------------
/src/js/app/code/controller/cacheStrategy.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | var _ = require('lodash'),
3 | path = require('path'),
4 | fs = require('fs'),
5 | fsExtra = require('fs-extra'),
6 | utils = require('../../../common/utils'),
7 | CACHESTRATEGY = require('../../../common/constant').CACHESTRATEGY;
8 |
9 | var modulesCachePath = utils.getServerCachePath(),
10 | fileExt = utils.getFileExt();
11 |
12 | var storage = require('../storage'),
13 | renderTool = require('../../widget/render');
14 |
15 | /*@Controller*/
16 | module.exports = {
17 | /*@Autowired("privatemodules")*/
18 | packageList: null,
19 | /*@RequestMapping("/strategy")*/
20 | strategy: function(req, res){
21 | res.render('strategy', {
22 | modules: this.packageList.list()
23 | });
24 | },
25 | /*@RequestMapping("/strategy/api/list")*/
26 | /*@ResponseBodyDeal*/
27 | list: function(req, res){
28 | res.end({
29 | status: 200,
30 | modules: this.packageList.list()
31 | });
32 | },
33 | /*@RequestMapping("/strategy/api/add")*/
34 | /*@ResponseBodyDeal*/
35 | add: function(req, res, reqData){
36 | var name = reqData.moduleName,
37 | strategy = reqData.strategy;
38 | if(!name){
39 | res.end({
40 | status: 400,
41 | message: '模块名称不能为空'
42 | });
43 | return;
44 | }
45 | if(!strategy[CACHESTRATEGY.ALWAYSUPDATE]
46 | && !strategy[CACHESTRATEGY.IGNORECACHE]
47 | && !strategy[CACHESTRATEGY.POSTINSTALL]
48 | && !strategy[CACHESTRATEGY.BLACKLIST]){
49 | res.end({
50 | status: 400,
51 | message: '模块策略不能为空'
52 | });
53 | return;
54 | }
55 | //添加模块策略
56 | this.packageList.add(name, strategy, function(err){
57 | if(err){
58 | res.end({
59 | status: 500,
60 | message: err.stack || err
61 | });
62 | return;
63 | }
64 | res.end({
65 | status: 200
66 | });
67 | });
68 | },
69 | /*@RequestMapping("/strategy/api/remove")*/
70 | /*@ResponseBodyDeal*/
71 | remove: function(req, res, reqData){
72 | var name = reqData.moduleName,
73 | moduleStragety = this.packageList.list();
74 | if(!name){
75 | res.end({
76 | status: 400,
77 | message: '模块名称不能为空'
78 | });
79 | return;
80 | }
81 | if(!moduleStragety){
82 | res.end({
83 | status: 500,
84 | errmsg: '不存在该缓存策略'
85 | });
86 | return;
87 | }
88 | this.packageList.remove(name, function(err){
89 | rs(err);
90 | });
91 | function rs(err){
92 | if(err){
93 | res.end({
94 | status: 500,
95 | message: err.stack || err
96 | });
97 | return;
98 | }
99 | res.end({
100 | status: 200
101 | });
102 | }
103 | return;
104 | },
105 | /*@ExceptionHandler*/
106 | /*@ResponseBodyDeal*/
107 | error: function(err, req, res){
108 | console.info(err.stack);
109 | res.status(500).end(err.message || err.stack || err);
110 | }
111 | }
--------------------------------------------------------------------------------
/src/js/app/code/controller/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: robin
3 | * @Date: 2016-09-14 14:31:25
4 | * @Email: xin.lin@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2016-09-19 15:53:07
7 | */
8 |
9 | 'use strict'
10 | var fs = require('fs'),
11 | _ = require('lodash'),
12 | path = require('path'),
13 | semver = require('semver'),
14 | stream = require('stream'),
15 | fsExtra = require('fs-extra'),
16 | multiparty = require('multiparty'),
17 | utils = require('../../../common/utils'),
18 | npmUtils = require('../../../common/npmUtils'),
19 | constant = require('../../../common/constant'),
20 | shellUtils = require('../../../common/shellUtils'),
21 | storage = require('../storage');
22 |
23 | var modulesCachePath = utils.getServerCachePath(),
24 | fileExt = utils.getFileExt(),
25 | TEMPDIR = path.resolve(modulesCachePath, '.tempdir'),
26 | UPLOADDIR = constant.UPLOADDIR,
27 | SPLIT = constant.SPLIT,
28 | token = process.env.token;
29 |
30 | fsExtra.ensureDirSync(TEMPDIR);
31 |
32 | /*@Controller*/
33 | module.exports = {
34 | /*@Autowired("privatemodules")*/
35 | packageList: null,
36 | /*@RequestMapping(["/{repository}/check"])*/
37 | /*@ResponseBodyDeal*/
38 | check: function(req, res, reqData, repository){
39 | var list = reqData.list || [],
40 | checkSyncList = reqData.checkSyncList || [];
41 | if(!(Array.isArray(list) && Array.isArray(checkSyncList))){
42 | res.end({
43 | status: -1,
44 | message: 'list should be an array!'
45 | });
46 | return;
47 | }
48 | var repos = utils.getRepoNames(repository),
49 | platform = reqData.platform;
50 | res.end({
51 | status: 0,
52 | message: 'success',
53 | data: storage.diffPackages(repos, list, checkSyncList, platform, this.packageList.list())
54 | });
55 | },
56 | /*@RequestMapping(["/{repository}/fetch/{name}"])*/
57 | fetch: function(req, res, repository, name) {
58 | console.info('[fetch]', repository || 'default', name);
59 | var getRepo = utils.getRepos(repository),
60 | filename = decodeURIComponent(name) + fileExt;
61 | storage.get(getRepo(filename), filename, res);
62 | },
63 | /*@RequestMapping(["/{repository}/upload"])*/
64 | /*@ResponseBodyDeal*/
65 | upload: function(req, res, repository) {
66 | // check token for permission,if token exists
67 | if(token && token !== req.headers.token){
68 | res.status(404).end({
69 | message: 'Token missing or wrong! Forbid uploading without token.'
70 | });
71 | return;
72 | }
73 |
74 | // parse a file upload
75 | var form = new multiparty.Form({
76 | encoding: 'utf-8',
77 | uploadDir: TEMPDIR
78 | });
79 |
80 | var packageList = this.packageList,
81 | getRepo = utils.getRepos(repository);
82 | form.parse(req, function(err, fields, files) {
83 | if(err){
84 | console.error(err);
85 | res.end({
86 | status: 500,
87 | message: '文件流出错!!!'
88 | });
89 | return;
90 | }
91 | console.info('接收到的附加信息', JSON.stringify(fields));
92 | // TODO: mutliparty的fields的每个字段都是数组,暂时先全取数组第一项
93 | if(fields && fields.name){
94 | var name = fields.name[0],
95 | password = fields.password[0];
96 | if(packageList.auth(name, password)){
97 | packageList.add(name, {
98 | isPrivate: fields.isPrivate && (fields.isPrivate[0] === 'on'),
99 | isGyp: fields.isGyp && (fields.isGyp[0] === 'on'),
100 | user: fields.user[0],
101 | password: password
102 | }, function(err){
103 | if(err){
104 | res.end({
105 | status: 500,
106 | message: err.stack || err
107 | });
108 | return;
109 | }
110 | });
111 | } else {
112 | res.end({
113 | status: -2,
114 | message: '没有upload名为'+name+'的包的权限!请检查-p/--password配置!'
115 | });
116 | return;
117 | }
118 | }
119 | if (!files){
120 | res.end({
121 | status: 500,
122 | message: '文件流出错!!!'
123 | });
124 | return;
125 | }
126 | if (files.modules.length != 0) {
127 | var file = files.modules[0].path,
128 | target = path.resolve(TEMPDIR, String(Date.now()));
129 | console.info('开始接收文件!' + files.modules[0].originalFilename);
130 |
131 | fs.createReadStream(file).pipe(utils.extract(file, target, function(){
132 | //删除压缩文件
133 | shellUtils.rm('-f', file);
134 | //压缩每个模块
135 | var modules = shellUtils.ls(path.resolve(target, UPLOADDIR)),
136 | count = 0, errorObj = false;
137 | modules.forEach(function(file) {
138 | var riverCompress = new stream.PassThrough();
139 |
140 | utils.compress(path.resolve(target, UPLOADDIR, file), file, constant.COMPRESS_TYPE.TAR).pipe(riverCompress);
141 |
142 | storage.put(getRepo(file), file + fileExt, riverCompress, function(err){
143 | if (err) {
144 | console.error(file + fileExt + ' upload to swift is wrong: ', err.stack);
145 | errorObj = err.message || err.stack;
146 | }else{
147 | console.debug(file + fileExt + ' upload to swift done');
148 | }
149 | count++;
150 | if (count == modules.length) {
151 | //删除临时目录
152 | process.nextTick(function() {
153 | shellUtils.rm('-rf', target);
154 | console.info('upload done!!');
155 | if(errorObj){
156 | res.end({
157 | status: 500,
158 | message: errorObj
159 | });
160 | }else{
161 | res.end({
162 | status: 0,
163 | message: 'success'
164 | });
165 | }
166 | });
167 | }
168 | });
169 | });
170 | }));
171 | }
172 | });
173 | },
174 | /*@RequestMapping("/list/{versionType}")*/
175 | /*@ResponseBodyDeal*/
176 | list: function(req, res, versionType, reqData){
177 | var isSnapshot = utils.isSnapshot(versionType);
178 | if(reqData.repository){
179 | var repos = utils.getRepoNames(reqData.repository),
180 | repository = isSnapshot ? repos.snapshot : repos.release;
181 | if(reqData.name){
182 | res.end(storage.listPackages(isSnapshot, repository, reqData.name));
183 | } else {
184 | res.end(storage.listModules(isSnapshot, repository));
185 | }
186 | } else {
187 | res.end(storage.listAll(isSnapshot));
188 | }
189 | },
190 | /*@RequestMapping("/{repository}/info")*/
191 | /*@ResponseBodyDeal*/
192 | info: function(req, res, reqData, repository){
193 | var name = reqData.name,
194 | version = reqData.version,
195 | platform = reqData.platform,
196 | isSnapshot = utils.isSnapshot(version),
197 | all = storage.listPackages(isSnapshot, utils.getRepos(repository)(isSnapshot), name.replace(RegExp('/', 'g'), SPLIT)) || [],
198 | fileExt = utils.getFileExt(),
199 | packages = _.map(all, function(el){
200 | return _.trimEnd(el, fileExt);
201 | }),
202 | rtn = {
203 | name: name,
204 | version: version,
205 | full: null,
206 | isPrivate: false
207 | };
208 | // 判断包是否是私有包
209 | rtn.isPrivate = this.packageList.checkPrivate(name);
210 | if(rtn.isPrivate){
211 | if(!version || version === 'latest'){
212 | rtn.version = utils.getLastestVersion(packages);
213 | }
214 | var fullname = name.replace(RegExp('/', 'g'), SPLIT) + '@' + rtn.version,
215 | packageNameForPlatform = utils.joinPackageName(fullname, platform);
216 | if(packages.indexOf(packageNameForPlatform) > -1){
217 | rtn.full = packageNameForPlatform;
218 | } else if (packages.indexOf(fullname) > -1){
219 | rtn.full = fullname;
220 | }
221 | }
222 | console.info('info', JSON.stringify(rtn));
223 | res.end({
224 | status: 0,
225 | message: 'succ',
226 | data: rtn
227 | });
228 | },
229 | /*@RequestMapping("/{repository}/versions/latest")*/
230 | /*@ResponseBodyDeal*/
231 | latestVersion: function(req, res, reqData, repository){
232 | var name = reqData.name,
233 | version;
234 | // 判断包是否是私有包
235 | if(this.packageList.checkPrivate(name)){
236 | var all = storage.listPackages(false, utils.getRepos(repository)(false), name.replace(RegExp('/', 'g'), SPLIT)) || [],
237 | fileExt = utils.getFileExt(),
238 | packages = _.map(all, function(el){
239 | return _.trimEnd(el, fileExt);
240 | });
241 | version = utils.getLastestVersion(packages);
242 | }
243 | npmUtils.getLastestVersion(name, function(err, v){
244 | //如果不存在则报错
245 | if(err){
246 | if(version){
247 | res.end({
248 | status: 0,
249 | message: 'succ',
250 | data: version
251 | });
252 | }else{
253 | res.end({
254 | status: 1,
255 | message: err
256 | });
257 | }
258 | return;
259 | }
260 | //比较ncs源和npm源之间的版本,取大者
261 | if(!version){
262 | res.end({
263 | status: 0,
264 | message: 'succ',
265 | data: v
266 | });
267 | }else{
268 | res.end({
269 | status: 0,
270 | message: 'succ',
271 | data: semver.gt(v, version) ? v : version
272 | });
273 | }
274 | });
275 | },
276 | /*@ExceptionHandler*/
277 | /*@ResponseBodyDeal*/
278 | error: function(err, req, res){
279 | console.error(err.stack || err);
280 | res.status(500).end(err.stack || err);
281 | }
282 | }
--------------------------------------------------------------------------------
/src/js/app/code/controller/manage.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | var _ = require('lodash'),
3 | path = require('path'),
4 | fs = require('fs'),
5 | fsExtra = require('fs-extra'),
6 | utils = require('../../../common/utils'),
7 | constant = require('../../../common/constant');
8 |
9 | var modulesCachePath = utils.getServerCachePath(),
10 | fileExt = utils.getFileExt();
11 |
12 | var storage = require('../storage'),
13 | renderTool = require('../../widget/render');
14 |
15 | /*@Controller*/
16 | module.exports = {
17 | /*@RequestMapping("/")*/
18 | redirect: function(req, res){
19 | res.redirect('/versionType')
20 | },
21 | /*@RequestMapping(["/manage/createRepository/{versionType}/{repository}"])*/
22 | createRepository: function(versionType, repository, req, res){
23 | var isSnapshot = utils.isSnapshot(versionType);
24 | storage.createRepository(isSnapshot, utils.getRepos(repository)(isSnapshot), function(err){
25 | if(err) {
26 | res.statusCode(500).end(String(err.message || err.stack || err));
27 | } else {
28 | res.end('success!');
29 | }
30 | });
31 | },
32 | /*@RequestMapping(["/manage/sync"])*/
33 | sync: function(req, res){
34 | storage.sync();
35 | res.writeHead(200, {'Content-Type': 'application/json;charset=UTF-8'});
36 | res.end('已触发同步!');
37 | },
38 | /*@RequestMapping(["/versionType"])*/
39 | versionType: function(req, res){
40 | var fileList = [
41 | {name: 'snapshot', icon: 'drive'},
42 | {name: 'release', icon: 'drive'}
43 | ];
44 | renderTool.renderDirectory({
45 | title: 'versionType',
46 | fileList: fileList,
47 | backpath: '/versionType',
48 | backname: 'versionType',
49 | view: 'details'
50 | }, res);
51 | },
52 | /*@RequestMapping(["/versionType/{versionType}"])*/
53 | repository: function(versionType, req, res){
54 | var fileList = _.map(storage.listRepository(utils.isSnapshot(versionType)), function(v, k){
55 | return {name: v.name, stat: v.stat, icon: 'folder'}
56 | });
57 | renderTool.renderDirectory({
58 | title: versionType,
59 | fileList: fileList,
60 | backpath: '/versionType',
61 | backname: versionType,
62 | view: 'details'
63 | }, res);
64 | },
65 | /*@RequestMapping("/versionType/{versionType}/{repository}")*/
66 | modules: function(req, res, versionType, repository){
67 | var isSnapshot = utils.isSnapshot(versionType),
68 | fileList = _.map(storage.listModules(isSnapshot, utils.getRepos(repository)(isSnapshot)), function(v){
69 | return {name: v, icon: 'folder'}
70 | });
71 | renderTool.renderDirectory({
72 | title: repository,
73 | fileList: fileList,
74 | backpath: '/versionType/' + versionType,
75 | backname: repository,
76 | view: 'repository'
77 | }, res);
78 | },
79 | /*@RequestMapping("/versionType/{versionType}/{repository}/{name}")*/
80 | packages: function(req, res, versionType, repository, name){
81 | var isSnapshot = utils.isSnapshot(versionType),
82 | fileList = _.map(storage.listPackages(isSnapshot, utils.getRepos(repository)(isSnapshot), decodeURIComponent(name)), function(v, k){
83 | return {name: v, icon: 'box'}
84 | });
85 | renderTool.renderDirectory({
86 | title: name,
87 | fileList: fileList,
88 | backpath: '/versionType/' + versionType + '/' + repository,
89 | backname: name
90 | }, res);
91 | },
92 | /*@RequestMapping("/versionType/{versionType}/{repository}/{name}/{subname}")*/
93 | info: function(req, res, versionType, repository, subname){
94 | var filename = decodeURIComponent(subname),
95 | isSnapshot = utils.isSnapshot(versionType);
96 |
97 | repository = utils.getRepos(repository)(isSnapshot);
98 |
99 | storage.listPackageInfo(isSnapshot, repository, filename, function(err, stat){
100 | if(err){
101 | res.status(err.statusCode || 404).end(String(err.message || err.stack || err));
102 | } else {
103 | renderTool.renderInfo({
104 | name: subname,
105 | stat: stat,
106 | repository: repository,
107 | versionType: versionType
108 | }, res);
109 | }
110 | });
111 | },
112 | /*@RequestMapping("/download/{versionType}/{repository}/{name}")*/
113 | download: function(req, res, repository, name){
114 | var filename = decodeURIComponent(name);
115 | storage.get(utils.getRepos(repository)(filename), filename, res);
116 | },
117 | /*@ExceptionHandler*/
118 | /*@ResponseBody*/
119 | error: function(err, req, res){
120 | console.info(err.stack);
121 | res.status(500).end(err.message || err);
122 | }
123 | }
--------------------------------------------------------------------------------
/src/js/app/code/dao/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: robin
3 | * @Date: 2017-05-08 10:37
4 | * @Email: xin.lin@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2017-05-08 10:37
7 | */
8 |
9 |
10 | var Factory = require('../annotation/Factory');
11 | /*@Component("privatemodules")*/
12 | module.exports = (function(opts) {
13 | var zkClass = opts.zookeeper && opts.zookeeper != 'undefined' ? require('./zkPackageList') : require('./packageList');
14 | return new zkClass(opts);
15 | })(process.env);
16 |
--------------------------------------------------------------------------------
/src/js/app/code/dao/packageList.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: wyw.wang
3 | * @Date: 2016-12-07 16:53
4 | * @Email: wyw.wang@qunar.com
5 | * @Last modified by: wyw
6 | * @Last modified time: 2016-12-07 16:54
7 | */
8 |
9 | var fs = require('fs'),
10 | _ = require('lodash'),
11 | path = require('path'),
12 | fsExtra = require('fs-extra'),
13 | utils = require('../../../common/utils'),
14 | constant = require('../../../common/constant'),
15 | CACHESTRATEGY = require('../../../common/constant').CACHESTRATEGY;
16 |
17 | var serverCachePath = utils.getServerCachePath(),
18 | packageListName = 'packageList.json',
19 | packageListPath = path.join(serverCachePath, packageListName);
20 |
21 | function PackageList(){
22 | this._map = {};
23 | }
24 |
25 | PackageList.prototype = {
26 | /**
27 | * 加载包配置信息
28 | * @return {void} [description]
29 | */
30 | load: function(){
31 | if(fs.existsSync(packageListPath)){
32 | this._map = fsExtra.readJsonSync(packageListPath);
33 | }
34 | },
35 | /**
36 | * 列出指定策略的模块
37 | */
38 | list: function(){
39 | return this._map;
40 | },
41 | /**
42 | * 追加一个包的信息
43 | * @param {string} name 包名称
44 | * @param {json} info 包信息
45 | * @param {Function} cbk 回调函数
46 | */
47 | add: function(name, info, cbk){
48 | console.info('[synclist] add:', name, JSON.stringify(info));
49 | var data = this._map[name] || {};
50 | _.forEach(info, function(v, k){
51 | data[k] = v;
52 | });
53 | this._map[name] = data;
54 | this.save();
55 | cbk();
56 | },
57 | /**
58 | * 删除一个包的信息
59 | * @param {string} name 包名称
60 | * @param {Function} cbk 回调函数
61 | */
62 | remove: function(name, cbk){
63 | console.info('[synclist] remove:', name);
64 | var data = this._map[name];
65 | if(data.isPrivate){
66 | _.forEach(CACHESTRATEGY, function(v, k){
67 | data[v] = null;
68 | delete data[v];
69 | });
70 | }else{
71 | this._map[name] = null;
72 | delete this._map[name];
73 | }
74 | this.save();
75 | cbk();
76 | },
77 | /**
78 | * 固化包信息到文件
79 | * @return {void} [description]
80 | */
81 | save: function(){
82 | fsExtra.writeJsonSync(packageListPath, this._map);
83 | },
84 | /**
85 | * 比较出需要同步的包
86 | * @param {array} list 需要依赖包
87 | * @return {map} 需要同步的依赖包
88 | */
89 | diffSync: function(list){
90 | var hit = {},
91 | _map = this._map;
92 | _.forEach(list, function(el){
93 | var name = utils.splitModuleName(el),
94 | moduleStrategy = _map[name];
95 | //只要含有SNAPSHOT标示就算,由于存在本地会导致多机情况下实效,最低要保证SNAPSHOT版本的更新
96 | if((moduleStrategy && moduleStrategy[constant.CACHESTRATEGY.ALWAYSUPDATE]) || utils.isSnapshot(el)){
97 | hit[el] = {
98 | flag: constant.CACHESTRATEGY.ALWAYSUPDATE
99 | };
100 | }
101 | });
102 | return hit;
103 | },
104 | /**
105 | * 判断一个包是否是私有模块
106 | * @param {string} name 包名称
107 | * @return {boolean} [description]
108 | */
109 | checkPrivate: function(name){
110 | return this._map[name] && this._map[name].isPrivate;
111 | },
112 | /**
113 | * 判断对一个私有模块是否有权限
114 | * @param {string} name 包名称
115 | * @param {string} password 私钥
116 | * @return {boolean} [description]
117 | */
118 | auth: function(name, password){
119 | var _map = this._map;
120 | if(_map[name] && _map[name].isPrivate && _map[name].password){
121 | return password === _map[name].password;
122 | } else {
123 | return true;
124 | }
125 | }
126 | }
127 |
128 | module.exports = PackageList;
129 |
--------------------------------------------------------------------------------
/src/js/app/code/dao/zkPackageList.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: wyw.wang
3 | * @Date: 2016-12-07 16:53
4 | * @Email: wyw.wang@qunar.com
5 | * @Last modified by: wyw
6 | * @Last modified time: 2016-12-07 16:54
7 | */
8 |
9 | var _ = require('lodash'),
10 | path = require('path'),
11 | utils = require('../../../common/utils'),
12 | zkClient = require('../../../common/zkClient'),
13 | constant = require('../../../common/constant'),
14 | SPLIT = constant.SPLIT,
15 | CACHESTRATEGY = constant.CACHESTRATEGY;
16 |
17 | var ROOT = 'private';
18 |
19 | function ZkPackageList() {
20 | this._map = {};
21 | }
22 |
23 | ZkPackageList.prototype = {
24 | /**
25 | * 加载包配置信息
26 | * @return {void} [description]
27 | */
28 | load: function(){
29 | var oriData = [],
30 | _map = this._map;
31 | zkClient.exist(ROOT).then(function(isExist){
32 | if(isExist){
33 | init();
34 | return;
35 | }
36 | zkClient.mkdirp(ROOT).then(function(){
37 | init();
38 | });
39 | });
40 | function init(){
41 | //初始化私有模块数据
42 | zkClient.getChildren(ROOT).then(function(data){
43 | if(!data || data.length == 0){
44 | return;
45 | }
46 | oriData = data;
47 | _.forEach(data, function(v){
48 | //监听节点数据变更
49 | zkClient.register(zkClient.Event.NODE_DATA_CHANGED, [ROOT, v].join('/'), function(data){
50 | if(data){
51 | console.debug('设置模块' + v + '的信息');
52 | _map[v] = JSON.parse(data);
53 | }
54 | });
55 | zkClient.getData([ROOT, v].join('/')).then(function(data){
56 | if(data){
57 | _map[v] = JSON.parse(data);
58 | }
59 | });
60 | });
61 | });
62 | //监控
63 | zkClient.register(zkClient.Event.NODE_CHILDREN_CHANGED, ROOT, function(data){
64 | console.debug('触发' + ROOT + '节点监听事件');
65 | var addChanges = _.difference(data, oriData),
66 | rmChanges = _.difference(oriData, data);
67 | //置换成新的缓存
68 | oriData = data;
69 | //新增模块处理
70 | _.forEach(addChanges, function(v){
71 | if(v){
72 | //由于新增节点的同时可能获取数据,故需要马上获取数据
73 | zkClient.getData([ROOT, v].join('/')).then(function(data){
74 | if(data){
75 | console.debug('初始化' + [ROOT, v].join('/') + '节点数据');
76 | _map[v] = JSON.parse(data);
77 | }
78 | });
79 | //监听节点数据变更
80 | zkClient.register(zkClient.Event.NODE_DATA_CHANGED, [ROOT, v].join('/'), function(data){
81 | if(data){
82 | console.debug('设置模块' + v + '的信息');
83 | _map[v] = JSON.parse(data);
84 | }
85 | });
86 | }
87 | });
88 | //删除模块处理
89 | _.forEach(rmChanges, function(v){
90 | //注销监听
91 | zkClient.unregister(zkClient.Event.NODE_DATA_CHANGED, [ROOT, v].join('/'));
92 | //删除节点对应的内存映射
93 | _map[v] = null;
94 | delete _map[v];
95 | });
96 | });
97 | }
98 | },
99 | /**
100 | * 列出指定策略的模块
101 | */
102 | list: function(){
103 | return this._map;
104 | },
105 | /**
106 | * 追加一个包的信息
107 | * @param {string} name 包名称
108 | * @param {json} info 包信息
109 | * @param {Function} cbk 回调函数
110 | */
111 | add: function(name, info, cbk){
112 | name = name.replace(RegExp('/', 'g'), SPLIT);
113 | console.info('[synclist] add:', name, JSON.stringify(info));
114 | var p = [ROOT, name].join('/');
115 | zkClient.exist(p).then(function(isExist){
116 | if(isExist){
117 | zkClient.getData(p).then(function(data){
118 | if(data){
119 | data = JSON.parse(data);
120 | _.forEach(info, function(v, k){
121 | data[k] = v;
122 | });
123 | setData(data);
124 | }else{
125 | setData(info);
126 | }
127 | });
128 | return;
129 | }
130 | zkClient.mkdirp(p).then(function(){
131 | setData(info);
132 | });
133 | });
134 | function setData(d){
135 | zkClient.setData(p, JSON.stringify(d)).then(function(){
136 | cbk();
137 | });
138 | }
139 | },
140 | /**
141 | * 删除一个包的信息
142 | * @param {string} name 包名称
143 | * @param {Function} cbk 回调函数
144 | */
145 | remove: function(name, cbk){
146 | name = name.replace(RegExp('/', 'g'), SPLIT);
147 | console.info('[synclist] remove:', name);
148 | var p = [ROOT, name].join('/');
149 | zkClient.exist(p).then(function(isExist){
150 | if(isExist){
151 | zkClient.getData(p).then(function(data){
152 | if(data){
153 | data = JSON.parse(data);
154 | if(data.isPrivate){
155 | _.forEach(CACHESTRATEGY, function(v, k){
156 | data[v] = null;
157 | delete data[k];
158 | });
159 | zkClient.setData(p, JSON.stringify(data)).then(function(){
160 | cbk();
161 | });
162 | }else{
163 | zkClient.remove(p).then(function(){
164 | cbk();
165 | });
166 | }
167 | }else{
168 | cbk();
169 | }
170 | });
171 | }else{
172 | cbk();
173 | }
174 | });
175 | },
176 | /**
177 | * 比较出需要同步的包
178 | * @param {array} list 需要依赖包
179 | * @return {map} 需要同步的依赖包
180 | */
181 | diffSync: function(list){
182 | var hit = {},
183 | _map = this._map;
184 | _.forEach(list, function(el){
185 | var name = utils.splitModuleName(el).replace(RegExp('/', 'g'), SPLIT),
186 | moduleStrategy = _map[name];
187 | //只要含有SNAPSHOT标示就算,由于存在本地会导致多机情况下实效,最低要保证SNAPSHOT版本的更新
188 | if(moduleStrategy || utils.isSnapshot(el)){
189 | moduleStrategy[CACHESTRATEGY.ALWAYSUPDATE] = 1;
190 | hit[el] = moduleStrategy;
191 | }
192 | });
193 | return hit;
194 | },
195 | /**
196 | * 列出指定策略的模块
197 | */
198 | list: function(){
199 | return this._map;
200 | },
201 | /**
202 | * 判断一个包是否是私有模块
203 | * @param {string} name 包名称
204 | * @return {boolean} [description]
205 | */
206 | checkPrivate: function(name){
207 | name = name.replace(RegExp('/', 'g'), SPLIT);
208 | return this._map[name] && this._map[name].isPrivate;
209 | },
210 | /**
211 | * 判断对一个私有模块是否有权限
212 | * @param {string} name 包名称
213 | * @param {string} password 私钥
214 | * @return {boolean} [description]
215 | */
216 | auth: function(name, password){
217 | var _map = this._map;
218 | name = name.replace(RegExp('/', 'g'), SPLIT);
219 | if(_map[name] && _map[name].isPrivate && _map[name].password){
220 | return password === _map[name].password;
221 | } else {
222 | return true;
223 | }
224 | }
225 | }
226 |
227 | module.exports = ZkPackageList;
228 |
--------------------------------------------------------------------------------
/src/js/app/code/storage/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: wyw.wang
3 | * @Date: 2016-10-12 10:37
4 | * @Email: wyw.wang@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2017-05-08 10:37
7 | */
8 |
9 | var Factory = require('../annotation/Factory'),
10 | cache = require('../cache'),
11 | _ = require('lodash');
12 |
13 | var storage = null;
14 |
15 | function getStorage(storageType, opts, snapshotOpts, swiftTokenTimeout, envOps){
16 | if(!storage){
17 | storageClass = storageType == 'localfile' ? require('./localfile') : require('./swift');
18 | storage = new storageClass(opts, snapshotOpts, swiftTokenTimeout, envOps);
19 | }
20 | return storage;
21 | };
22 |
23 |
24 | module.exports = {
25 | init: function(type, opts, snapshotOpts, swiftTokenTimeout, envOps){
26 | opts == snapshotOpts && cache.same();
27 | // opts can be a STRING ! ‘undefined’
28 | cache.setStorage(getStorage(type,
29 | opts === 'undefined' ? [] : (opts || "").split('|'),
30 | snapshotOpts === 'undefined' ? [] : (snapshotOpts || "").split('|'), swiftTokenTimeout, envOps));
31 | },
32 | sync: function(){
33 | var sto = getStorage();
34 | return sto.sync.apply(sto, arguments);
35 | },
36 | createRepository: function(){
37 | var sto = getStorage();
38 | return sto.createRepository.apply(sto, arguments);
39 | },
40 | listPackageInfo: function(){
41 | var sto = getStorage();
42 | return sto.listPackageInfo.apply(sto, arguments);
43 | },
44 | get: function(){
45 | var sto = getStorage();
46 | return sto.get.apply(sto, arguments);
47 | },
48 | put: function() {
49 | var sto = getStorage();
50 | return sto.put.apply(sto, arguments);
51 | },
52 | listAll: _.bind(cache.listAll, cache),
53 | listRepository: _.bind(cache.listRepository, cache),
54 | listModules: _.bind(cache.listModules, cache),
55 | listPackages: _.bind(cache.listPackages, cache),
56 | diffPackages: _.bind(cache.diffPackages, cache)
57 | };
58 |
--------------------------------------------------------------------------------
/src/js/app/code/storage/localfile.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: wyw.wang
3 | * @Date: 2016-10-11 18:04
4 | * @Email: wyw.wang@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2017-05-08 10:37
7 | */
8 |
9 | var _ = require('lodash'),
10 | path = require('path'),
11 | fs = require('fs'),
12 | fsExtra = require('fs-extra'),
13 | utils = require('../../../common/utils');
14 |
15 |
16 | var cache = require('../cache');
17 |
18 | function localfile(config){
19 | this.dir = config.length > 0 ? config[0] : utils.getServerCachePath();
20 | this.ignoreDir = ['.tempdir'];
21 | this.init();
22 | }
23 |
24 | localfile.prototype.sync = function(){
25 | // do nothing, do not need sync
26 | };
27 |
28 | localfile.prototype.init = function(){
29 | var self = this;
30 | fs.readdir(self.dir, function(err, files){
31 | _.forEach(files, function(file){
32 | if(self.ignoreDir.indexOf(file) < 0){
33 | _cacheRepository(self.dir, file);
34 | }
35 | });
36 | });
37 | fs.watch(self.dir, function(event, filename){
38 | console.info('[watch]', self.dir, event, filename);
39 | if(filename){
40 | _cacheRepository(self.dir, filename);
41 | } else {
42 | // TODO? watch的第二个参数貌似又兼容性问题,待测试
43 | console.error('cannot get filename when watching file at', self.dir);
44 | }
45 | });
46 | }
47 |
48 | /**
49 | * 缓存各个仓库
50 | * @param {path} base 跟路径
51 | * @param {string} name 仓库名称
52 | * @return {void} [description]
53 | */
54 | function _cacheRepository(base, name){
55 | var filepath = path.resolve(base, name),
56 | check = _checkPath(filepath),
57 | stat;
58 | console.info('[file change]', check.type, filepath);
59 | if( (check.type === 'create')
60 | && (stat = check.stat)
61 | && stat.isDirectory() ){
62 | cache.addRepository(name, stat);
63 | _traverseModule(name, filepath);
64 | } else if ( check.type === 'deleted'){
65 | cache.delRepository(name);
66 | }
67 | }
68 |
69 | /**
70 | * 遍历仓库的每个模块
71 | * @param {string} repository 仓库名称
72 | * @param {path} dir 仓库所在路径
73 | * @return {void} [description]
74 | */
75 | function _traverseModule(repository, dir){
76 | fs.readdir(dir, function(err, files){
77 | _.forEach(files, function(file){
78 | _cacheModule(dir, repository, file);
79 | });
80 | });
81 | fs.watch(dir, function(event, filename){
82 | console.info('[watch]', dir, event, filename);
83 | if(filename){
84 | _cacheModule(dir, repository, filename);
85 | } else {
86 | // TODO? watch的第二个参数貌似又兼容性问题,待测试
87 | console.error('cannot get filename when watching file at', base);
88 | }
89 | });
90 | }
91 |
92 | /**
93 | * 缓存每个模块(按模块名一级,之后再按照版本号一级)
94 | * @param {path} dir 模块所在路径
95 | * @param {string} repository 仓库名称
96 | * @param {string} name 模块对应的文件名
97 | * @return {void} [description]
98 | */
99 | function _cacheModule(dir, repository, name){
100 | var filepath = path.join(dir, name),
101 | check = _checkPath(filepath);
102 |
103 | console.info('[file change]', check.type, filepath);
104 | if( check.type === 'create' ){
105 | cache.addPackage(repository, name);
106 | } else if (check.type === 'delete'){
107 | cache.delPackage(repository, name);
108 | }
109 | }
110 |
111 |
112 | /**
113 | * 检查一个文件的变化,
114 | * 如果是新增,返回{type:‘create’,stat:文件stat},如果是删除,返回{type:‘delete’,stat:undefined}
115 | * @param {path} filepath 文件路径
116 | * @return {Object} [description]
117 | */
118 | function _checkPath(filepath){
119 | var stat,type = 'create';
120 | try {
121 | stat = fs.statSync(filepath);
122 | } catch (e) {
123 | if(e.code === 'ENOENT'){
124 | type = 'delete';
125 | } else {
126 | console.error(e);
127 | }
128 | }
129 | return {
130 | type: type,
131 | stat: stat
132 | }
133 | }
134 |
135 | localfile.prototype.check = function(){
136 | return true;
137 | };
138 |
139 | localfile.prototype.createRepository = function(repository, cbk){
140 | var dirpath = path.resolve(this.dir, repository);
141 | fs.mkdir(dirpath, cbk);
142 | };
143 |
144 | localfile.prototype.listPackageInfo = function(repository, name, cbk){
145 | fs.stat(path.join(this.dir, repository, name), cbk);
146 | };
147 |
148 | localfile.prototype.get = function(repository, name, res){
149 | var filepath = path.join(this.dir, repository, name);
150 | fs.access(filepath, fs.R_OK, function(err){
151 | if(err){
152 | res.status(404).end(name + ' not exist!')
153 | } else {
154 | res.setHeader('modulename', name);
155 | res.download(filepath);
156 | }
157 | });
158 | };
159 |
160 | localfile.prototype.put = function(repository, name, stream, cbk){
161 | var filepath = path.resolve(this.dir, repository, name);
162 | target = fs.createWriteStream(filepath);
163 | stream.pipe(target)
164 | .on('error', function(err){
165 | cbk(err);
166 | })
167 | .on('end', function(){
168 | cbk();
169 | });
170 | };
171 |
172 | module.exports = localfile;
173 |
--------------------------------------------------------------------------------
/src/js/app/code/storage/swift.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: wyw.wang
3 | * @Date: 2016-10-11 18:04
4 | * @Email: wyw.wang@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2017-05-08 10:37
7 | */
8 |
9 | var _ = require('lodash'),
10 | cache = require('../cache'),
11 | Swift = require('../../../lib/swiftClient'),
12 | Utils = require('../../../common/utils');
13 |
14 | function swift(config, snapshotConfig, swiftTokenTimeout, envOps){
15 | var self = this;
16 | //SNAPSHOT版本
17 | this.snapshot = init(snapshotConfig, true);
18 | //正式版本
19 | this.release = init(config, false);
20 |
21 | //当使用ceph存储时,获取资源的url里不需要拼接账户信息,ceph兼容swift的协议
22 | this.ceph = envOps ? envOps.ceph === true || envOps.ceph == 'true' : false;
23 |
24 | function init(opts, isSnapshot) {
25 | //swiftTokenTimeout,swift可以设置客户端连接上去后token的时效性
26 | var host = opts[0].split(':'),
27 | rs = {
28 | avaliable: false,
29 | config: {
30 | host: host[0],
31 | user: opts[1],
32 | pass: opts[2],
33 | port: host[1] || 80,
34 | swiftTokenTimeout: swiftTokenTimeout
35 | }
36 | };
37 | rs.user = rs.config.user.split(':')[0];
38 | rs.storage = new Swift(rs.config, function(err, res){
39 | if(err) {
40 | handleInitError(err);
41 | } else if(rs.storage.token){
42 | rs.avaliable = true;
43 | self.init(isSnapshot);
44 | } else {
45 | handleInitError(new Error('Request token fail:'+res.statusCode));
46 | }
47 | });
48 | return rs;
49 | }
50 | }
51 |
52 | swift.prototype.init = function(isSnapshot){
53 | var self = this;
54 | self.listRepository(isSnapshot, function(err, list){
55 | if(err){
56 | handleInitError(err);
57 | return;
58 | }
59 | _.forEach(list, function(el){
60 | var repository = el.name;
61 | cache.addRepository(isSnapshot, repository, {
62 | size: el.bytes,
63 | count: el.count
64 | });
65 | self.listPackages(isSnapshot, repository, function(e, pcks){
66 | if(e){
67 | handleInitError(e);
68 | return;
69 | }
70 | _.forEach(pcks, function(pl){
71 | cache.addPackage(isSnapshot, repository, pl.name);
72 | });
73 | });
74 | });
75 | });
76 | };
77 |
78 | swift.prototype.sync = function(){
79 | var self = this;
80 | cache.clear().then(function(){
81 | console.info('重新加载本地缓存');
82 | //SNAPSHOT
83 | self.init(true);
84 | //RELEASE
85 | self.init(false);
86 | });
87 | };
88 |
89 | swift.prototype.check = function(){
90 | return this.snapshot.avaliable && this.release.avaliable;
91 | };
92 |
93 | swift.prototype.createRepository = function(isSnapshot, repository, cbk){
94 | this.getConfig(isSnapshot).storage.createContainer(repository, function(err, res){
95 | if(err) {
96 | cbk(err);
97 | } else {
98 | cache.addRepository(isSnapshot, repository);
99 | cbk(null, res);
100 | }
101 | });
102 | };
103 |
104 | swift.prototype.listRepository = function(isSnapshot, cbk){
105 | // eg:[{"count": 5, "bytes": 36464, "name": "template"}]
106 | this.getConfig(isSnapshot).storage.listContainers(cbk);
107 | };
108 |
109 | swift.prototype.listPackages = function(isSnapshot, repository, cbk){
110 | // eg: [{"hash": "9f6e6800cfae7749eb6c486619254b9c", "last_modified": "2016-08-11T07:20:38.174980", "bytes": 3, "name": "/abssc/11.txt", "content_type": "text/plain"},{..}]
111 | this.getConfig(isSnapshot).storage.listObjects(repository, cbk);
112 | };
113 |
114 | swift.prototype.listPackageInfo = function(isSnapshot, repository, name, cbk){
115 | this.getConfig(isSnapshot).storage.retrieveObjectMetadata(repository, name, function(err, res){
116 | if(err || res.statusCode !== 200){
117 | cbk(err || {
118 | statusCode: res.statusCode,
119 | message: res.body
120 | });
121 | return;
122 | }
123 | var header = res.headers;
124 | /*headers:
125 | { server: 'openresty/1.7.10.2',
126 | date: 'Fri, 14 Oct 2016 07:38:13 GMT',
127 | 'content-type': 'image/jpeg',
128 | 'content-length': '41243',
129 | connection: 'close',
130 | 'accept-ranges': 'bytes',
131 | 'last-modified': 'Mon, 11 Jul 2016 03:25:00 GMT',
132 | etag: '9e5d2e6ab7f7dbc18bb1c30ad1deb98a',
133 | 'x-timestamp': '1468207499.97934',
134 | 'x-object-meta-mtime': '1468207479.850734',
135 | 'x-trans-id': 'txad18a3ae328b4725b4cd0-0058008b65',
136 | expires: 'Thu, 31 Dec 2037 23:55:55 GMT',
137 | 'cache-control': 'max-age=315360000' }
138 | */
139 | cbk(null, {
140 | size: header['content-length'],
141 | birthtime: null,
142 | mtime: new Date(header['last-modified'])
143 | });
144 | });
145 | };
146 |
147 | swift.prototype.get = function(repository, name, res){
148 | var opts = this.getConfig(name), url;
149 | if(this.ceph){
150 | url = ['http:/', opts.config.host, repository, name].join('/');
151 | }else{
152 | url = ['http:/', opts.config.host, opts.user, repository, name].join('/');
153 | }
154 | if(res){
155 | res.setHeader('modulename', name);
156 | res.redirect(url);
157 | }else{
158 | return url;
159 | }
160 | };
161 |
162 | swift.prototype.put = function(repository, name, stream, cbk){
163 | var storage = this.getConfig(name).storage;
164 | storage.createObjectWithStream(repository, name, stream, function(err){
165 | if(err) {
166 | cbk(err);
167 | } else {
168 | //由于存在上传swift后仍然有不存在的情况,但缓存里已经记录,导致最终获取失败,故增加校验
169 | storage.retrieveObjectMetadata(repository, name, function(err, res){
170 | if(err){
171 | cbk(err);
172 | }else if(res && res.statusCode == 200){
173 | cache.addPackage(Utils.isSnapshot(name), repository, name);
174 | cbk();
175 | }else{
176 | cbk(new Error('upload ' + name + ' fail: retrieve object not found!!!' + (res && res.statusCode)));
177 | }
178 | });
179 | }
180 | });
181 | };
182 |
183 | swift.prototype.getConfig = function(name) {
184 | if(typeof name == 'boolean'){
185 | return name ? this.snapshot : this.release;
186 | }
187 | return Utils.isSnapshot(name) ? this.snapshot : this.release;
188 | };
189 |
190 | /**
191 | * 处理初始化阶段的错误
192 | * @param {[type]} err [description]
193 | * @return {[type]} [description]
194 | */
195 | function handleInitError(err){
196 | console.error(err);
197 | }
198 |
199 | module.exports = swift;
200 |
--------------------------------------------------------------------------------
/src/js/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Robinlim/npm_cache_share/5854187e4d0be6aa33a55d2c1058393dbef03484/src/js/app/favicon.ico
--------------------------------------------------------------------------------
/src/js/app/healthcheck.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Robinlim/npm_cache_share/5854187e4d0be6aa33a55d2c1058393dbef03484/src/js/app/healthcheck.html
--------------------------------------------------------------------------------
/src/js/app/index.js:
--------------------------------------------------------------------------------
1 | var nodeAnnotation = require('node-annotation'),
2 | path = require('path');
3 |
4 |
5 | /**
6 | * 配置全局错误处理
7 | * [Function] 错误处理函数
8 | */
9 | nodeAnnotation.setGlobalErrorHandler(function(err){
10 | console.error(err.stack || err);
11 | });
12 |
13 | /**
14 | * 配置node-annotation内的日志流出
15 | * [Boolean] 是否开启日志,默认true
16 | * [String] "error/warn/info/log" 输出日至级别,默认warn
17 | * [Function/LoggerObject] 日志处理函数或对象(类似log4js的Logger对象),默认为console
18 | */
19 | nodeAnnotation.setLogger(true, 'error', function(str, level) {
20 | console.error('[ERROR]', str);
21 | });
22 |
23 | nodeAnnotation.start(path.resolve(__dirname, 'code'), function(){
24 | var app = require('./app');
25 | nodeAnnotation.app(app);
26 | var server = app.listen(process.env.port || '8888', function() {
27 | console.info('Express server listening on port ', server.address().port);
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/src/js/app/public/scripts/strategy/index.js:
--------------------------------------------------------------------------------
1 | var createNew = document.getElementById("js-create-new"), //新增策略按钮
2 | createDialog = document.getElementById("js-create-dialog"), //新增策略弹框
3 | tableList = document.getElementById("js-table-list"),
4 | myForm = document.getElementById("js-create-form");
5 |
6 | var urlObj = {
7 | listUrl: "/strategy/api/list",
8 | addUrl: "/strategy/api/add",
9 | removeUrl: "/strategy/api/remove?moduleName={name}"
10 | };
11 |
12 | var strategy = {
13 | init: function() {
14 | this.getList();
15 | this.bindEvent();
16 | },
17 | bindEvent: function() {
18 | var self = this;
19 |
20 | createNew.addEventListener('click', function() {
21 | self.renderForm({
22 | moduleName: '',
23 | alwaysUpdate: false,
24 | postInstall: false,
25 | ignoreCache: false,
26 | blackList: false,
27 | postInstallVal: ''
28 | });
29 | if(createDialog.style.display === 'none'){
30 | createDialog.style.display = 'block';
31 | }
32 | });
33 |
34 | createDialog.addEventListener('click', function(e) {
35 |
36 | if(e.target.classList.contains('js-confirm-dialog')) {
37 | self.createStg();
38 | }
39 |
40 | if(e.target.classList.contains('js-close-dialog') ||
41 | e.target.classList.contains('js-cancel-dialog')) {
42 | createDialog.style.display = 'none';
43 | }
44 |
45 | if(e.target.classList.contains('js-change-post')){
46 | if(!e.target.checked){
47 | myForm.elements.postInstallVal.value = '';
48 | }
49 | myForm.elements.postInstallVal.disabled = !e.target.checked;
50 | }
51 | });
52 |
53 | tableList.addEventListener('click', function(e) {
54 |
55 | if(e.target.classList.contains('js-stg-update')) {
56 | self.modifyStg(e.target);
57 | }
58 |
59 | if(e.target.classList.contains('js-stg-del')) {
60 | self.delStg(e.target);
61 | }
62 | });
63 | },
64 | getList: function() {
65 | var self = this;
66 | self.sendAjax('get', urlObj.listUrl, null, function(res) {
67 | if(res && res.status === 200) {
68 | self.modules = res.modules;
69 | self.renderListData(res.modules);
70 | }
71 | }, function(res) {
72 | alert(res.message);
73 | });
74 | },
75 | renderListData: function(data) {
76 | var html = [];
77 | for(var key in data) {
78 | if(data[key]['alwaysUpdate'] || data[key]['ignoreCache'] || data[key]['postInstall'] || data[key]['blackList']) {
79 | html.push(
80 | '' +
81 | '' + key + ' | ' +
82 | '' + (data[key]['alwaysUpdate'] || '') + ' | ' +
83 | '' + (data[key]['ignoreCache'] || '') + ' | ' +
84 | '' + (data[key]['postInstall'] || '') + ' | ' +
85 | '' + (data[key]['blackList'] || '') + ' | ' +
86 | '' +
87 | '修改' +
88 | '删除' +
89 | ' | ' +
90 | '
'
91 | )
92 | }
93 | }
94 |
95 | tableList.innerHTML = html.join('');
96 | },
97 | createStg: function() {
98 |
99 | var self = this,
100 | param = self.beforeSubmit();
101 |
102 | if(param) {
103 | self.sendAjax('post', urlObj.addUrl, JSON.stringify(param), function(res) {
104 | if(res && res.status === 200) {
105 | createDialog.style.display = 'none';
106 | myForm.reset();
107 | self.getList();
108 | }
109 | }, function(res) {
110 | alert(res.message);
111 | });
112 | }
113 | },
114 | modifyStg: function(target) {
115 | myForm.reset();
116 | createDialog.style.display = 'block';
117 | var id = target.getAttribute('data-id'),
118 | curRowData = this.modules[id];
119 | this.renderForm({
120 | moduleName: id,
121 | alwaysUpdate: curRowData.alwaysUpdate == 1,
122 | ignoreCache: curRowData.ignoreCache == 1,
123 | blackList: curRowData.blackList == 1,
124 | postInstall: !!curRowData.postInstall,
125 | postInstallVal: curRowData.postInstall || ''
126 | });
127 | myForm.elements.moduleName.disabled = true;
128 | },
129 | delStg: function(target) {
130 | var self = this;
131 | var stgId = target.getAttribute('data-id');
132 | var removeUrl = urlObj.removeUrl.replace('{name}', stgId);
133 | if(confirm('是否确定删除?')){
134 | self.sendAjax('get', removeUrl, null, function(res) {
135 | if(res && res.status === 200) {
136 | self.getList();
137 | // var item = target.parentNode.parentNode;
138 | // tableList.removeChild(item);
139 | }
140 | }, function(res) {
141 | alert(res.message);
142 | });
143 | } else {
144 | return;
145 | }
146 | },
147 | renderForm: function(formData) {
148 | if(!formData) {
149 | return;
150 | }
151 | myForm.elements.moduleName.value = formData.moduleName;
152 | myForm.elements.moduleName.disabled = false;
153 | myForm.elements.alwaysUpdate.checked = formData.alwaysUpdate;
154 | myForm.elements.ignoreCache.checked = formData.ignoreCache;
155 | myForm.elements.blackList.checked = formData.blackList;
156 | myForm.elements.postInstall.checked = formData.postInstall;
157 | myForm.elements.postInstallVal.value = formData.postInstall ? formData.postInstallVal : '';
158 | myForm.elements.postInstallVal.disabled = !formData.postInstall;
159 | },
160 | beforeSubmit: function() {
161 | var el = myForm.elements;
162 |
163 | if(el.moduleName.value === '') {
164 | alert('请输入模块名');
165 | return null;
166 | }
167 |
168 | if(!el.alwaysUpdate.checked && !el.ignoreCache.checked && !el.postInstall.checked && !el.blackList.checked) {
169 | alert('请选择策略');
170 | return null;
171 | }
172 |
173 | if(el.postInstall.checked && el.postInstallVal.value === '') {
174 | alert('请输入postInstall 的策略值');
175 | return null;
176 | }
177 |
178 | var formData = {
179 | moduleName: el.moduleName.value,
180 | strategy: {
181 | alwaysUpdate: el.alwaysUpdate.checked ? 1 : 0,
182 | ignoreCache: el.ignoreCache.checked ? 1 : 0,
183 | blackList: el.blackList.checked ? 1 : 0,
184 | postInstall: el.postInstall.checked ? el.postInstallVal.value : 0
185 | }
186 | };
187 |
188 | return formData;
189 | },
190 | sendAjax: function(method, url, param, successCallBack, errorCallBack) {
191 |
192 | var xhr = new XMLHttpRequest();
193 |
194 | xhr.open(method, url, true);
195 | xhr.setRequestHeader('Content-Type', 'application/json;charset=utf-8');
196 | xhr.responseType = 'json';
197 |
198 | xhr.onload = function() {
199 | if(this.status == 200){
200 | if(this.response.status == 200){
201 | successCallBack && successCallBack(this.response);
202 | }else{
203 | errorCallBack && errorCallBack(this.response.message);
204 | }
205 | } else {
206 | errorCallBack && errorCallBack(this.response);
207 | }
208 | };
209 | xhr.onerror = function(e) {
210 | errorCallBack && errorCallBack(e);
211 | }
212 | var type = method.toLowerCase();
213 | if(type === 'post') {
214 | xhr.send(param);
215 | }
216 |
217 | if(type === 'get') {
218 | xhr.send();
219 | }
220 | }
221 | }
222 |
223 | strategy.init();
224 |
--------------------------------------------------------------------------------
/src/js/app/public/styles/strategy.css:
--------------------------------------------------------------------------------
1 | html,body,div,table,th,tr,td,ul,li,button{ margin:0; padding:0;}
2 | a{ color: #555; text-decoration: none;}
3 | input{ outline: none;}
4 | html,body{ color: #48576a;}
5 | body {
6 | padding: 80px;
7 | font: 13px "Helvetica Neue", "Lucida Grande", "Arial";
8 | background: #ECE9E9 -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#ECE9E9));
9 | background: #ECE9E9 -moz-linear-gradient(top, #fff, #ECE9E9);
10 | background-repeat: no-repeat;
11 | color: #555;
12 | -webkit-font-smoothing: antialiased;
13 | }
14 | .m-wrap{
15 | margin-left: 200px;
16 | }
17 | /*left menu*/
18 | .m-left-menu{
19 | float: left;
20 | width: 200px;
21 | font-size: 18px;
22 | font-weight: bold;
23 | }
24 | .m-left-menu ul{
25 | width: 100%;
26 | overflow: hidden;
27 | }
28 | .m-left-menu li{
29 | line-height: 40px;
30 | }
31 | .m-left-menu li a{
32 | color: #a6a7a6;
33 | }
34 | .m-left-menu li a:before{
35 | content: '';
36 | display: inline-block;
37 | width: 3px;
38 | height: 12px;
39 | border-left: 2px solid #a6a7a6;
40 | margin-right: 5px;
41 | }
42 | .m-left-menu .cur{
43 | color: #000;
44 | }
45 | .m-left-menu .cur:before{
46 | border-left: 2px solid #000;
47 | }
48 | .m-title{
49 | font-size: 18px;
50 | color: #555;
51 | }
52 | .m-box{
53 | margin: 15px 0;
54 | }
55 | .m-table{
56 | position: relative;
57 | overflow: hidden;
58 | box-sizing: border-box;
59 | width: 100%;
60 | max-width: 100%;
61 | background-color: #fff;
62 | border: 1px solid #dfe6ec;
63 | font-size: 13px;
64 | color: #1f2d3d;
65 | }
66 | .m-table tr{
67 | background: #fff;
68 | }
69 | .m-table tr:hover{
70 | background: #ECE9E9;
71 | }
72 | .m-table thead tr:hover{
73 | background: #fff;
74 | }
75 | .m-table th,.m-table td{
76 | height: 34px;
77 | min-width: 0;
78 | box-sizing: border-box;
79 | text-overflow: ellipsis;
80 | vertical-align: middle;
81 | position: relative;
82 | }
83 | .m-table th{
84 | white-space: nowrap;
85 | overflow: hidden;
86 | border-bottom: 1px solid #dfe6ec;
87 | border-right: 1px solid #dfe6ec;
88 | }
89 | .m-table td{
90 | border-bottom: 1px solid #dfe6ec;
91 | border-right: 1px solid #dfe6ec;
92 | }
93 | .m-table tr:last-child td{
94 | border-bottom: 0;
95 | }
96 | .m-table td:last-child{
97 | border-right: 0;
98 | }
99 | .m-table .cell {
100 | box-sizing: border-box;
101 | overflow: hidden;
102 | text-overflow: ellipsis;
103 | white-space: normal;
104 | word-break: break-all;
105 | line-height: 24px;
106 | padding: 0 18px;
107 | text-align: center;
108 | }
109 | .m-table .column_5{
110 | text-align: center;
111 | }
112 |
113 | .m-button{
114 | display: inline-block;
115 | line-height: 1;
116 | white-space: nowrap;
117 | cursor: pointer;
118 | background: #000;
119 | border: 0px solid #000;
120 | border-color: #c4c4c4;
121 | color: #fff;
122 | -webkit-appearance: none;
123 | text-align: center;
124 | box-sizing: border-box;
125 | outline: none;
126 | margin: 0;
127 | padding: 10px 15px;
128 | font-size: 13px;
129 | border-radius: 3px;
130 | }
131 | .m-button-small{
132 | background: none;
133 | outline: none;
134 | border: none;
135 | padding: 7px 9px;
136 | font-size: 12px;
137 | border-radius: 4px;
138 | color: #20a0ff;
139 | }
140 | .m-button-primary{
141 | margin-left: 10px;
142 | color: #fff;
143 | background-color: #929ea7;
144 | border-color: #929ea7;
145 | }
146 | .m-button:hover{
147 | border-color:#222;
148 | color: #eee
149 | }
150 | .m-button-primary:hover{
151 | color: #fff;
152 | }
153 | .m-dialog-wrap{
154 | position: fixed;
155 | left: 0;
156 | top: 0;
157 | width: 100%;
158 | height: 100%;
159 | background: rgba(0,0,0,.5);
160 | }
161 | .m-dialog{
162 | position: absolute;
163 | width: 40%;
164 | left: 50%;
165 | top: 50%;
166 | transform: translateX(-50%) translateY(-50%);
167 | background: #fff;
168 | border-radius: 2px;
169 | box-shadow: 0 1px 3px rgba(0,0,0,.3);
170 | box-sizing: border-box;
171 | margin-bottom: 50px;
172 | }
173 | .m-dialog .close{
174 | position: absolute;
175 | right: 0;
176 | top: 0;
177 | display: block;
178 | width: 50px;
179 | height: 50px;
180 | line-height: 50px;
181 | color: #bfcbd9;
182 | font-size: 30px;
183 | text-align: center;
184 | cursor: pointer;
185 | }
186 | .m-dialog-title{
187 | padding: 20px 20px 0;
188 | line-height: 1;
189 | font-size: 16px;
190 | font-weight: 700;
191 | color: #1f2d3d;
192 | }
193 | .m-dialog-content{
194 | padding: 30px 40px;
195 | color: #48576a;
196 | font-size: 14px;
197 | }
198 | .m-dialog-content .m-dialog-item{
199 | padding: 12px 0;
200 | }
201 | .m-dialog-content .m-el-item{
202 | display: inline-block;
203 | margin-left: 20px;
204 | }
205 | .m-dialog-content .m-el-radio{
206 | margin-right: 10px;
207 | }
208 | .m-dialog-content .m-el-input{
209 | width: 200px;
210 | padding: 7px 10px;
211 | border: 1px solid #d0d0d0;
212 | border-radius: 4px;
213 | }
214 | .m-dialog-content .m-el-input[disabled]{
215 | background: #efefef;
216 | }
217 | .m-dialog-footer{
218 | padding: 10px 20px 15px;
219 | text-align: right;
220 | box-sizing: border-box;
221 | }
222 |
--------------------------------------------------------------------------------
/src/js/app/public/tree/directory.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | listing directory <%= directory %>
7 |
8 |
74 |
75 |
76 |
77 |
83 |
84 |
85 |
86 | <%= files %>
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/src/js/app/public/tree/icons/box.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Robinlim/npm_cache_share/5854187e4d0be6aa33a55d2c1058393dbef03484/src/js/app/public/tree/icons/box.png
--------------------------------------------------------------------------------
/src/js/app/public/tree/icons/drive.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Robinlim/npm_cache_share/5854187e4d0be6aa33a55d2c1058393dbef03484/src/js/app/public/tree/icons/drive.png
--------------------------------------------------------------------------------
/src/js/app/public/tree/icons/folder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Robinlim/npm_cache_share/5854187e4d0be6aa33a55d2c1058393dbef03484/src/js/app/public/tree/icons/folder.png
--------------------------------------------------------------------------------
/src/js/app/public/tree/info.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | package info <%= name %>
7 |
8 |
9 |
10 | <%= name %>
11 |
12 | size | <%= size %> |
13 | create_time | <%= create_time %> |
14 | last_modified_time | <%= last_modified_time %> |
15 | download package | download |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/js/app/public/tree/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | outline: 0;
5 | }
6 |
7 | body {
8 | padding: 80px;
9 | font: 13px "Helvetica Neue", "Lucida Grande", "Arial";
10 | background: #ECE9E9 -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#ECE9E9));
11 | background: #ECE9E9 -moz-linear-gradient(top, #fff, #ECE9E9);
12 | background-repeat: no-repeat;
13 | color: #555;
14 | -webkit-font-smoothing: antialiased;
15 | }
16 | h1, h2, h3 {
17 | font-size: 22px;
18 | color: #343434;
19 | }
20 | h1 em, h2 em {
21 | padding: 0 5px;
22 | font-weight: normal;
23 | }
24 | h1 {
25 | font-size: 60px;
26 | }
27 | h2 {
28 | margin-top: 10px;
29 | }
30 | h3 {
31 | margin: 5px 0 10px 0;
32 | padding-bottom: 5px;
33 | border-bottom: 1px solid #eee;
34 | font-size: 18px;
35 | }
36 | ul li {
37 | list-style: none;
38 | }
39 | ul li:hover {
40 | cursor: pointer;
41 | color: #2e2e2e;
42 | }
43 | ul li .path {
44 | padding-left: 5px;
45 | font-weight: bold;
46 | }
47 | ul li .line {
48 | padding-right: 5px;
49 | font-style: italic;
50 | }
51 | ul li:first-child .path {
52 | padding-left: 0;
53 | }
54 | p {
55 | line-height: 1.5;
56 | }
57 | a {
58 | color: #555;
59 | text-decoration: none;
60 | }
61 | a:hover {
62 | color: #303030;
63 | }
64 | #stacktrace {
65 | margin-top: 15px;
66 | }
67 | .directory h1 {
68 | margin-bottom: 15px;
69 | font-size: 18px;
70 | }
71 | ul#files {
72 | width: 100%;
73 | height: 100%;
74 | overflow: hidden;
75 | }
76 | ul#files li {
77 | float: left;
78 | width: 30%;
79 | line-height: 25px;
80 | margin: 1px;
81 | }
82 | ul#files li a {
83 | display: block;
84 | height: 25px;
85 | border: 1px solid transparent;
86 | -webkit-border-radius: 5px;
87 | -moz-border-radius: 5px;
88 | border-radius: 5px;
89 | overflow: hidden;
90 | white-space: nowrap;
91 | }
92 | ul#files li a:focus,
93 | ul#files li a:hover {
94 | background: rgba(255,255,255,0.65);
95 | border: 1px solid #ececec;
96 | }
97 | ul#files li a.highlight {
98 | -webkit-transition: background .4s ease-in-out;
99 | background: #ffff4f;
100 | border-color: #E9DC51;
101 | }
102 | #search {
103 | display: block;
104 | position: fixed;
105 | top: 20px;
106 | right: 20px;
107 | width: 90px;
108 | -webkit-transition: width ease 0.2s, opacity ease 0.4s;
109 | -moz-transition: width ease 0.2s, opacity ease 0.4s;
110 | -webkit-border-radius: 32px;
111 | -moz-border-radius: 32px;
112 | -webkit-box-shadow: inset 0px 0px 3px rgba(0, 0, 0, 0.25), inset 0px 1px 3px rgba(0, 0, 0, 0.7), 0px 1px 0px rgba(255, 255, 255, 0.03);
113 | -moz-box-shadow: inset 0px 0px 3px rgba(0, 0, 0, 0.25), inset 0px 1px 3px rgba(0, 0, 0, 0.7), 0px 1px 0px rgba(255, 255, 255, 0.03);
114 | -webkit-font-smoothing: antialiased;
115 | text-align: left;
116 | font: 13px "Helvetica Neue", Arial, sans-serif;
117 | padding: 4px 10px;
118 | border: none;
119 | background: transparent;
120 | margin-bottom: 0;
121 | outline: none;
122 | opacity: 0.7;
123 | color: #888;
124 | }
125 | #search:focus {
126 | width: 120px;
127 | opacity: 1.0;
128 | }
129 |
130 | /*views*/
131 | #files span {
132 | display: inline-block;
133 | overflow: hidden;
134 | text-overflow: ellipsis;
135 | text-indent: 10px;
136 | }
137 | #files .name {
138 | background-repeat: no-repeat;
139 | }
140 | #files .icon .name {
141 | text-indent: 28px;
142 | }
143 |
144 | /*tiles*/
145 | .view-tiles .name {
146 | width: 100%;
147 | background-position: 8px 5px;
148 | }
149 | .view-tiles .size,
150 | .view-tiles .date {
151 | display: none;
152 | }
153 |
154 | /*details*/
155 | ul#files.view-details li {
156 | float: none;
157 | display: block;
158 | width: 90%;
159 | }
160 | ul#files.view-details li.header {
161 | height: 25px;
162 | background: #000;
163 | color: #fff;
164 | font-weight: bold;
165 | }
166 | .view-details .header {
167 | border-radius: 5px;
168 | }
169 | .view-details .name {
170 | width: 40%;
171 | background-position: 8px 5px;
172 | }
173 | .view-details .size {
174 | width: 10%;
175 | }
176 | .view-details .date {
177 | width: 30%;
178 | }
179 | .view-details .size,
180 | .view-details .date {
181 | text-align: right;
182 | direction: rtl;
183 | }
184 |
185 | .button {
186 | line-height: 24px;
187 | padding: 2px;
188 | background-color: #ddd;
189 | border: 1px solid black;
190 | border-radius: 5px;
191 | cursor: pointer;
192 | }
193 |
194 | #info-table td {
195 | padding: 2px;
196 | }
197 | /*left menu*/
198 | .m-left-menu{
199 | float: left;
200 | width: 200px;
201 | font-size: 18px;
202 | font-weight: bold;
203 | }
204 | .m-left-menu ul{
205 | width: 100%;
206 | overflow: hidden;
207 | }
208 | .m-left-menu li{
209 | line-height: 40px;
210 | }
211 | .m-left-menu li a{
212 | color: #a6a7a6;
213 | }
214 | .m-left-menu li a:before{
215 | content: '';
216 | display: inline-block;
217 | width: 3px;
218 | height: 12px;
219 | border-left: 2px solid #a6a7a6;
220 | margin-right: 5px;
221 | }
222 | .m-left-menu .cur{
223 | color: #000;
224 | }
225 | .m-left-menu .cur:before{
226 | border-left: 2px solid #000;
227 | }
228 | .main-content{
229 | margin-left: 200px;
230 | }
231 | /*mobile*/
232 | @media (max-width: 768px) {
233 | body {
234 | font-size: 13px;
235 | line-height: 16px;
236 | padding: 0;
237 | }
238 | .main-content{
239 | margin-left: 0;
240 | }
241 | #search {
242 | position: static;
243 | width: 100%;
244 | font-size: 2em;
245 | line-height: 1.8em;
246 | text-indent: 10px;
247 | border: 0;
248 | border-radius: 0;
249 | padding: 10px 0;
250 | margin: 0;
251 | }
252 | #search:focus {
253 | width: 100%;
254 | border: 0;
255 | opacity: 1;
256 | }
257 | .directory h1 {
258 | font-size: 2em;
259 | line-height: 1.5em;
260 | color: #fff;
261 | background: #000;
262 | padding: 15px 10px;
263 | margin: 0;
264 | }
265 | ul#files {
266 | border-top: 1px solid #cacaca;
267 | }
268 | ul#files li {
269 | float: none;
270 | width: auto !important;
271 | display: block;
272 | border-bottom: 1px solid #cacaca;
273 | font-size: 2em;
274 | line-height: 1.2em;
275 | text-indent: 0;
276 | margin: 0;
277 | }
278 | ul#files li:nth-child(odd) {
279 | background: #e0e0e0;
280 | }
281 | ul#files li a {
282 | height: auto;
283 | border: 0;
284 | border-radius: 0;
285 | padding: 15px 10px;
286 | }
287 | ul#files li a:focus,
288 | ul#files li a:hover {
289 | border: 0;
290 | }
291 | #files .header,
292 | #files .size,
293 | #files .date {
294 | display: none !important;
295 | }
296 | #files .name {
297 | float: none;
298 | display: inline-block;
299 | width: 100%;
300 | text-indent: 0;
301 | background-position: 0 50%;
302 | }
303 | #files .icon .name {
304 | text-indent: 41px;
305 | }
306 | .m-left-menu{
307 | float: none;
308 | width: 100%;
309 | font-size: 2em;
310 | padding: 1em 0;
311 | }
312 | .m-left-menu li{
313 | float: left;
314 | width: 50%;
315 | }
316 | .m-left-menu li a{
317 | margin: 0 10%;
318 | }
319 | }
320 |
--------------------------------------------------------------------------------
/src/js/app/views/strategy.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 模块策略管理
7 |
8 |
9 |
10 |
11 |
17 |
18 |
模块策略管理
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | 模块名
33 | |
34 |
35 | 策略名
36 | |
37 |
38 | 操作
39 | |
40 |
41 |
42 |
43 | 忽略本地缓存
44 | |
45 |
46 | 强制安装
47 | |
48 |
49 | 安装后执行
50 | |
51 |
52 | 黑名单
53 | |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | 新增策略
66 | ×
67 |
68 |
91 |
95 |
96 |
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/src/js/app/widget/render.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 |
5 | var _ = require('lodash'),
6 | escapeHtml = require('escape-html');
7 |
8 | var fs = require('fs')
9 | , path = require('path')
10 | , normalize = path.normalize
11 | , sep = path.sep
12 | , extname = path.extname
13 | , join = path.join;
14 |
15 | /*!
16 | * Icon cache.
17 | */
18 |
19 | var cache = {};
20 |
21 |
22 | /*!
23 | * Stylesheet.
24 | */
25 |
26 | var defaultStylesheet = fs.readFileSync(join(__dirname, '../public/tree/style.css'), 'utf8');
27 |
28 | module.exports = {
29 | templatesPath: {
30 | directory: join(__dirname, '../public/tree/directory.html'),
31 | info : join(__dirname, '../public/tree/info.html')
32 | },
33 | templates: {
34 | directory: null,
35 | info: null
36 | },
37 | renderDirectory: function(params, res){
38 | if(!this.templates.directory){
39 | var str = fs.readFileSync(this.templatesPath.directory, 'utf8');
40 | this.templates.directory = _.template(str);
41 | }
42 | // create locals for rendering
43 | var locals = {
44 | style: defaultStylesheet.concat(iconStyle(['drive','box','folder'], true)),
45 | files: createHtmlFileList(params.fileList, params.title, true, params.view),
46 | directory: escapeHtml(params.title),
47 | backpath: params.backpath,
48 | backname: params.backname
49 | };
50 | var buf = new Buffer(this.templates.directory(locals), 'utf8');
51 | res.setHeader('Content-Type', 'text/html; charset=utf-8');
52 | res.setHeader('Content-Length', buf.length);
53 | res.end(buf);
54 | },
55 | renderInfo: function(params, res){
56 | if(!this.templates.info){
57 | var str = fs.readFileSync(this.templatesPath.info, 'utf8');
58 | this.templates.info = _.template(str);
59 | }
60 | var locals = {
61 | style: defaultStylesheet.concat(iconStyle(['box'], true)),
62 | name: params.name,
63 | repository: params.repository,
64 | versionType: params.versionType,
65 | size: (params.stat.size/1024).toFixed(2) + 'Kb',
66 | create_time: params.stat.birthtime?params.stat.birthtime.toLocaleString():'',
67 | last_modified_time: params.stat.mtime.toLocaleString()
68 | }
69 | var buf = new Buffer(this.templates.info(locals), 'utf8');
70 | res.setHeader('Content-Type', 'text/html; charset=utf-8');
71 | res.setHeader('Content-Length', buf.length);
72 | res.end(buf);
73 | }
74 | };
75 |
76 | /**
77 | * Map html `files`, returning an html unordered list.
78 | * @private
79 | */
80 |
81 | function createHtmlFileList(files, dir, useIcons, view) {
82 | var html = ''
83 | + (view == 'details' ? (
84 | '') : '');
90 |
91 | html += files.sort(fileSort).map(function (file) {
92 | var classes = [];
93 | var isDir = true;
94 | var path = dir.split('/').map(function (c) { return encodeURIComponent(c); });
95 |
96 | if (useIcons) {
97 | classes.push('icon');
98 | classes.push('icon-' + file.icon);
99 | }
100 |
101 | path.push(encodeURIComponent(file.name));
102 | var date = file.stat && file.stat.mtime
103 | ? file.stat.mtime.toLocaleTimeString() + ' ' + file.stat.mtime.toLocaleDateString()
104 | : '';
105 |
106 | var size = file.stat && typeof(file.stat.size) !== 'undefined' ? file.stat.size : '',
107 | count = file.stat && typeof(file.stat.count) !== 'undefined' ? file.stat.count : '';
108 | return '- '
112 | + '' + escapeHtml(file.name.replace(/@@@/g,'\/')) + ''
113 | + '' + size + ''
114 | + ''+ count + ''
115 | + '' + escapeHtml(date) + ''
116 | + '
';
117 | }).join('\n');
118 |
119 | html += '
';
120 |
121 | return html;
122 | }
123 |
124 |
125 | /**
126 | * Sort function for with directories first.
127 | */
128 |
129 | function fileSort(a, b) {
130 | return String(a.name).toLocaleLowerCase().localeCompare(String(b.name).toLocaleLowerCase());
131 | }
132 |
133 |
134 | /**
135 | * Load icon images, return css string.
136 | */
137 |
138 | function iconStyle(iconNames, useIcons) {
139 | if (!useIcons) return '';
140 | var className;
141 | var i;
142 | var iconName;
143 | var list = [];
144 | var rules = {};
145 | var selector;
146 | var selectors = {};
147 | var style = '';
148 |
149 | for (i = 0; i < iconNames.length; i++) {
150 | iconName = iconNames[i];
151 | selector = '#files .' + 'icon-' + iconName + ' .name';
152 |
153 | if (!rules[iconName]) {
154 | rules[iconName] = 'background-image: url(data:image/png;base64,' + load(iconName) + ');'
155 | selectors[iconName] = [];
156 | list.push(iconName);
157 | }
158 |
159 | if (selectors[iconName].indexOf(selector) === -1) {
160 | selectors[iconName].push(selector);
161 | }
162 | }
163 |
164 | for (i = 0; i < list.length; i++) {
165 | iconName = list[i];
166 | style += selectors[iconName].join(',\n') + ' {\n ' + rules[iconName] + '\n}\n';
167 | }
168 |
169 | return style;
170 | }
171 |
172 | /**
173 | * Load and cache the given `icon`.
174 | *
175 | * @param {String} icon
176 | * @return {String}
177 | * @api private
178 | */
179 |
180 | function load(icon) {
181 | if (cache[icon]) return cache[icon];
182 | return cache[icon] = fs.readFileSync(path.join(__dirname, '../public/tree/icons/' + icon + '.png'), 'base64');
183 | }
184 |
185 | /**
186 | * Normalizes the path separator from system separator
187 | * to URL separator, aka `/`.
188 | *
189 | * @param {String} path
190 | * @return {String}
191 | * @api private
192 | */
193 |
194 | function normalizeSlashes(path) {
195 | return path.split(sep).join('/');
196 | };
197 |
--------------------------------------------------------------------------------
/src/js/command/clean.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: robin
3 | * @Date: 2016-08-08 17:30:24
4 | * @Email: xin.lin@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2016-09-14 15:31:29
7 | */
8 |
9 | 'use strict'
10 |
11 | var utils = require('../common/utils'),
12 | shellUtils = require('../common/shellUtils'),
13 | path = require('path');
14 |
15 | /*@Command({
16 | "name": "clean",
17 | "alias":"c",
18 | "des":"Clear the local npm module cache",
19 | options:[
20 | ["-s, --forServer", "if it is false, clean the npm cache in client, or clean the server cache"]
21 | ]
22 | })*/
23 | module.exports = {
24 | run: function(ops) {
25 | //清除服务端缓存
26 | if(!ops.forServer){
27 | shellUtils.rm('-rf', utils.getCachePath() + path.sep + '*');
28 | return;
29 | }
30 | //清除客户端缓存
31 | shellUtils.rm('-rf', path.resolve(process.cwd(), 'npm_cache_share'));
32 | process.exit();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/js/command/config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: wyw.wang
3 | * @Date: 2016-10-09 11:44
4 | * @Email: wyw.wang@qunar.com
5 | * @Last modified by: wyw
6 | * @Last modified time: 2016-10-09 11:49
7 | */
8 |
9 | var fs = require('fs'),
10 | fsExtra = require('fs-extra'),
11 | utils = require('../common/utils'),
12 | constant = require('../common/constant');
13 |
14 | /*@Command({
15 | "name": "config [action] [key] [value]",
16 | "alias":"f",
17 | "des":"Set config for npm cache",
18 | options:[]
19 | })*/
20 | module.exports = {
21 | run: function(action, key, value, opts) {
22 | this.configPath = opts.config || utils.getConfigPath();
23 | this.config = fsExtra.readJsonSync(this.configPath);
24 | if(key && constant.CONFIGKEY.indexOf(key) < 0){
25 | console.error('非法的配置键值:', key);
26 | process.exit(1);
27 | }
28 | switch(action){
29 | case 'set':
30 | this.config[key] = value;
31 | fsExtra.writeJsonSync(this.configPath, this.config);
32 | break;
33 | case 'get':
34 | console.log(this.config[key]);
35 | break;
36 | case 'list':
37 | console.log(this.config);
38 | break;
39 | case 'delete':
40 | delete this.config[key];
41 | fsExtra.writeJsonSync(this.configPath, this.config);
42 | break;
43 | default:
44 | console.error('非法的配置操作:', action);
45 | }
46 | process.exit();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/js/command/download.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: wyw.wang
3 | * @Date: 2017-02-08 16:17
4 | * @Email: wyw.wang@qunar.com
5 | * @Last modified by: wyw
6 | * @Last modified time: 2017-02-08 16:19
7 | */
8 |
9 | var swiftUtils = require('../common/swiftUtils'),
10 | utils = require('../common/utils'),
11 | _ = require('lodash');
12 |
13 | var __cwd = process.cwd();
14 | /*@Command({
15 | "name": "download [name] [path]",
16 | "alias":"d",
17 | "des":"Download a static source to repository, you can set the swift setting by parameters or use command 'ncs config set resourceSwift host|user|pass|container'",
18 | options:[
19 | ["-h, --host [host]", "host of swift"],
20 | ["-u, --user [user]", "user of swift"],
21 | ["-p, --pass [pass]", "pass of swift"],
22 | ["-c, --container [container]", "container in swift"]
23 | ]
24 | })*/
25 | module.exports = {
26 | run: function(name, path, options){
27 | try{
28 | if(!path){
29 | this.exit('请指定下载至本地的路径!!!指令为`ncs download [name] [path]`');
30 | }
31 | var params = _.extend({}, swiftUtils.getConfig(options, utils.isSnapshot(name) ? 'resourceSnapshotSwift' : 'resourceSwift'), {
32 | name: name,
33 | path: path,
34 | ceph: options.ceph
35 | });
36 | swiftUtils.download(params, this.exit);
37 | }catch(err){
38 | this.exit(err);
39 | }
40 | },
41 | /**
42 | * 退出
43 | * @return {[type]} [description]
44 | */
45 | exit: function(err){
46 | if(err){
47 | console.error(err);
48 | process.exit(1);
49 | } else {
50 | process.exit(0);
51 | }
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/src/js/command/help.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: robin
3 | * @Date: 2016-08-22 17:30:24
4 | * @Email: xin.lin@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2016-08-22 19:09:20
7 | */
8 |
9 | 'use strict'
10 |
11 | /*@Command("help")*/
12 | module.exports = {
13 | run: function(ops, nomnom) {
14 | nomnom
15 | .printer()
16 | .parse(["-h"]);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/js/command/publish.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: wyw.wang
3 | * @Date: 2016-12-07 10:12
4 | * @Email: wyw.wang@qunar.com
5 | * @Last modified by: xin.lin
6 | * @Last modified time: 2017-11-24 10:13
7 | */
8 |
9 |
10 |
11 | var fs = require('fs'),
12 | path = require('path'),
13 | fstream = require('fstream'),
14 | fsExtra = require('fs-extra'),
15 | utils = require('../common/utils'),
16 | fignore = require('fstream-ignore'),
17 | constant = require('../common/constant'),
18 | npmUtils = require('../common/npmUtils'),
19 | Factory = require('../annotation/Factory'),
20 | checkUtils = require('../common/checkUtils'),
21 | shellUtils = require('../common/shellUtils'),
22 | f2bConfigUtils = require('../common/f2bConfigUtils');
23 |
24 | var __cwd = process.cwd(),
25 | __cache = utils.getCachePath(),
26 | LIBNAME = constant.LIBNAME,
27 | UPLOADDIR = constant.UPLOADDIR,
28 | PACKAGE = 'package.json',
29 | npmPackagePath = path.resolve(__cwd, PACKAGE);
30 | /*@Command({
31 | "name": "publish",
32 | "alias":"p",
33 | "des":"Publish a dir as a package to center cache server,only for node type",
34 | options:[
35 | ["-c, --type [type]", "server type node/npm, default is node", "node"],
36 | ["-e, --repository [repository]", "specify the repository, format as HOST:PORT/REPOSITORY-NAME"],
37 | ["-t, --token [token]", "use the token to access the npm_cache_share server"],
38 | ["-p, --password [password]", "use the password to access certain package"],
39 | ["-r, --registry [registry]", "specify the npm registry"],
40 | ["-v, --moduleVersion [moduleVersion]", "specify the module version"],
41 | ["-s, --snapshot", "specify this is a snapshot version"],
42 | ["-u, --alwaysUpdate", "this module will publish overwrite the same version on the server, and will always update when install, if -s not specify, the version remain unchanged"],
43 | ["-o, --overwrite", "if -s exist, it will overwrite the version into package.json"],
44 | ["--checkSnapshotDeps", "check if or not dependend on the snapshot module, default is ignore check"]
45 | ]
46 | })*/
47 |
48 | module.exports = {
49 | run: function(options){
50 | console.info('******************开始发布******************');
51 | var type = options.type,
52 | packageInfo;
53 | try {
54 | packageInfo = fsExtra.readJsonSync(npmPackagePath);
55 | } catch (e) {
56 | exit(e);
57 | return;
58 | }
59 | if(options.checkSnapshotDeps){
60 | checkUtils.snapshotDepsCheck(packageInfo.dependencies);
61 | }
62 | if(type === 'node'){
63 | console.info('将发布到中央缓存');
64 | this.toNode(options, packageInfo);
65 | } else if (type === 'npm'){
66 | console.info('将发布到npm');
67 | this.toNpm(options);
68 | } else {
69 | this.exit(new Error('不支持的发布类型:'+type+',请指定--type为node或npm'));
70 | }
71 | },
72 | /**
73 | * 发布到node中央缓存
74 | * @param {object} options 输入参数
75 | * @return {[type]} [description]
76 | */
77 | toNode: function(options, packageInfo){
78 | var exit = this.exit;
79 | //校验工程命名
80 | var moduleName = packageInfo.name;
81 | f2bConfigUtils.checkName(moduleName, options.nameReg);
82 |
83 | var moduleVersion = options.moduleVersion || packageInfo.version;
84 | if(utils.isSnapshot(moduleVersion)){
85 | options.snapshot = true;
86 | }else if(options.snapshot){
87 | moduleVersion += (options.SNAPSHOTLINK || '-') + constant.VERSION_TYPE.SNAPSHOT;
88 | }
89 |
90 | if(options.snapshot){
91 | console.info('将发布SNAPSHOT版本,每次发布都会覆盖现有同名版本。');
92 | }else if(options.alwaysUpdate){
93 | console.info('设定该模块本次发布将会覆盖现有同名版本,但客户端已存在的缓存不会更新,可以到管理后台配置策略来强制更新客户端缓存!');
94 | }
95 |
96 | var packageName = utils.getModuleName(moduleName, moduleVersion);
97 | console.info('即将上传的包名称:', packageName.replace(constant.SPLIT, '/'));
98 |
99 |
100 | // info value in form-data must be a string or buffer
101 | var info = {
102 | name: moduleName,
103 | isPrivate: 'on',
104 | isGyp: utils.isGypModule(packageInfo.dependencies, __cwd) ? 'on': 'off',
105 | user: 'default',
106 | password: options.password || ''
107 | };
108 | var uploadDir = path.resolve(__cache, UPLOADDIR);
109 | utils.ensureDirWriteablSync(uploadDir);
110 | var tempDir = path.resolve(uploadDir, packageName);
111 | console.debug('temp upload path:', tempDir);
112 | fsExtra.emptyDirSync(tempDir);
113 |
114 | var source = fignore({
115 | path: __cwd,
116 | ignoreFiles: [".ignore", ".gitignore"]
117 | })
118 | .on('child', function (c) {
119 | console.debug('walking:', c.path.substr(c.root.path.length + 1));
120 | })
121 | .on('error', exit);
122 | //增加忽略文件
123 | source.addIgnoreRules(['.git/', '.gitignore', 'node_modules/']);
124 |
125 | var target = fstream.Writer(tempDir)
126 | .on('error', exit)
127 | .on('close', function(){
128 | //如果是snapshot,则上传的包需要修改package.json里的version
129 | if(options.snapshot){
130 | var pckPath = path.resolve(tempDir, PACKAGE);
131 | packageInfo = fsExtra.readJsonSync(pckPath);
132 | if(!utils.isSnapshot(packageInfo.version)){
133 | packageInfo.version = moduleVersion;
134 | try {
135 | fsExtra.writeJsonSync(pckPath, packageInfo);
136 | } catch (e) {
137 | exit(e);
138 | return;
139 | }
140 | }
141 | }
142 | console.info('开始上传模块');
143 | var registry = Factory.instance(options.type, options);
144 | registry.check([packageName], [], function(avaliable, data){
145 | if(avaliable){
146 | if(data && data.downloads && data.downloads[packageName]){
147 | if(utils.isSnapshot(packageName) || options.alwaysUpdate){
148 | console.info('中央缓存已存在', packageName, ',本次上传将覆盖之前的包!');
149 | }else{
150 | exit('中央缓存已存在' + packageName + ',请更新版本!!!');
151 | return;
152 | }
153 | }
154 | console.debug('请求附加信息:' + JSON.stringify(info));
155 | registry.put(uploadDir, info, function(err){
156 | //将版本重写package.json
157 | options.snapshot && options.overwrite && rewritePkg();
158 |
159 | console.info('删除临时目录');
160 | shellUtils.rm('-rf', uploadDir);
161 |
162 | exit(err);
163 | });
164 | } else {
165 | exit('中央缓存服务不可用,无法上传!');
166 | }
167 | });
168 | });
169 | source.pipe(target);
170 |
171 | //重写version
172 | function rewritePkg() {
173 | console.info('更新package.json里的版本信息');
174 | packageInfo.version = moduleVersion;
175 | fsExtra.writeJsonSync(npmPackagePath, packageInfo);
176 | }
177 | },
178 | /**
179 | * 发布到npm
180 | * @param {object} options 输入参数
181 | * @return {[type]} [description]
182 | */
183 | toNpm: function(options){
184 | var registry = options.registry;
185 | npmUtils.npmPublish(registry, this.exit);
186 | },
187 | /**
188 | * 退出
189 | * @return {[type]} [description]
190 | */
191 | exit: function(err){
192 | if(err){
193 | console.error('发布失败:',err);
194 | process.exit(1);
195 | } else {
196 | console.info('******************发布结束******************');
197 | process.exit(0);
198 | }
199 | }
200 | };
201 |
--------------------------------------------------------------------------------
/src/js/command/qdownload.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: wyw.wang
3 | * @Date: 2017-02-22 10:22
4 | * @Email: wyw.wang@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2017-02-28 10:23
7 | */
8 | var _ = require('lodash'),
9 | asyncMap = require("async").every,
10 | f2bConfigUtils = require('../common/f2bConfigUtils'),
11 | swiftUtils = require('../common/swiftUtils'),
12 | utils = require('../common/utils');
13 |
14 | var __cwd = process.cwd();
15 | /*@Command({
16 | "name": "qdownload",
17 | "alias":"qd",
18 | "des":"Download a static source to repository, you can set the swift setting by parameters or use command 'ncs config set resourceSwift host|user|pass|container'",
19 | options:[
20 | ["-h, --host [host]", "host of swift"],
21 | ["-u, --user [user]", "user of swift"],
22 | ["-p, --pass [pass]", "pass of swift"],
23 | ["-c, --container [container]", "container in swift"],
24 | ["-a, --auto", "according to package.json,use project parameter in f2b to set the value of container, it will ignore container parameter in command"]
25 | ]
26 | })*/
27 | module.exports = {
28 | run: function(options){
29 | try {
30 | //如果设定了auto参数,会忽略指令的container参数以及resourceSwift中container的配置,会将package.json里的f2b下的key值作为container来下载对象
31 | var whitelist = {};
32 | if(options.auto){
33 | whitelist.container = 1;
34 | }
35 |
36 | var params = swiftUtils.getConfig(options, 'resourceSwift', whitelist),
37 | snapshotParams = options['resourceSnapshotSwift'] && swiftUtils.getConfig(options, 'resourceSnapshotSwift', whitelist),
38 | rs = f2bConfigUtils.getConfig(__cwd, options).format(options.auto);
39 | asyncMap(rs, function(el, cb){
40 | var p = _.extend({}, utils.isSnapshot(el.name) && snapshotParams || params, el, {
41 | name: el.name + '.' + el.compressType
42 | });
43 | //如果auto为true,则将package.json中f2b里的key值作为container来下载对象,否则container取参数或者配置文件里的。
44 | if(options.auto){
45 | p.container = el.container;
46 | }else{
47 | params.container && (p.container = params.container);
48 | }
49 | swiftUtils.download(p, cb);
50 | }, this.exit);
51 | } catch (e) {
52 | this.exit(e);
53 | }
54 | },
55 | /**
56 | * 退出
57 | * @return {[type]} [description]
58 | */
59 | exit: function(err){
60 | if(err){
61 | console.error(err.stack || err);
62 | process.exit(1);
63 | } else {
64 | process.exit(0);
65 | }
66 | }
67 | };
68 |
--------------------------------------------------------------------------------
/src/js/command/qupload.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: wyw.wang
3 | * @Date: 2017-02-22 10:22
4 | * @Email: wyw.wang@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2017-02-28 10:23
7 | */
8 |
9 |
10 |
11 | var _ = require('lodash'),
12 | f2bConfigUtils = require('../common/f2bConfigUtils'),
13 | swiftUtils = require('../common/swiftUtils'),
14 | utils = require('../common/utils');
15 |
16 | var __cwd = process.cwd();
17 | /*@Command({
18 | "name": "qupload",
19 | "alias":"qu",
20 | "des":"Upload a static source to repository by package.json, you can set the swift setting by parameters or use command 'ncs config set resourceSwift host|user|pass|container'",
21 | options:[
22 | ["-h, --host [host]", "host of swift"],
23 | ["-u, --user [user]", "user of swift"],
24 | ["-p, --pass [pass]", "pass of swift"],
25 | ["-c, --container [container]", "container in swift"],
26 | ["-f, --forceUpdate", "if exists, module on the server will be overrided"],
27 | ["-a, --auto", "create container automatically according to package.json,use project parameter in f2b, it will ignore container parameter in command"]
28 | ]
29 | })*/
30 | module.exports = {
31 | run: function(options){
32 | try {
33 | //如果设定了auto参数,会忽略指令的container参数以及resourceSwift中container的配置,会根据package.json里f2b的project属性值来动态创建container
34 | var whitelist = {};
35 | if(options.auto){
36 | whitelist.container = 1;
37 | }
38 | var params = swiftUtils.getConfig(options, 'resourceSwift', whitelist),
39 | snapshotParams = options['resourceSnapshotSwift'] && swiftUtils.getConfig(options, 'resourceSnapshotSwift', whitelist),
40 | rs = f2bConfigUtils.getConfig(__cwd, options).format(options.auto),
41 | self = this;
42 | //如果指定container,则对象创建在该container下
43 | if(!options.auto && params.container){
44 | _.each(rs, function(cf){
45 | var p = _.extend({
46 | destpath: cf.destpath
47 | }, cf, utils.isSnapshot(cf.name) && snapshotParams || params);
48 | swiftUtils.ensureContainerExist(p, function(err, res){
49 | if(err){
50 | self.exit(err);
51 | return;
52 | }
53 | swiftUtils.upload(p, self.exit, options.forceUpdate);
54 | });
55 | });
56 | //如果没有指定container,则会根据rs里的container值来创建(该值等同于package.json中的project),对象会存放在该container下
57 | }else{
58 | _.each(rs, function(cf){
59 | var p = _.extend({
60 | destpath: cf.destpath
61 | }, utils.isSnapshot(cf.name) && snapshotParams || params, cf);
62 | swiftUtils.ensureContainerExist(p, function(err, res){
63 | if(err){
64 | self.exit(err);
65 | return;
66 | }
67 | swiftUtils.upload(p, self.exit, options.forceUpdate);
68 | });
69 | });
70 | }
71 | } catch (e) {
72 | if(e){
73 | console.error(e.stack || e);
74 | }
75 | this.exit(e);
76 | }
77 | },
78 | /**
79 | * 退出
80 | * @return {[type]} [description]
81 | */
82 | exit: function(err){
83 | if(err){
84 | process.exit(1);
85 | } else {
86 | process.exit(0);
87 | }
88 | }
89 | };
90 |
--------------------------------------------------------------------------------
/src/js/command/server.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: robin
3 | * @Date: 2016-08-17 17:30:24
4 | * @Email: xin.lin@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2016-09-18 11:06:07
7 | */
8 |
9 | 'use strict'
10 | var _ = require('lodash'),
11 | path = require('path'),
12 | utils = require('../common/utils'),
13 | shellUtils = require('../common/shellUtils'),
14 | fsExtra = require('fs-extra'),
15 | childProcess = require('child_process');
16 |
17 | var constant = require('../common/constant'),
18 | app = path.join(__dirname, '../app'),
19 | pm2OpsMap = constant.PM2OPS;
20 |
21 | /*@Command({
22 | "name": "server [command] [name]",
23 | "alias":"s",
24 | "des":"Start a server to store the npm module cache, command is for pm2, like start、stop、restart,and so on, name mean the name of application for pm2.",
25 | options:[
26 | ["-s, --storage [storage]", "specify the type of storage, could be localfile or swift"],
27 | ["-c, --storageConfig [storageConfig]", "specify the config of storage, serveral arguments joined with '|', the format of swift is 'host|user|pass', localfile is 'cache path'"],
28 | ["-p, --port [port]", "specify the port of the service, default is 8888"],
29 | ["-f, --useFork", "start with fork"],
30 | ["-t, --token [token]", "control the auth to access the server"],
31 | ["-i, --i [i]", "thread count only for pm2"],
32 | ["-n --name [name]", "app name only for pm2"]
33 | ]
34 | })*/
35 | module.exports = {
36 | run: function(command, name, opts) {
37 | var pm2 = shellUtils.which('pm2'),
38 | env = _.extend({ //进程传递参数
39 | port: opts.port || '8888',
40 | token: opts.token || '', //请求校验
41 | storage: opts.storage || 'localfile',
42 | storageConfig: opts.storageConfig,
43 | storageSnapshotConfig: opts.storageSnapshotConfig || opts.storageConfig,
44 | swiftTokenTimeout: opts.swiftTokenTimeout,
45 | zookeeper: opts.zookeeper,
46 | zkRoot: opts.zkRoot,
47 | ceph: opts.ceph,
48 | DEBUG: !!global.DEBUG
49 | }, process.env);
50 | // 没有pm2或者指定了useFork就使用fork子进程方式
51 | if (!pm2 || opts.useFork) {
52 | console.info('Connot find pm2, will start server with fork.');
53 | childProcess.fork(app, {
54 | env: env
55 | });
56 | } else {
57 | // 使用pm2管理server
58 | console.info(
59 | 'Will start server with PM2. \n' +
60 | '- If you want to just start with fork ,use "--useFork" or "-f" option. \n' +
61 | '- You can append actions like "start/stop/list" and options like "--instances 4,etc." after.\n'
62 | );
63 | // 指令中的_,数字以及port参数之外的参数全部传给pm2
64 | var options =[];
65 | _.forIn(opts, function(value ,key){
66 | if(key[0] != '_' && typeof value != 'object' && typeof value != 'function' && typeof value != 'undefined' && pm2OpsMap[key]){
67 | options.push((key.length == 1 ? '-' : '--') + key + ' ' + value);
68 | }
69 | });
70 | // 指令的第二个参数标示pm2的动作,默认start
71 | var cmd = [pm2, command || 'start', name || app].concat(options).join(' ');
72 | console.info('exec:',cmd);
73 | shellUtils.exec(cmd, {
74 | env: env
75 | });
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/js/command/swift.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: robin
3 | * @Date: 2017-03-07 17:30:24
4 | * @Email: xin.lin@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2017-03-07 17:30:24
7 | */
8 |
9 | 'use strict'
10 |
11 | var _ = require('lodash'),
12 | swiftUtils = require('../common/swiftUtils'),
13 | utils = require('../common/utils');
14 |
15 | /*@Command({
16 | "name": "swift [action] [name]",
17 | "alias":"w",
18 | "des":"use swift quickly",
19 | options:[
20 | ["-h, --host [host]", "host of swift"],
21 | ["-u, --user [user]", "user of swift"],
22 | ["-p, --pass [pass]", "pass of swift"],
23 | ["-c, --container [container]", "container in swift"]
24 | ]
25 | })*/
26 | module.exports = {
27 | run: function(action, name, options) {
28 | try {
29 | var params = swiftUtils.getConfig(options, utils.isSnapshot(name) ? 'resourceSnapshotSwift' : 'resourceSwift'),
30 | command = { query:1, delete:1 , deleteContainer:1};
31 | params.name = name;
32 |
33 | if(command[action]){
34 | this[action](params, this.exit);
35 | }else{
36 | throw new Error('非法swift指令操作:' + action);
37 | }
38 | } catch (e) {
39 | this.exit(e);
40 | }
41 | },
42 | /**
43 | * 查询对象是否存在
44 | * @param {Object} params [description]
45 | * @param {Function} callback [description]
46 | * @return {[type]} [description]
47 | */
48 | query: function(params, callback) {
49 | swiftUtils.objectExist(params, function(err, res){
50 | if(!err){
51 | console.info('该对象存在!!');
52 | }
53 | callback(err, res);
54 | });
55 | },
56 | /**
57 | * 删除对象
58 | * @param {Object} params [description]
59 | * @param {Function} callback [description]
60 | */
61 | delete: function(params, callback){
62 | swiftUtils.deleteObject(params, function(err, res){
63 | if(!err){
64 | console.info('删除对象完成!!');
65 | }
66 | callback(err, res);
67 | });
68 | },
69 | /**
70 | * 删除对象
71 | * @param {Object} params [description]
72 | * @param {Function} callback [description]
73 | */
74 | deleteContainer: function(params, callback){
75 | swiftUtils.deleteContainer(params, function(err, res){
76 | if(!err){
77 | console.info('删除容器完成!!');
78 | }
79 | callback(err, res);
80 | });
81 | },
82 | /**
83 | * 退出
84 | * @return {[type]} [description]
85 | */
86 | exit: function(err){
87 | if(err){
88 | console.error(err.stack || err);
89 | process.exit(1);
90 | } else {
91 | process.exit(0);
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/js/command/upload.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: wyw.wang
3 | * @Date: 2017-02-08 16:17
4 | * @Email: wyw.wang@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2017-02-28 16:19
7 | */
8 | var _ = require('lodash'),
9 | Path = require('path'),
10 | swiftUtils = require('../common/swiftUtils'),
11 | utils = require('../common/utils');
12 |
13 | var __cwd = process.cwd();
14 | /*@Command({
15 | "name": "upload [path] [name]",
16 | "alias":"u",
17 | "des":"Upload a static source to repository, you can set the swift setting by parameters or use command 'ncs config set resourceSwift host|user|pass|container'",
18 | options:[
19 | ["-h, --host [host]", "host of swift"],
20 | ["-u, --user [user]", "user of swift"],
21 | ["-p, --pass [pass]", "pass of swift"],
22 | ["-c, --container [container]", "container in swift"],
23 | ["-f, --forceUpdate", "if exists, module on the server will be overrided"],
24 | ["-z, --compressType [compressType]", "compress type, it is tar or zip, default is tar", "tar"]
25 | ]
26 | })*/
27 | module.exports = {
28 | run: function(path, name, options){
29 | try{
30 | if(!name){
31 | console.error('请指定文件别名!!!指令为`ncs upload [path] [name]`');
32 | process.exit(1);
33 | }
34 | var params = _.extend({}, swiftUtils.getConfig(options, utils.isSnapshot(name) ? 'resourceSnapshotSwift': 'resourceSwift'), {
35 | name: name,
36 | path: swiftUtils.check(path),
37 | compressType: options.compressType,
38 | destpath: Path.relative(process.cwd(), path)
39 | });
40 | swiftUtils.upload(params, this.exit, options.forceUpdate);
41 | }catch(err){
42 | this.exit(err);
43 | }
44 | },
45 | /**
46 | * 退出
47 | * @return {[type]} [description]
48 | */
49 | exit: function(err){
50 | if(err){
51 | console.error(err.statck || err);
52 | process.exit(1);
53 | } else {
54 | process.exit(0);
55 | }
56 | }
57 | };
58 |
--------------------------------------------------------------------------------
/src/js/common/checkUtils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: wyw.wang
3 | * @Date: 2016-11-15 14:42
4 | * @Email: wyw.wang@qunar.com
5 | * @Last modified by: wyw
6 | * @Last modified time: 2016-11-15 14:43
7 | */
8 |
9 |
10 | var fs = require('fs'),
11 | _ = require('lodash'),
12 | path = require('path'),
13 | fsExtra = require('fs-extra'),
14 | utils = require('./utils');
15 |
16 | module.exports = {
17 | npmShrinkwrapCheck: function(pkgJson, shrinkwrapJson){
18 | if(pkgJson){
19 | var miss = diff(pkgJson.dependencies, shrinkwrapJson.dependencies);
20 | if(miss.length > 0){
21 | console.error('校验npm-shrinkwrap.json和package.json的依赖一致性失败');
22 | throw new Error('npm-shrinkwrap.json中缺少package.json中的以下依赖:' + miss.join(','));
23 | } else {
24 | console.info('校验npm-shrinkwrap.json和package.json的依赖一致性成功');
25 | }
26 | } else {
27 | console.info('未找到package.json,跳过依赖一致性校验。');
28 | }
29 | },
30 | yarnLockCheck: function(pkgJson, lockJson){
31 | if(pkgJson){
32 | var miss = diff(pkgJson.dependencies, lockJson.dependencies);
33 | if(miss.length > 0){
34 | console.error('校验yarn.lock和package.json的依赖一致性失败');
35 | throw new Error('yarn.lock中缺少package.json中的以下依赖:' + miss.join(','));
36 | } else {
37 | console.info('校验yarn.lock和package.json的依赖一致性成功');
38 | }
39 | } else {
40 | console.info('未找到package.json,跳过依赖一致性校验。');
41 | }
42 | },
43 | snapshotDepsCheck: function(dependencies){
44 | var deps = [];
45 | utils.traverseDependencies(dependencies, function(v, k){
46 | if(utils.isSnapshot(v.version || v)){
47 | deps.push(k + ': ' + (v.version || v));
48 | }
49 | });
50 | if(deps.length > 0){
51 | throw new Error('依赖中含有SNAPSHOT模块:' + deps.join(','));
52 | }
53 | }
54 | };
55 |
56 | function diff(source, dist){
57 | if(!source){
58 | return [];
59 | }
60 | var missing = [];
61 | _.each(source, function(v, k){
62 | if(!dist[k]){
63 | missing.push(k);
64 | }
65 | });
66 | return missing;
67 | }
68 |
--------------------------------------------------------------------------------
/src/js/common/compressUtils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: robin
3 | * @Date: 2017-03-27 14:18:18
4 | * @Email: xin.lin@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2017-03-27 15:21:30
7 | */
8 | var fs = require('fs'),
9 | path = require('path'),
10 | archiver = require('archiver'),
11 | tar = require('tar'),
12 | unzipper = require('unzipper');
13 |
14 | var COMPRESS_TYPE = require('./constant').COMPRESS_TYPE;
15 |
16 | var utils = module.exports = {
17 | /**
18 | * 压缩流
19 | * @param {String} dir 需要压缩的文件夹路径
20 | * @param {String} destpath 压缩文件里的路径
21 | * @param {String} type 压缩类型
22 | * @param {Function} callback 回调
23 | * @return {stream}
24 | */
25 | compressStream: function(dir, destpath, type, callback){
26 | return tarZipCompressStream(dir, destpath, type, callback);
27 | },
28 | /**
29 | * 解压流
30 | * @param {String} dir 解压到的目录
31 | * @param {String} type 解压模式 zip 或者 tar
32 | * @param {Function} callback 回调
33 | * @return {[type]} [description]
34 | */
35 | extractStream: function(dir, type, callback){
36 | if(type !== COMPRESS_TYPE.TAR && type !== COMPRESS_TYPE.ZIP ){
37 | throw new Error('compress type must be tar or zip!!!');
38 | }
39 | return type == COMPRESS_TYPE.TAR ? tarExtractStream(dir, callback) : zipExtractStream(dir, callback);
40 | }
41 | };
42 |
43 | /**
44 | * 压缩流
45 | * @param {String} dir 需要压缩的文件夹路径
46 | * @param {String} destpath 压缩文件里的路径
47 | * @param {String} type 压缩类型
48 | * @param {Function} callback 回调
49 | * @return {stream}
50 | */
51 | function tarZipCompressStream(dir, destpath, type, callback) {
52 | //错误处理
53 | function onError(err) {
54 | throw err;
55 | }
56 |
57 | //压缩结束
58 | function onEnd() {
59 | callback && callback();
60 | }
61 |
62 | var compressStream = archiver(type)
63 | .on('error', onError)
64 | .on('end', onEnd),
65 | stat = fs.statSync(dir);
66 |
67 | //文件命名不要以.properties结尾,会导致上级目录也存在
68 | if(stat.isDirectory()){
69 | compressStream.directory(dir, destpath);
70 | }else{
71 | compressStream.file(destpath || dir);
72 | }
73 | return compressStream.finalize();
74 |
75 | }
76 |
77 | /**
78 | * 解压流
79 | * @param {String} dir 解压到的目录
80 | * @param {Function} callback 回调
81 | * @return {void}
82 | */
83 | function tarExtractStream(dir, callback) {
84 | //错误处理
85 | function onError(err) {
86 | throw err;
87 | }
88 | //处理结束
89 | function onEnd() {
90 | callback && callback();
91 | }
92 |
93 | var extractor = tar.Extract({
94 | path: dir
95 | })
96 | .on('error', onError)
97 | .on('end', onEnd);
98 |
99 | return extractor;
100 | }
101 | function zipExtractStream(dir, callback) {
102 | //错误处理
103 | function onError(err) {
104 | throw err;
105 | }
106 | //处理结束
107 | function onClose() {
108 | callback && callback();
109 | }
110 | var extractor = unzipper.Extract({
111 | path: dir
112 | })
113 | .on('error', onError)
114 | .on('close', onClose);
115 | return extractor;
116 | }
117 |
--------------------------------------------------------------------------------
/src/js/common/console.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: robin
3 | * @Date: 2016-08-08 19:12:42
4 | * @Email: xin.lin@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2016-08-11 10:48:26
7 | */
8 |
9 | var ansi = require('ansi-styles');
10 |
11 | var DOUBLEQUOTE = /^\s*"|"\s*$/g;
12 | var joinArgs = function(args) {
13 | var arr = Array.prototype.slice.call(args), tmp;
14 | arr.forEach(function(v, i){
15 | try{
16 | arr[i] = JSON.stringify(v).replace(DOUBLEQUOTE, "");
17 | }catch(e){}
18 | });
19 | return arr.join(' ');
20 | };
21 |
22 | console.info = function() {
23 | return console.log(ansi.green.open, '>[npm-cache-share]', new Date().toLocaleString(), joinArgs(arguments), ansi.green.close);
24 | };
25 |
26 | console.warn = function(){
27 | return console.log(ansi.yellow.open, '>[npm-cache-share]', new Date().toLocaleString(), joinArgs(arguments), ansi.yellow.close)
28 | };
29 |
30 | console.error = function() {
31 | return console.log(ansi.red.open, '>[npm-cache-share]', new Date().toLocaleString(), joinArgs(arguments), ansi.red.close);
32 | };
33 |
34 | console.debug = function() {
35 | if(global.DEBUG){
36 | return console.log(ansi.green.open, '>[npm-cache-share]', new Date().toLocaleString(), joinArgs(arguments), ansi.green.close);
37 | } else {
38 | return null;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/js/common/constant.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: robin
3 | * @Date: 2016-09-18 10:58:34
4 | * @Email: xin.lin@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2016-09-18 11:06:16
7 | */
8 |
9 | module.exports = {
10 | LIBNAME: 'node_modules',
11 | UPLOADDIR: 'upload_dir',
12 | MODULECHECKER: 'modules_check_dir',
13 | SPLIT: '@@@',
14 | NPMOPS: {
15 | noOptional: 'no-optional',
16 | saveDev: 'save-dev',
17 | save: 'save',
18 | production: 'production',
19 | registry: 'registry',
20 | disturl: 'disturl'
21 | },
22 | NPMOPSWITHOUTSAVE: {
23 | noOptional: 'no-optional',
24 | production: 'production',
25 | registry: 'registry',
26 | disturl: 'disturl'
27 | },
28 | PM2OPS: {
29 | i: 1,
30 | name: 1
31 | },
32 | NPM_MAX_BUNDLE: 50,
33 | LOAD_MAX_RESOUCE: 100,
34 | CONFIGKEY: [
35 | 'registry',
36 | 'token',
37 | 'type',
38 | 'repository',
39 | 'disturl',
40 | 'port',
41 | 'i',
42 | 'name',
43 | 'storage',
44 | 'storageConfig',
45 | 'storageSnapshotConfig',
46 | 'installTimeout',
47 | 'npmPlatBinds',
48 | 'swift',
49 | 'resourceSwift',
50 | 'resourceSnapshotSwift',
51 | 'swiftTokenTimeout',
52 | 'compressType',
53 | 'nameReg',
54 | 'zookeeper',
55 | 'zkRoot',
56 | 'ceph'
57 | ],
58 | F2B: {
59 | CONFIG_FILE: 'package.json',
60 | SPLIT: '/',
61 | CONFIG_KEY: 'f2b'
62 | },
63 | COMPRESS_TYPE: {
64 | TAR: 'tar',
65 | ZIP: 'zip'
66 | },
67 | VERSION_TYPE:{
68 | SNAPSHOT: 'SNAPSHOT',
69 | RELEASE: 'RELEASE'
70 | },
71 | CACHESTRATEGY: {
72 | //强制更新,始终从中央缓存获取的,含有SNAPSHOT标示的默认为该策略
73 | ALWAYSUPDATE: 'alwaysUpdate',
74 | //强制安装,忽略本地缓存和中央缓存
75 | IGNORECACHE: 'ignoreCache',
76 | //模块包安装后运行指定脚本
77 | POSTINSTALL: 'postInstall',
78 | //模块黑名单
79 | BLACKLIST:'blackList'
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/js/common/f2bConfigUtils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: wyw.wang
3 | * @Date: 2017-02-21 18:56
4 | * @Email: wyw.wang@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2017-02-27 18:56
7 | */
8 |
9 | var _ = require('lodash'),
10 | path = require('path'),
11 | fsExtra = require('fs-extra'),
12 | Constant = require('./constant'),
13 | F2B = Constant['F2B'];
14 | function Config(cwd, config, options, root){
15 | console.debug('f2b:', config);
16 | var configs = this.configs = [];
17 |
18 | this.cwd = cwd;
19 |
20 | if ((typeof config.path) === 'undefined'){
21 | for(var k in config){
22 | var el = config[k];
23 | configs.push({
24 | project: k,
25 | version: el.version,
26 | path: el.path,
27 | type: el.type,
28 | ceph: options.ceph
29 | });
30 | }
31 | } else {
32 | configs.push({
33 | project: config.project || root.name,
34 | version: config.version,
35 | path: config.path,
36 | type: config.type,
37 | ceph: options.ceph
38 | });
39 | }
40 | };
41 |
42 | Config.prototype.format = function(withoutProject){
43 | var cwd = this.cwd;
44 | return _.flatMap(this.configs, function(el){
45 | return {
46 | container: el.project,
47 | name: withoutProject ? el.version : el.project + F2B.SPLIT + el.version,
48 | path: path.join(cwd, el.path),
49 | compressType: el.type || Constant.COMPRESS_TYPE.TAR,
50 | destpath: el.path,
51 | ceph: el.ceph
52 | };
53 | });
54 | };
55 |
56 | var utils = module.exports = {
57 | getConfig: function(cwd, options){
58 | var content = fsExtra.readJsonSync(path.join(cwd, F2B.CONFIG_FILE)),
59 | config = content[F2B.CONFIG_KEY];
60 | if(typeof config === 'undefined'){
61 | throw new Error('Can`t find ' + F2B.CONFIG_KEY + ' in ' + F2B.CONFIG_FILE);
62 | } else {
63 | return new Config(cwd, config, options, content);
64 | }
65 | },
66 | checkName: function(name, nameReg){
67 | if(!nameReg){
68 | return;
69 | }
70 | if(!RegExp(nameReg).test(name)){
71 | throw new Error('命名不符合规则' + nameReg + '!!!');
72 | }
73 | }
74 | };
75 |
--------------------------------------------------------------------------------
/src/js/common/npmUtils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: wyw.wang
3 | * @Date: 2016-10-08 14:39
4 | * @Email: wyw.wang@qunar.com
5 | * @Last modified by: wyw
6 | * @Last modified time: 2016-10-08 14:39
7 | */
8 |
9 | 'use strict'
10 | var _ = require('lodash'),
11 | semver = require('semver'),
12 | fsExtra = require('fs-extra'),
13 | rpt = require('read-package-tree');
14 |
15 | var utils = require('./utils'),
16 | constant = require('./constant'),
17 | shellUtils = require('./shellUtils');
18 |
19 | var config = fsExtra.readJsonSync(utils.getConfigPath()),
20 | instInModulePath = {};
21 |
22 | module.exports = {
23 | npmPath: 'npm',
24 | // 判断是否可以使用yarn
25 | checkYarn: (function(){
26 | var yarnCmd = shellUtils.which('yarn'),
27 | nodeVer = process.versions.node.split('-')[0];
28 | return yarnCmd && semver.satisfies(nodeVer, '>=4.0.0');
29 | })(),
30 | /**
31 | * 配置npm路径
32 | * @param {[String]} npmPath [npm path]
33 | * @return {[void]}
34 | */
35 | config: function(npmPath){
36 | this.npmPath = npmPath || 'npm';
37 | },
38 | getLastestVersion: function(moduleName, cbk) {
39 | shellUtils.exec(this.npmPath + ' view ' + moduleName + ' versions --json', { silent: true }, function(code, stdout, stderr){
40 | if(code !== 0){
41 | cbk(stderr);
42 | }else{
43 | try {
44 | var versions = eval(_.trim(stdout));
45 | } catch (e) {
46 | cbk(e);
47 | }
48 | if(Array.isArray(versions)){
49 | cbk(null, versions[versions.length-1]);
50 | }else{
51 | cbk('versions is not an array!');
52 | }
53 | }
54 | });
55 | },
56 | npmDedupe: function(npmopts) {
57 | console.info('模块重组');
58 | //将层次结构重新组织
59 | var optstr = utils.toString(npmopts, constant.NPMOPS);
60 | shellUtils.exec(this.npmPath + ' dedupe ' + optstr, {
61 | async: false
62 | });
63 | },
64 | npmPrune: function(npmopts) {
65 | console.info('清除多余模块');
66 | //删除多余的模块
67 | var optstr = utils.toString(npmopts, constant.NPMOPS);
68 | shellUtils.exec(this.npmPath + ' prune ' + optstr, {
69 | async: false
70 | });
71 | },
72 | npmShrinkwrap: function(npmopts, cbk){
73 | var optstr = utils.toString(npmopts, constant.NPMOPS);
74 | shellUtils.exec(this.npmPath + ' shrinkwrap ' + optstr, function(code, stdout, stderr){
75 | if (code!== 0) {
76 | cbk('自动更新执行npm shrinkwrap失败:'+stderr+'如不需要版本锁定可忽略该错误。');
77 | } else {
78 | cbk(null);
79 | }
80 | });
81 | },
82 | npmInstall: function(npmopts, opts, cbk){
83 | //避免在模块路径下重复安装,和工程安装一样
84 | if(instInModulePath[opts.cwd]){
85 | cbk(null);
86 | return;
87 | }
88 | instInModulePath[opts.cwd] = 1;
89 | var optstr = utils.toString(npmopts, constant.NPMOPS),
90 | cmd = (this.checkYarn ? 'yarn' : this.npmPath) + ' install ' + optstr;
91 | console.debug(cmd, opts);
92 | shellUtils.exec(cmd, opts, function(code, stdout, stderr){
93 | if (code!== 0) {
94 | cbk(stderr);
95 | } else {
96 | cbk(null);
97 | }
98 | });
99 | },
100 | /**
101 | * 发布一个包到npm
102 | * @param {url} registry
103 | * @param {function} cbk 回调
104 | * @return {void} [description]
105 | */
106 | npmPublish: function(registry, cbk){
107 | var optstr = registry ? (" --registry " + registry):"",
108 | cmd = "npm publish" + optstr;
109 | console.debug(cmd);
110 | shellUtils.exec(cmd, function(code, stdout, stderr){
111 | if(code!==0){
112 | cbk(stderr);
113 | }else{
114 | cbk(null);
115 | }
116 | });
117 | },
118 | /**
119 | * 由于yarn add会修改package.json,所以如果不修改的话需要指定成npm来安装
120 | */
121 | npmInstallModules: function(moduleNames, npmopts, opts, notSave){
122 | if(moduleNames.length === 0){
123 | return;
124 | }
125 | var optstr = utils.toString(npmopts, constant.NPMOPSWITHOUTSAVE),
126 | cmd;
127 | if(notSave){
128 | cmd = this.npmPath + ' install ' + moduleNames.join(' ') + ' ' + optstr;
129 | }else{
130 | var leading = this.checkYarn ? 'yarn add ' : this.npmPath + ' install ';
131 | cmd = leading + moduleNames.join(' ') + ' ' + optstr + (this.checkYarn && ' --non-interactive ' || '');
132 | }
133 | console.debug(cmd);
134 | var result = shellUtils.exec(cmd, opts);
135 | if(result.code !== 0) {
136 | throw result.stderr;
137 | }
138 | },
139 | /**
140 | * 过滤掉不恰当的依赖(平台不需要的optionalDependecise)
141 | * @param {String} action 模块pacakage.json中scripts属性所支持的
142 | * @param {Object} opts shelljs.exec支持的参数
143 | * @param {Function} cbk 回调函数
144 | */
145 | npmRunScript: function(action, opts, cbk){
146 | var cmd = (this.checkYarn ? 'yarn' : this.npmPath) + ' run ' + action;
147 | console.debug(cmd + ' ' + JSON.stringify(opts));
148 | //由于shelljs.exec中只要cbk存在则默认async会被设置成true,故需要区分
149 | if(opts.async){
150 | shellUtils.exec(cmd, opts, function(code, stdout, stderr){
151 | if (code!== 0) {
152 | cbk(stderr);
153 | } else {
154 | cbk(null);
155 | }
156 | });
157 | }else{
158 | var rs = shellUtils.exec(cmd, opts);
159 | if (rs.code!== 0) {
160 | cbk(rs.stderr);
161 | } else {
162 | cbk(null);
163 | }
164 | }
165 | },
166 | /**
167 | * 过滤掉不恰当的依赖(平台不需要的optionalDependecise)
168 | * @param {JSON} dependencies [description]
169 | * @return {JSON} [description]
170 | */
171 | filter: function(dependencies){
172 | var platform = process.platform,
173 | binds = config.npmPlatBinds || {},
174 | filterArr = [];
175 | _.forEach(binds, function(v, k){
176 | if(k !== platform){
177 | filterArr = filterArr.concat(v);
178 | }
179 | });
180 | console.debug('将被过滤的依赖:',filterArr);
181 | if(filterArr.length > -1){
182 | var news = _.cloneDeep(dependencies);
183 | filterEach(news, filterArr);
184 | return news;
185 | } else {
186 | return dependencies;
187 | }
188 | }
189 | };
190 |
191 | /**
192 | * 递归清除所有需过滤项
193 | * @param {[type]} dependencies [description]
194 | * @param {[type]} filter [description]
195 | * @return {[type]} [description]
196 | */
197 | function filterEach(dependencies, filter){
198 | _.forEach(dependencies, function(v, k){
199 | if(filter.indexOf(k) > -1){
200 | console.debug('舍弃依赖:', k);
201 | delete dependencies[k];
202 | return;
203 | }
204 | if(v.dependencies){
205 | filterEach(v.dependencies, filter);
206 | }
207 | })
208 | }
209 |
210 | // 原来用以处理npm对于安装optionDependecies 出现平台不匹配的而误报错误中断安装的处理,现在使用白名单方式过滤
211 | // function installTry(moduleNames, optstr, opts, skipDependencies){
212 | // if(moduleNames.length === 0){
213 | // return;
214 | // }
215 | // var cmd = 'npm install ' + moduleNames.join(' ') + ' ' + optstr,
216 | // result = exec(cmd, opts);
217 | // console.debug(cmd);
218 | // if(result.code !== 0) {
219 | // var err = result.stderr,
220 | // errTarget = check(err),
221 | // index = moduleNames.indexOf(errTarget);
222 | // if(errTarget && index > -1) {
223 | // console.info(errTarget, 'is not suitable for current platform, skip it.');
224 | // moduleNames.splice(index, 1);
225 | // skipDependencies.push(errTarget);
226 | // installTry(moduleNames, optstr, opts, skipDependencies);
227 | // } else {
228 | // console.error(err);
229 | // }
230 | // }
231 | // }
232 | //
233 | // function check(err){
234 | // var codeMatch = err.match(/npm ERR\! code (\w*)/);
235 | // if(codeMatch && codeMatch[1] === 'EBADPLATFORM'){
236 | // return err.match(/npm ERR\! notsup Not compatible with your operating system or architecture\: ([\w@\.]*)/)[1];
237 | // } else {
238 | // return false;
239 | // }
240 | // }
241 |
--------------------------------------------------------------------------------
/src/js/common/shellUtils.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = require('shelljs/shell');
3 |
--------------------------------------------------------------------------------
/src/js/common/zkClient.js:
--------------------------------------------------------------------------------
1 | /**
2 | * // Event types
3 | * var TYPES = {
4 | * NODE_CREATED : 1,
5 | * NODE_DELETED : 2,
6 | * NODE_DATA_CHANGED : 3,
7 | * NODE_CHILDREN_CHANGED : 4
8 | * };
9 | */
10 | var _ = require('lodash'),
11 | zookeeper = require('node-zookeeper-client'),
12 | CreateMode = zookeeper.CreateMode,
13 | Event = zookeeper.Event;
14 |
15 | var zk,
16 | eventCache = {},
17 | bindsEvent = {},
18 | ZKROOT = '/npm_cache_share';
19 |
20 | module.exports = {
21 | Event: Event,
22 | /**
23 | * 创建zookeeper clinet
24 | * @param {String} zkConfig zookeeper的链接,如host:port
25 | * @param {String} zkRoot zookeeper上根路径节点名称
26 | * @return {void}
27 | */
28 | init: function(zkConfig, zkRoot) {
29 | zk = zookeeper.createClient(zkConfig, {
30 | sessionTimeout : 30000,
31 | spinDelay : 1000,
32 | retries : 3
33 | });
34 | if(zkRoot && zkRoot != 'undefined'){
35 | ZKROOT = '/' + zkRoot;
36 | }
37 | },
38 | /**
39 | * 连接zookeeper
40 | * @return {type} [description]
41 | */
42 | connect: function(){
43 | return new Promise(function(resolve, reject){
44 | zk.once('connected', function () {
45 | console.info('Connected to the zookeeper.');
46 | resolve();
47 | });
48 | zk.connect();
49 | });
50 | },
51 | /**
52 | * 获取节点数据
53 | * @param {String} path 路径
54 | * @return {Promise}
55 | */
56 | getData: function(path) {
57 | var self = this;
58 | path = generatePath(path);
59 | return new Promise(function(resolve, reject){
60 | //由于要监听节点数据变化需要通过该函数第二个参数来指定,每次处理就需要重新监听
61 | if(bindsEvent[Event.NODE_DATA_CHANGED + path]){
62 | zk.getData(path, function(error, data, stat){
63 | //初次获取节点数据
64 | if(error){
65 | console.error(error);
66 | reject(error);
67 | return;
68 | }
69 | //data为Buffer类型,转换为String
70 | resolve((data || "").toString('utf8'));
71 | });
72 | return;
73 | }
74 | bindsEvent[Event.NODE_DATA_CHANGED + path] = 1;
75 | zk.getData(path, function (event) {
76 | watchEvent(event);
77 | }, function(error, data, stat){
78 | //初次获取节点数据
79 | if(error){
80 | console.error(error);
81 | reject(error);
82 | return;
83 | }
84 | //data为Buffer类型,转换为String
85 | resolve((data || "").toString('utf8'));
86 | });
87 | });
88 |
89 | /**
90 | * 事件处理,由于事件被触发后,想要再次监听,需要重新绑定事件
91 | * @param {Event} event
92 | * @return {void}
93 | */
94 | function watchEvent(event) {
95 | //删除对象节点时会触发两次事件
96 | if(event.name == 'NODE_DELETED'){
97 | return;
98 | }
99 | //节点数据变更处理,由于触发监听后event中获取不到数据,故需要通过getData来获取数据并绑定监听
100 | zk.getData(path, function(event){
101 | //递归监听
102 | watchEvent(event);
103 | }, function(error, data, stats){
104 | if(error){
105 | console.error(error);
106 | return;
107 | }
108 | if(event.name == 'NODE_DATA_CHANGED'){
109 | //触发事件监听
110 | self.trigger(Event[event.name], path, (data || "").toString('utf8'));
111 | }
112 | });
113 | }
114 | },
115 | /**
116 | * 设置节点数据
117 | * @param {String} path 路径
118 | * @param {String} data 数据
119 | * @return {Promise}
120 | */
121 | setData: function(path, data){
122 | path = generatePath(path);
123 | return new Promise(function (resolve, reject) {
124 | zk.setData(path, Buffer.from(String(data)), function(error, stat){
125 | if(error){
126 | console.error(error);
127 | reject(error);
128 | return;
129 | }
130 | resolve();
131 | });
132 | });
133 | },
134 | /**
135 | * 判断节点路径是否存在
136 | * @param {String} path 节点路径
137 | * @return {Promise}
138 | */
139 | exist: function(path){
140 | path = generatePath(path);
141 | return new Promise(function (resolve, reject) {
142 | zk.exists(path, function(err, stat){
143 | //错误
144 | if (err) {
145 | console.error(err);
146 | reject(err);
147 | return;
148 | }
149 | //存在
150 | if (stat) {
151 | resolve(true);
152 | return;
153 | }
154 | //不存在
155 | resolve(false);
156 | });
157 | });
158 | },
159 | /**
160 | * 获取子节点
161 | * @param {String} path 节点路径
162 | * @return {Promise}
163 | */
164 | getChildren: function(path){
165 | var self = this;
166 | path = generatePath(path);
167 | return new Promise(function (resolve, reject) {
168 | //由于要监听子节点变化需要通过该函数第二个参数来指定,每次处理就需要重新监听
169 | if(bindsEvent[Event.NODE_CHILDREN_CHANGED + path]){
170 | zk.getChildren(path, function(error, children, stats){
171 | if(error){
172 | console.error(error);
173 | reject(error);
174 | return;
175 | }
176 | resolve(children || []);
177 | });
178 | return;
179 | }
180 | bindsEvent[Event.NODE_CHILDREN_CHANGED + path] = 1;
181 | zk.getChildren(path, function(event){
182 | watchEvent(event);
183 | }, function(error, children, stats){
184 | if(error){
185 | console.error(error);
186 | reject(error);
187 | return;
188 | }
189 | resolve(children || []);
190 | });
191 | });
192 |
193 | /**
194 | * 事件处理,由于事件被触发后,想要再次监听,需要重新绑定事件
195 | * @param {Event} event
196 | * @return {void}
197 | */
198 | function watchEvent(event) {
199 | //删除容器节点时会触发两次事件
200 | if(event.name == 'NODE_DELETED'){
201 | return;
202 | }
203 | //节点变更处理,由于触发监听后event中获取不到数据,故需要通过getChildren来获取数据并绑定监听
204 | zk.getChildren(path, function(event){
205 | //递归监听
206 | watchEvent(event);
207 | }, function(error, children, stats){
208 | if(error){
209 | console.error(error);
210 | return;
211 | }
212 | if(event.name == 'NODE_CHILDREN_CHANGED'){
213 | self.trigger(Event[event.name], path, children);
214 | }
215 | });
216 | }
217 | },
218 | /**
219 | * 创建路径,允许是不存在的节点,永久类型节点
220 | * @param {String} path 路径
221 | * @return {Promise}
222 | */
223 | mkdirp: function(path) {
224 | path = generatePath(path);
225 | return new Promise(function (resolve, reject) {
226 | zk.mkdirp(path, CreateMode.PERSISTENT, function (error, path) {
227 | if (error) {
228 | console.error(error);
229 | reject(error);
230 | return;
231 | }
232 | resolve(path);
233 | });
234 | });
235 | },
236 | /**
237 | * 删除节点
238 | * @param {String} path 节点路径
239 | * @return {Promise}
240 | */
241 | remove: function(path){
242 | path = generatePath(path);
243 | return new Promise(function (resolve, reject) {
244 | zk.remove(path, function (error) {
245 | if (error) {
246 | console.error(error);
247 | reject(error);
248 | return;
249 | }
250 | resolve();
251 | });
252 | });
253 | },
254 | /**
255 | * 触发事件
256 | * @param {String} type 事件类型
257 | * @param {String} path 节点路径
258 | * @param {Object} data 数据
259 | * @return {void}
260 | */
261 | trigger: function(type, path, data){
262 | if(!eventCache[type] || !eventCache[type][path]){
263 | return;
264 | }
265 | eventCache[type][path](data, type, path);
266 | },
267 | /**
268 | * 监听事件
269 | * @param {String} type 事件类型
270 | * @param {String} path 节点路径
271 | * @param {Function} callback 事件回调
272 | * @return {void}
273 | */
274 | register: function(type, path, callback) {
275 | path = generatePath(path);
276 | console.debug('注册' + path + '节点的' + type + '类型监听');
277 | if(!eventCache[type]){
278 | eventCache[type] = {};
279 | }
280 | eventCache[type][path] = callback;
281 | },
282 | /**
283 | * 注销监听事件
284 | * @param {String} type 事件类型
285 | * @param {String} path 节点路径
286 | * @return {void}
287 | */
288 | unregister: function(type, path) {
289 | if(!eventCache[type]){
290 | return;
291 | }
292 | path = generatePath(path);
293 | console.debug('注销' + path + '节点的' + type + '类型监听');
294 | eventCache[type][path] = null;
295 | bindsEvent[type + path] = 0;
296 | delete bindsEvent[type + path];
297 | }
298 | }
299 | /**
300 | * 生成节点路径
301 | * @param {Boolean} isSnapshot 是否是SNAPSHOT版本
302 | * @param {String} repository 容器名称
303 | * @param {String} moduleName 模块名称
304 | * @return {String}
305 | */
306 | function generatePath(path){
307 | return [ZKROOT, path].join('/');
308 | }
309 |
--------------------------------------------------------------------------------
/src/js/lib/swiftClient/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/swift');
2 |
--------------------------------------------------------------------------------
/src/js/lib/swiftClient/lib/multipart.js:
--------------------------------------------------------------------------------
1 | var Buffer = require('buffer').Buffer,
2 | s = 0,
3 | S =
4 | { PARSER_UNINITIALIZED: s++,
5 | START: s++,
6 | START_BOUNDARY: s++,
7 | HEADER_FIELD_START: s++,
8 | HEADER_FIELD: s++,
9 | HEADER_VALUE_START: s++,
10 | HEADER_VALUE: s++,
11 | HEADER_VALUE_ALMOST_DONE: s++,
12 | HEADERS_ALMOST_DONE: s++,
13 | PART_DATA_START: s++,
14 | PART_DATA: s++,
15 | PART_END: s++,
16 | END: s++,
17 | },
18 |
19 | f = 1,
20 | F =
21 | { PART_BOUNDARY: f,
22 | LAST_BOUNDARY: f *= 2,
23 | },
24 |
25 | LF = 10,
26 | CR = 13,
27 | SPACE = 32,
28 | HYPHEN = 45,
29 | COLON = 58,
30 | A = 97,
31 | Z = 122,
32 |
33 | lower = function(c) {
34 | return c | 0x20;
35 | };
36 |
37 | for (var s in S) {
38 | exports[s] = S[s];
39 | }
40 |
41 | function MultipartParser() {
42 | this.boundary = null;
43 | this.boundaryChars = null;
44 | this.lookbehind = null;
45 | this.state = S.PARSER_UNINITIALIZED;
46 |
47 | this.index = null;
48 | this.flags = 0;
49 | };
50 | exports.MultipartParser = MultipartParser;
51 |
52 | MultipartParser.stateToString = function(stateNumber) {
53 | for (var state in S) {
54 | var number = S[state];
55 | if (number === stateNumber) return state;
56 | }
57 | };
58 |
59 | MultipartParser.prototype.initWithBoundary = function(str) {
60 | this.boundary = new Buffer(str.length+4);
61 | this.boundary.write('\r\n--', 'ascii', 0);
62 | this.boundary.write(str, 'ascii', 4);
63 | this.lookbehind = new Buffer(this.boundary.length+8);
64 | this.state = S.START;
65 |
66 | this.boundaryChars = {};
67 | for (var i = 0; i < this.boundary.length; i++) {
68 | this.boundaryChars[this.boundary[i]] = true;
69 | }
70 | };
71 |
72 | MultipartParser.prototype.write = function(buffer) {
73 | var self = this,
74 | i = 0,
75 | len = buffer.length,
76 | prevIndex = this.index,
77 | index = this.index,
78 | state = this.state,
79 | flags = this.flags,
80 | lookbehind = this.lookbehind,
81 | boundary = this.boundary,
82 | boundaryChars = this.boundaryChars,
83 | boundaryLength = this.boundary.length,
84 | boundaryEnd = boundaryLength - 1,
85 | bufferLength = buffer.length,
86 | c,
87 | cl,
88 |
89 | mark = function(name) {
90 | self[name+'Mark'] = i;
91 | },
92 | clear = function(name) {
93 | delete self[name+'Mark'];
94 | },
95 | callback = function(name, buffer, start, end) {
96 | if (start !== undefined && start === end) {
97 | return;
98 | }
99 |
100 | var callbackSymbol = 'on'+name.substr(0, 1).toUpperCase()+name.substr(1);
101 | if (callbackSymbol in self) {
102 | self[callbackSymbol](buffer, start, end);
103 | }
104 | },
105 | dataCallback = function(name, clear) {
106 | var markSymbol = name+'Mark';
107 | if (!(markSymbol in self)) {
108 | return;
109 | }
110 |
111 | if (!clear) {
112 | callback(name, buffer, self[markSymbol], buffer.length);
113 | self[markSymbol] = 0;
114 | } else {
115 | callback(name, buffer, self[markSymbol], i);
116 | delete self[markSymbol];
117 | }
118 | };
119 |
120 | for (i = 0; i < len; i++) {
121 | c = buffer[i];
122 | switch (state) {
123 | case S.PARSER_UNINITIALIZED:
124 | return i;
125 | case S.START:
126 | index = 0;
127 | state = S.START_BOUNDARY;
128 | case S.START_BOUNDARY:
129 | if (index == boundary.length - 2) {
130 | if (c != CR) {
131 | return i;
132 | }
133 | index++;
134 | break;
135 | } else if (index - 1 == boundary.length - 2) {
136 | if (c != LF) {
137 | return i;
138 | }
139 | index = 0;
140 | callback('partBegin');
141 | state = S.HEADER_FIELD_START;
142 | break;
143 | }
144 |
145 | if (c != boundary[index+2]) {
146 | return i;
147 | }
148 | index++;
149 | break;
150 | case S.HEADER_FIELD_START:
151 | state = S.HEADER_FIELD;
152 | mark('headerField');
153 | index = 0;
154 | case S.HEADER_FIELD:
155 | if (c == CR) {
156 | clear('headerField');
157 | state = S.HEADERS_ALMOST_DONE;
158 | break;
159 | }
160 |
161 | index++;
162 | if (c == HYPHEN) {
163 | break;
164 | }
165 |
166 | if (c == COLON) {
167 | if (index == 1) {
168 | // empty header field
169 | return i;
170 | }
171 | dataCallback('headerField', true);
172 | state = S.HEADER_VALUE_START;
173 | break;
174 | }
175 |
176 | cl = lower(c);
177 | if (cl < A || cl > Z) {
178 | return i;
179 | }
180 | break;
181 | case S.HEADER_VALUE_START:
182 | if (c == SPACE) {
183 | break;
184 | }
185 |
186 | mark('headerValue');
187 | state = S.HEADER_VALUE;
188 | case S.HEADER_VALUE:
189 | if (c == CR) {
190 | dataCallback('headerValue', true);
191 | callback('headerEnd');
192 | state = S.HEADER_VALUE_ALMOST_DONE;
193 | }
194 | break;
195 | case S.HEADER_VALUE_ALMOST_DONE:
196 | if (c != LF) {
197 | return i;
198 | }
199 | state = S.HEADER_FIELD_START;
200 | break;
201 | case S.HEADERS_ALMOST_DONE:
202 | if (c != LF) {
203 | return i;
204 | }
205 |
206 | callback('headersEnd');
207 | state = S.PART_DATA_START;
208 | break;
209 | case S.PART_DATA_START:
210 | state = S.PART_DATA
211 | mark('partData');
212 | case S.PART_DATA:
213 | prevIndex = index;
214 |
215 | if (index == 0) {
216 | // boyer-moore derrived algorithm to safely skip non-boundary data
217 | i += boundaryEnd;
218 | while (i < bufferLength && !(buffer[i] in boundaryChars)) {
219 | i += boundaryLength;
220 | }
221 | i -= boundaryEnd;
222 | c = buffer[i];
223 | }
224 |
225 | if (index < boundary.length) {
226 | if (boundary[index] == c) {
227 | if (index == 0) {
228 | dataCallback('partData', true);
229 | }
230 | index++;
231 | } else {
232 | index = 0;
233 | }
234 | } else if (index == boundary.length) {
235 | index++;
236 | if (c == CR) {
237 | // CR = part boundary
238 | flags |= F.PART_BOUNDARY;
239 | } else if (c == HYPHEN) {
240 | // HYPHEN = end boundary
241 | flags |= F.LAST_BOUNDARY;
242 | } else {
243 | index = 0;
244 | }
245 | } else if (index - 1 == boundary.length) {
246 | if (flags & F.PART_BOUNDARY) {
247 | index = 0;
248 | if (c == LF) {
249 | // unset the PART_BOUNDARY flag
250 | flags &= ~F.PART_BOUNDARY;
251 | callback('partEnd');
252 | callback('partBegin');
253 | state = S.HEADER_FIELD_START;
254 | break;
255 | }
256 | } else if (flags & F.LAST_BOUNDARY) {
257 | if (c == HYPHEN) {
258 | callback('partEnd');
259 | callback('end');
260 | state = S.END;
261 | } else {
262 | index = 0;
263 | }
264 | } else {
265 | index = 0;
266 | }
267 | }
268 |
269 | if (index > 0) {
270 | // when matching a possible boundary, keep a lookbehind reference
271 | // in case it turns out to be a false lead
272 | lookbehind[index-1] = c;
273 | } else if (prevIndex > 0) {
274 | // if our boundary turned out to be rubbish, the captured lookbehind
275 | // belongs to partData
276 | callback('partData', lookbehind, 0, prevIndex);
277 | prevIndex = 0;
278 | mark('partData');
279 |
280 | // reconsider the current character even so it interrupted the sequence
281 | // it could be the beginning of a new sequence
282 | i--;
283 | }
284 |
285 | break;
286 | case S.END:
287 | break;
288 | default:
289 | return i;
290 | }
291 | }
292 |
293 | dataCallback('headerField');
294 | dataCallback('headerValue');
295 | dataCallback('partData');
296 |
297 | this.index = index;
298 | this.state = state;
299 | this.flags = flags;
300 |
301 | return len;
302 | };
303 |
304 | MultipartParser.prototype.end = function() {
305 | if (this.state != S.END) {
306 | return new Error('MultipartParser.end(): stream ended unexpectedly: ' + this.explain());
307 | }
308 | };
309 |
310 | MultipartParser.prototype.explain = function() {
311 | return 'state = ' + MultipartParser.stateToString(this.state);
312 | };
313 |
--------------------------------------------------------------------------------
/src/js/registry/nexus.js:
--------------------------------------------------------------------------------
1 | /**
2 | * nexus 类型的registry在新版功能中不兼容,已弃用!!!
3 | */
4 |
5 | var path = require('path'),
6 | fs = require('fs'),
7 | stream = require('stream'),
8 | fsExtra = require('fs-extra'),
9 | fstream = require('fstream'),
10 | request = require('request'),
11 | _ = require('lodash');
12 |
13 | var utils = require('../common/utils'),
14 | constant = require('../common/constant');
15 |
16 | var UPLOADDIR = constant.UPLOADDIR;
17 |
18 | function nexusRegistry(config) {
19 | this.repository = path.resolve(config.server, config.repository);
20 | var auth = config.auth && config.auth.split(':');
21 | auth && (this.auth = {
22 | user: auth[0],
23 | password: auth[1]
24 | });
25 | this.fileExt = '.tar';
26 | }
27 |
28 | /**
29 | * 拉取一个依赖
30 | * @param {String} name 模块名
31 | * @param {path} dir 放置到的文件夹
32 | * @param {Function} callback [description]
33 | * @return {void} [description]
34 | */
35 | nexusRegistry.prototype._get = function(name, dir, callback) {
36 | var url = path.resolve(this.repository, name + this.fileExt);
37 | request.get({
38 | url: url
39 | }).on('response', function(res) {
40 | if (res.statusCode == 200) {
41 | // 获取文件名称
42 | var target = path.resolve(dir, moduleName),
43 | river = new stream.PassThrough();
44 |
45 | // 请求返回流通过管道流入解压流
46 | res.pipe(river);
47 |
48 | // 解压文件操作
49 | river.pipe(Utils.extract(target, dir, function(){
50 | cb(null, fs.existsSync(target) && target);
51 | }));
52 | return;
53 | } else {
54 | callback();
55 | }
56 | });
57 | };
58 |
59 | /**
60 | * 按照模块名或者含环境的模块名拉取依赖
61 | * @param {String} moduleName 不含环境的模块名
62 | * @param {String} moduleNameForPlatform 含环境的模块名
63 | * @param {path} dir 放置到的文件夹
64 | * @param {Function} callback [description]
65 | * @return {void} [description]
66 | */
67 | nexusRegistry.prototype.get = function(moduleName, moduleNameForPlatform, dir, callback) {
68 | var self = this;
69 | console.info('try get', moduleName);
70 | self._get(moduleName, dir, function(err) {
71 | if (err) {
72 | console.info('try get', moduleNameForPlatform);
73 | self._get(moduleNameForPlatform, dir, callback);
74 | } else {
75 | callback();
76 | }
77 | })
78 | };
79 |
80 |
81 | /**
82 | * 放置一个模块(文件夹)到公共缓存
83 | * @param {path} target 模块路径
84 | * @param {Function} callback [description]
85 | * @return {void} [description]
86 | */
87 | nexusRegistry.prototype._put = function(target, callback) {
88 | var name = path.basename(target),
89 | url = this.repository + name + this.fileExt;
90 |
91 | var river = new stream.PassThrough();
92 |
93 | utils.compress(target, UPLOADDIR, constant.COMPRESS_TYPE.TAR).pipe(river);
94 |
95 | request.put({
96 | url: url,
97 | auth: this.auth,
98 | body: river
99 | }, function(err, res, body) {
100 | if (err || res.statusCode !== 201) {
101 | console.error(name, '上传失败', err ? err.message || JSON.stringify(err) : res.toJSON());
102 | callback(err || new Error('send fail, statusCode:' + res.statusCode));
103 | return;
104 | }
105 | console.info(name, '上传成功');
106 | callback();
107 | });
108 | };
109 |
110 |
111 | /**
112 | * 放置一批模块到公共缓存
113 | * @param {path} dir 模块目录
114 | * @param {Function} callback [description]
115 | * @return {void} [description]
116 | */
117 | nexusRegistry.prototype.put = function(dir, callback) {
118 | var self = this;
119 | fs.readdir(dir, function(err, files) {
120 | if (err) {
121 | callback(err);
122 | return;
123 | }
124 | var bulk = [];
125 | _.forEach(files, function(file) {
126 | bulk.push(
127 | new Promise(function(resolve, reject) {
128 | self._put(path.resolve(dir, file), function(err) {
129 | if (err) {
130 | reject(err);
131 | } else {
132 | resolve();
133 | }
134 | });
135 | })
136 | );
137 | });
138 | Promise.all(bulk).then(function() {
139 | callback();
140 | }, function(err) {
141 | callback(err);
142 | });
143 | });
144 | };
145 |
146 | /**
147 | * 校验nexus服务是否可用,并返回服务端与当前工程模块依赖的交集
148 | * @param {Function} cb 检查完后的回调
149 | * @param {JSON} dependencies 工程的模块依赖
150 | * @return {void}
151 | */
152 | nexusRegistry.prototype.check = function(cb, dependencies) {
153 | //TODO 目前仅检查了可以访问,还需更精确的确定源可用,用户密码可用
154 | request.get(this.repository)
155 | .on('response', _.bind(function() {
156 | cb(this.serverHealth = true, {});
157 | }, this))
158 | .on('error', _.bind(function(err) {
159 | console.error(this.repository + '该服务不可正常访问,请检查服务!', err);
160 | cb(this.serverHealth = false, {});
161 | }, this));
162 | };
163 |
164 | module.exports = nexusRegistry;
165 |
--------------------------------------------------------------------------------
/src/js/registry/node.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @Author: wyw.wang
3 | * @Date: 2016-09-09 16:09
4 | * @Email: wyw.wang@qunar.com
5 | * @Last modified by: robin
6 | * @Last modified time: 2016-09-19 15:20:19
7 | */
8 |
9 | var path = require('path'),
10 | fs = require('fs'),
11 | fsExtra = require('fs-extra'),
12 | fstream = require('fstream'),
13 | request = require('request'),
14 | _ = require('lodash');
15 |
16 | var utils = require('../common/utils'),
17 | constant = require('../common/constant');
18 |
19 | var UPLOADDIR = constant.UPLOADDIR;
20 |
21 | /*@Factory("node")*/
22 | function nodeRegistry(config) {
23 | //兼容http://有无场景
24 | this.server = RegExp("https?:\/\/").test(config.repository) ? config.repository : 'http://' + config.repository;
25 | this.token = config.token;
26 | this.fileExt = utils.getFileExt();
27 | }
28 |
29 | /**
30 | * 从公共缓存拉取模块
31 | * @param {String} moduleName 不含环境的模块名
32 | * @param {String} moduleUrl 资源路径
33 | * @param {path} dir 将要放置到的目录路径
34 | * @param {Function} cb [description]
35 | * @return {void} [description]
36 | */
37 | nodeRegistry.prototype.get = function(moduleName, moduleUrl, dir, cb) {
38 | request
39 | .get({
40 | url: moduleUrl || generateUrl()
41 | })
42 | .on('response', function(response) {
43 | if (response.statusCode == 200) {
44 | // 获取文件名称
45 | var target = path.resolve(dir, [moduleName, constant.COMPRESS_TYPE.TAR].join('.'));
46 |
47 | // 解压文件操作
48 | response.pipe(utils.extract(target, dir, function(){
49 | cb(null, fs.existsSync(target) && target);
50 | }));
51 | return;
52 | } else {
53 | cb(new Error('下载模块异常:'+moduleName+',statusCode:'+response.statusCode));
54 | }
55 | })
56 | .on('error', function(err) {
57 | console.error(err);
58 | cb(err);
59 | });
60 | function generateUrl(){
61 | return [this.server, 'fetch', moduleName].join('/');
62 | }
63 | };
64 |
65 | /**
66 | * 上传模块目录到公共缓存
67 | * @param {path} dir 待上传的路径
68 | * @param {json} info 附加信息
69 | * @param {Function} callback [description]
70 | * @return {void} [description]
71 | */
72 | nodeRegistry.prototype.put = function(dir, info, callback) {
73 | if (!this.serverReady() || !fs.existsSync(dir)) {
74 | callback('上传模块失败!!');
75 | return;
76 | }
77 | console.info('开始压缩需要上传模块');
78 |
79 | var self = this,
80 | tmpFile = path.resolve(path.dirname(dir), Date.now() + self.fileExt),
81 | river = fs.createWriteStream(tmpFile);
82 |
83 | river.on('error', function(err) {
84 | console.error(err);
85 | callback(err);
86 | }).on('finish', function() {
87 | console.info('同步模块至服务' + self.server);
88 | var formData = info || {};
89 | request.post({
90 | headers: {
91 | token: self.token
92 | },
93 | url: self.server + '/upload',
94 | formData: _.extend(formData, {
95 | modules: fs.createReadStream(tmpFile)
96 | })
97 | }, function(err, response, body) {
98 | //删除上传文件
99 | fsExtra.removeSync(tmpFile);
100 |
101 | if (err) {
102 | console.error('上传失败:', err);
103 | callback(err);
104 | } else {
105 | var res, error;
106 | try {
107 | res = JSON.parse(body);
108 | } catch (e) {
109 | error = e;
110 | }
111 | if(error || res.status !== 0){
112 | console.error('上传发生错误:', error || res.message);
113 | callback(res && res.message);
114 | } else {
115 | console.info('上传成功');
116 | callback();
117 | }
118 | }
119 | });
120 | });
121 |
122 | utils.compress(dir, UPLOADDIR, constant.COMPRESS_TYPE.TAR).pipe(river);
123 | };
124 |
125 |
126 | /**
127 | * 判断服务是否正常,并返回服务端与当前工程模块依赖的交集
128 | * @param {Array} list 工程的模块依赖
129 | * @param {Array} checkSyncList 待检查是否需要同步的模块
130 | * @param {Function} cb 检查完后的回调
131 | * @return {void}
132 | */
133 | nodeRegistry.prototype.check = function(list, checkSyncList, cb) {
134 | var self = this;
135 | if (!self.server) {
136 | cb(false, null);
137 | return;
138 | }
139 | var form = {
140 | list: list,
141 | checkSyncList: checkSyncList,
142 | platform: utils.getPlatform()
143 | };
144 | request
145 | .post({
146 | url: self.server + '/check',
147 | form: form
148 | }, function(err, response, body) {
149 | if(err){
150 | console.error(self.server + '该服务不可正常访问,请检查服务!', err);
151 | cb(self.serverHealth = false, {});
152 | } else {
153 | var res, error;
154 | try {
155 | res = JSON.parse(body);
156 | } catch (e) {
157 | error = e;
158 | }
159 | if(error || res.status !== 0){
160 | console.error(self.server + '服务异常,请检查服务!', error || res.message);
161 | cb(self.serverHealth = false, {});
162 | } else {
163 | cb(self.serverHealth = true, res.data);
164 | }
165 | }
166 | });
167 | };
168 |
169 |
170 | nodeRegistry.prototype.info = function(name, version, cb){
171 | var self = this,
172 | url = this.server + '/info';
173 | request.post({
174 | url: url,
175 | form: {
176 | name: name,
177 | version: version,
178 | platform: utils.getPlatform()
179 | }
180 | }, function(err, response, body){
181 | if(err){
182 | console.error(url + '该服务不可正常访问,请检查服务!', err);
183 | cb(true);
184 | } else {
185 | var res, error;
186 | try {
187 | res = JSON.parse(body);
188 | } catch (e) {
189 | error = e;
190 | }
191 | if(error || res.status !== 0){
192 | console.error(url + '服务异常,请检查服务!', error || res.message);
193 | cb(true);
194 | } else {
195 | if(res.data.full){
196 | res.data.url = [self.server, 'fetch', res.data.full].join('/');
197 | }
198 | cb(null, res.data);
199 | }
200 | }
201 | });
202 | };
203 | /**
204 | * 判断服务是否正常
205 | * @return {[type]} [description]
206 | */
207 | nodeRegistry.prototype.serverReady = function() {
208 | if (!this.server) return false;
209 | if (this.serverHealth) return true;
210 | if (this.serverHealth === false) return false;
211 | return false;
212 | };
213 |
214 | module.exports = nodeRegistry;
215 |
--------------------------------------------------------------------------------