1 | const jsonfile = require('jsonfile');
|
2 | const fs = require("fs-extra");
|
3 | const path = require("path");
|
4 | const ora = require('ora');
|
5 | const decompress = require('decompress');
|
6 | const tmp = require('tmp');
|
7 | const request = require('request').defaults({
|
8 | headers: {
|
9 | 'User-Agent': 'request'
|
10 | }
|
11 | });
|
12 |
|
13 | const utils = require('./lib/utils');
|
14 | const logger = require("./lib/utils/logger");
|
15 |
|
16 | class TemplateRelease {
|
17 | |
18 |
|
19 |
|
20 |
|
21 |
|
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 |
|
37 |
|
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 |
|
56 |
|
57 |
|
58 |
|
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 |
|
130 |
|
131 |
|
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 |
|
165 |
|
166 |
|
167 |
|
168 |
|
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 |
|
228 |
|
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 |
|
243 | module.exports = TemplateRelease;
|