UNPKG

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