forked from kuaifan/eeui-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtemplate-release.js
243 lines (232 loc) · 9.01 KB
/
template-release.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
const jsonfile = require('jsonfile');
const fs = require("fs-extra");
const path = require("path");
const ora = require('ora');
const decompress = require('decompress');
const tmp = require('tmp');
const request = require('request').defaults({
headers: {
'User-Agent': 'request'
}
});
const utils = require('./lib/utils');
const logger = require("./lib/utils/logger");
class TemplateRelease {
/**
* 构造函数,必须传入以下参数
* @param {String} name 项目的名称,将用于在家目录创建形如 .eeui 的缓存目录
* @param {String} releaseUrl 形如 https://api.github.com/repos/kuaifan/eeui-template/releases
* @return {TemplateRelease}
*/
constructor (name, releaseUrl) {
if (!name || !releaseUrl) {
throw new Error('Invalid argument');
}
this.name = name;
this.releaseUrl = releaseUrl;
this.CACHE_DIR_NAME = '.' + name;
this.CACHE_DIR_PATH = path.join(require('os').homedir(), this.CACHE_DIR_NAME);
this.CACHE_TEMPLATE_PATH = path.join(this.CACHE_DIR_PATH, "template");
this.RELEASES_JSON_PATH = path.join(this.CACHE_TEMPLATE_PATH, "release.json");
}
/**
* 获取所有 release 的版本。只获取版本,不会下载到缓存区。
* @param {Function} cb 接受参数 error 以及无错时的版本数组 []string
*/
fetchReleaseVersions(cb) {
request.get(this.releaseUrl, function(err, res, body){
if (err) {
cb && cb(err);
return;
}
if (res.statusCode !== 200) {
cb && cb(`获取信息失败 - ${res.statusCode}: ${res.body}`);
return;
}
let tags = JSON.parse(body).map(function(e){return e["tag_name"]});
cb && cb(null, tags);
});
}
/**
* 获取指定版本的 release,首先尝试缓存(CACHE_TEMPLATE_PATH),如果未缓存,再尝试下载并缓存
* @param {string} release 指定版本,如果为空,表示最新版本
* @param {string} location 下载服务器
* @param {Function} cb 通过该回调返回错误 error,以及无错时的 release 的路径,一般形如 ~/.eeui/template/0.1.0
*/
fetchRelease(release, location, cb) {
let releasesInfo = this._readReleaseJSON();
if (release) {
let info = releasesInfo[release];
if (info) {
cb(null, path.join(this.CACHE_TEMPLATE_PATH, info.path));
return;
}
}
let url = this._getReleaseUrl(release);
if (location === 'eeui') {
url = url.replace('https://api.github.com/repos/', utils.apiUrl() + 'releases/')
}
let spinText = `正在下载模板版本: ${release ? release : "latest"}...`;
let spinDown = ora(spinText);
spinDown.start();
request(url, (err, res, body) => {
spinDown.stop();
if (err || res.statusCode !== 200) {
let errorInfo = err ? err : `${res.statusCode}: ${res.body}`;
logger.info(`未能下载 ${url} - ${errorInfo}`);
logger.info('正在清除缓存...');
if (!release) {
let latestRleaseInfo = this.getCachedReleaseInfo();
if (latestRleaseInfo) {
logger.info(`在缓存中找到最新版本: ${latestRleaseInfo.tag}.`);
cb(null, path.join(this.CACHE_TEMPLATE_PATH, latestRleaseInfo.path));
return;
}
}
cb(`未能获取版本 ${release ? release : "latest"}: ${errorInfo}`);
return;
}
//
let info = JSON.parse(body);
if (location === 'eeui') {
if (info.ret !== 1) {
logger.fatal(info.msg || "未知错误,请选择其他下载服务器!");
}
info = info['data'];
}
let newInfo = {};
let tag = newInfo.tag = info["tag_name"];
newInfo.time = info["published_at"];
newInfo.path = newInfo.tag;
let targetPath = path.join(this.CACHE_TEMPLATE_PATH, newInfo.path);
if (fs.pathExistsSync(targetPath)) {
logger.info(`已经缓存的版本。`);
cb(null, targetPath);
return;
}
spinDown.start();
this._downloadAndUnzip(info["zipball_url"], targetPath, (err) => {
spinDown.stop();
if (err) {
cb && cb(err);
return;
}
releasesInfo[tag] = newInfo;
jsonfile.writeFileSync(this.RELEASES_JSON_PATH, releasesInfo, {spaces: 2});
cb(null, targetPath);
}, (res) => {
spinDown.text = spinText + `(${res.progress}, ${res.speed})`;
});
});
}
/**
* 返回缓存里的 release 信息
* @param {string} [release] 指定版本,不指定则返回最新
* @return {Object} release 信息
*/
getCachedReleaseInfo(release) {
let releasesInfo = this._readReleaseJSON();
if (release) {
return releasesInfo[release];
}
let latestRleaseInfo = null;
for (let tag in releasesInfo) {
let info = releasesInfo[tag];
if (!latestRleaseInfo) {
latestRleaseInfo = info;
} else {
if (Date.parse(info.time) > Date.parse(latestRleaseInfo.time)) latestRleaseInfo = info;
}
}
return latestRleaseInfo;
}
_readReleaseJSON() {
fs.ensureFileSync(this.RELEASES_JSON_PATH);
try {
return jsonfile.readFileSync(this.RELEASES_JSON_PATH);
} catch (e) {
return {};
}
}
_getReleaseUrl(tag) {
return this.releaseUrl + "/" + (tag ? `tags/${tag}` : "latest");
}
/**
* 把 url (zipball_url) 的内容下载并解压到 savePath
* @param {string} url
* @param {string} savePath
* @param {Function} cb 接收参数 error
* @param {Function} progressCall 接收进度
*/
_downloadAndUnzip(url, savePath, cb, progressCall) {
let TMP_DOWNLOAD_PATH = tmp.tmpNameSync({dir: require('os').tmpdir()}) + ".zip";
let file = fs.createWriteStream(TMP_DOWNLOAD_PATH);
file.on("close", () => {
decompress(TMP_DOWNLOAD_PATH, this.CACHE_TEMPLATE_PATH).then(() => {
let origPath = this._getLastReleasePath();
fs.moveSync(origPath, savePath); // 重命名为指定名
fs.unlinkSync(TMP_DOWNLOAD_PATH); // 删除下载的压缩包
cb && cb();
}).catch((err) => {
cb && cb(`下载版本失败: ${err}`);
});
}).on("error", (err) => {
cb && cb(err)
});
//
let receivedBytes = 0;
let totalBytes = 0;
let speedBytes = 0;
let speedPer = "0B/S";
let speedInt = setInterval(() => {
speedPer = utils.renderSize(Math.max(0, receivedBytes - speedBytes)) + "/S";
speedBytes = receivedBytes;
}, 1000);
request.get(url)
.on("error", function (err) {
cb && cb(`下载版本错误: ${err}`);
})
.on("response", function (res) {
if (res.statusCode !== 200) {
cb && cb("Get zipUrl return a non-200 response.");
}
totalBytes = parseInt(res.headers['content-length'], 10);
if (isNaN(totalBytes)) totalBytes = 0;
})
.on('data', (chunk) => {
receivedBytes += chunk.length;
let progress = "0%";
if (totalBytes > 0) {
progress = parseFloat(Math.max(0, receivedBytes / totalBytes * 100).toFixed(2)) + "%";
}else{
progress = utils.renderSize(receivedBytes);
}
progressCall && progressCall({
received: receivedBytes,
total: totalBytes,
speed: speedPer,
progress: progress
});
})
.on("end", function () {
clearInterval(speedInt);
})
.pipe(file);
}
/**
* 获取刚下载解压的 release 的路径
* TODO: 目前无法准确获取 release 解压之后的目录名称,只能根据某种模式推断
*/
_getLastReleasePath() {
let files = fs.readdirSync(this.CACHE_TEMPLATE_PATH);
let part = this.releaseUrl.split('/');
const pattern = part[part.length - 2];
for (let f of files) {
if (f.indexOf(pattern) !== -1) {
return path.join( this.CACHE_TEMPLATE_PATH, f);
}
}
return null;
}
}
module.exports = TemplateRelease;