UNPKG

8.4 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': 'node request'
10 }
11});
12
13let logger = require("./lib/utils/logger");
14
15class TemplateRelease {
16 /**
17 * 构造函数,必须传入以下参数
18 * @param {String} name 项目的名称,将用于在家目录创建形如 .weiui 的缓存目录
19 * @param {String} releaseUrl 形如 https://api.github.com/repos/kuaifan/weiui-template/releases
20 * @return {TemplateRelease}
21 */
22 constructor (name, releaseUrl) {
23 if (!name || !releaseUrl) {
24 throw new Error('Invalid argument');
25 }
26 this.name = name;
27 this.releaseUrl = releaseUrl;
28 this.CACHE_DIR_NAME = '.' + name;
29 this.CACHE_DIR_PATH = path.join(require('os').homedir(), this.CACHE_DIR_NAME);
30 this.CACHE_TEMPLATE_PATH = path.join(this.CACHE_DIR_PATH, "template");
31 this.RELEASES_JSON_PATH = path.join(this.CACHE_TEMPLATE_PATH, "release.json");
32 this.TEMPLATE_DIR_NAME = "templates"; // 存放各种模版的目录
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 {Function} cb 通过该回调返回错误 error,以及无错时的 release 的路径,一般形如 ~/.weiui/template/0.1.0
58 */
59 fetchRelease(release, cb) {
60 let releasesInfo = this._readReleaseJSON();
61 if (release) {
62 let info = releasesInfo[release];
63 if (info) {
64 cb(null, path.join(this.CACHE_TEMPLATE_PATH, info.path));
65 return;
66 }
67 }
68
69 let url = this._getReleaseUrl(release);
70 let spinDown = ora(`正在下载模板版本: ${release ? release : "latest"}...`);
71 spinDown.start();
72 request(url, (err, res, body) => {
73 spinDown.stop();
74 if (err || res.statusCode !== 200) {
75 let errorInfo = err ? err : `${res.statusCode}: ${res.body}`;
76 logger.weiui(`未能下载 ${url} - ${errorInfo}`);
77 logger.weiui('正在清除缓存...');
78 if (!release) {
79 let latestRleaseInfo = this.getCachedReleaseInfo();
80 if (latestRleaseInfo) {
81 logger.weiui(`在缓存中找到最新版本: ${latestRleaseInfo.tag}.`);
82 cb(null, path.join(this.CACHE_TEMPLATE_PATH, latestRleaseInfo.path));
83 return;
84 }
85 }
86 cb(`未能获取版本 ${release ? release : "latest"}: ${errorInfo}`);
87 return;
88 }
89 let info = JSON.parse(body);
90 let newInfo = {};
91 let tag = newInfo.tag = info["tag_name"];
92 newInfo.time = info["published_at"];
93 newInfo.path = newInfo.tag;
94 let targetPath = path.join(this.CACHE_TEMPLATE_PATH, newInfo.path);
95 if (fs.pathExistsSync(targetPath)) {
96 logger.weiui(`已经缓存的版本。`);
97 cb(null, targetPath);
98 return;
99 }
100 spinDown.start();
101 this._downloadAndUnzip(info["zipball_url"], targetPath, (err) => {
102 spinDown.stop();
103 if (err) {
104 cb && cb(err);
105 return;
106 }
107 releasesInfo[tag] = newInfo;
108 jsonfile.writeFileSync(this.RELEASES_JSON_PATH, releasesInfo, {spaces: 2});
109 cb(null, targetPath);
110 });
111 });
112 }
113
114 /**
115 * 从 release 的项目路径里读取 templates 目录下的所有模版名称
116 * @param {[type]} projectPath [description]
117 * @return {[type]} [description]
118 */
119 getAvailableTemplateNames(projectPath) {
120 let result = [];
121 let tDir = path.join(projectPath, this.TEMPLATE_DIR_NAME);
122 if (!fs.existsSync(tDir)) return result;
123 let files = fs.readdirSync(tDir);
124 for (let f of files) {
125 if (fs.statSync(path.join(tDir, f)).isDirectory()) {
126 result.push(f);
127 }
128 }
129 return result;
130 }
131
132 /**
133 * 返回缓存里的 release 信息
134 * @param {string} [release] 指定版本,不指定则返回最新
135 * @return {Object} release 信息
136 */
137 getCachedReleaseInfo(release) {
138 let releasesInfo = this._readReleaseJSON();
139 if (release) {
140 return releasesInfo[release];
141 }
142 let latestRleaseInfo = null;
143 for (let tag in releasesInfo) {
144 let info = releasesInfo[tag];
145 if (!latestRleaseInfo) {
146 latestRleaseInfo = info;
147 } else {
148 if (Date.parse(info.time) > Date.parse(latestRleaseInfo.time)) latestRleaseInfo = info;
149 }
150 }
151 return latestRleaseInfo;
152 }
153
154 /**
155 * 返回缓存里的 release 路径
156 * @param {string} [release] 指定版本,不指定则返回最新
157 * @return {string} release 路径
158 */
159 getCachedRelease(release) {
160 let info = this.getCachedReleaseInfo(release);
161 return info ? path.join(this.CACHE_TEMPLATE_PATH, info.path) : null;
162 }
163
164 _readReleaseJSON() {
165 fs.ensureFileSync(this.RELEASES_JSON_PATH);
166 try {
167 return jsonfile.readFileSync(this.RELEASES_JSON_PATH);
168 } catch (e) {
169 return {};
170 }
171 }
172
173 _getReleaseUrl(tag) {
174 return this.releaseUrl + "/" + (tag ? `tags/${tag}` : "latest");
175 }
176
177 /**
178 * 把 url (zipball_url) 的内容下载并解压到 savePath
179 * @param {string} url
180 * @param {string} savePath
181 * @param {Function} cb 接收参数 error
182 */
183 _downloadAndUnzip(url, savePath, cb) {
184 const TMP_DOWNLOAD_PATH = tmp.tmpNameSync({dir: require('os').tmpdir()}) + ".zip";
185 let file = fs.createWriteStream(TMP_DOWNLOAD_PATH);
186 file.on("close", () => {
187 decompress(TMP_DOWNLOAD_PATH, this.CACHE_TEMPLATE_PATH).then(() => {
188 let origPath = this._getLastReleasePath();
189 fs.moveSync(origPath, savePath); // 重命名为指定名
190 fs.unlinkSync(TMP_DOWNLOAD_PATH); // 删除下载的压缩包
191 cb && cb();
192 })
193 }).on("error", (err) => {
194 cb && cb(err)
195 });
196 request.get(url)
197 .on("error", function (err) {
198 cb && cb(`下载版本错误: ${err}`);
199 })
200 .on("response", function (res) {
201 if (res.statusCode !== 200) {
202 cb && cb("Get zipUrl return a non-200 response.");
203 }
204 })
205 .on("end", function () {
206 //
207 })
208 .pipe(file);
209 }
210
211 /**
212 * 获取刚下载解压的 release 的路径
213 * TODO: 目前无法准确获取 release 解压之后的目录名称,只能根据某种模式推断
214 */
215 _getLastReleasePath() {
216 let files = fs.readdirSync(this.CACHE_TEMPLATE_PATH);
217 let part = this.releaseUrl.split('/');
218 const pattern = part[part.length - 2];
219 for (let f of files) {
220 if (f.indexOf(pattern) !== -1) {
221 return path.join( this.CACHE_TEMPLATE_PATH, f);
222 }
223 }
224 return null;
225 }
226}
227
228module.exports = TemplateRelease;