1 | 'use strict';
|
2 |
|
3 |
|
4 |
|
5 | var crypto = require('crypto');
|
6 | var fs = require('graceful-fs');
|
7 | var path = require('path');
|
8 | var url = require('url');
|
9 |
|
10 |
|
11 |
|
12 | var AppCache = require('@jokeyrhyme/appcache');
|
13 |
|
14 | var mkdirp = require('mkdirp');
|
15 | var request = require('request');
|
16 | var temp = require('temp').track();
|
17 |
|
18 |
|
19 |
|
20 | var FetcherIndex = require(path.join(__dirname, 'lib', 'fetcher-index'));
|
21 |
|
22 | var doBrowserify = require(path.join(__dirname, 'lib', 'do-browserify'));
|
23 | var urlVars = require(path.join(__dirname, 'lib', 'url_variations'));
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | function Fetcher (opts) {
|
32 | this.date = new Date();
|
33 | this.remoteUrl = opts.remoteUrl;
|
34 | this.localPath = opts.localPath;
|
35 | this.tempPath = '';
|
36 | this.index = new FetcherIndex({ remoteUrl: this.remoteUrl });
|
37 | this.manifestUrl = '';
|
38 |
|
39 | this.transforms = {
|
40 | css: [],
|
41 | html: [],
|
42 | js: []
|
43 | };
|
44 | this.extractors = {
|
45 | manifestUrl: []
|
46 | };
|
47 |
|
48 | this.addExtractor('manifestUrl', require(path.join(__dirname, 'lib', 'extractors', 'manifestUrl.w3c')));
|
49 |
|
50 | this.addTransform('css', require(path.join(__dirname, 'lib', 'transforms', 'css.localUrls')));
|
51 | this.addTransform('html', require(path.join(__dirname, 'lib', 'transforms', 'html.removeManifest')));
|
52 | this.addTransform('html', require(path.join(__dirname, 'lib', 'transforms', 'html.localLinkHrefs')));
|
53 | this.addTransform('html', require(path.join(__dirname, 'lib', 'transforms', 'html.localScriptSrcs')));
|
54 | this.addTransform('html', require(path.join(__dirname, 'lib', 'transforms', 'html.injectAppCacheIndex')));
|
55 | this.addTransform('html', require(path.join(__dirname, 'lib', 'transforms', 'html.injectRequireJsShim')));
|
56 | }
|
57 |
|
58 | Fetcher.prototype.addExtractor = function (prop, fn) {
|
59 | if (!Array.isArray(this.extractors[prop])) {
|
60 | this.extractors[prop] = [];
|
61 | }
|
62 | this.extractors[prop].push(fn);
|
63 | };
|
64 |
|
65 | Fetcher.prototype.addTransform = function (ext, fn) {
|
66 | if (!Array.isArray(this.transforms[ext])) {
|
67 | this.transforms[ext] = [];
|
68 | }
|
69 | this.transforms[ext].push(fn);
|
70 | };
|
71 |
|
72 | Fetcher.prototype.afterTempPath = function () {
|
73 | var me = this;
|
74 |
|
75 | return new Promise(function (resolve, reject) {
|
76 | temp.mkdir('appcache-fetcher-' + me.date.valueOf(), function (err, dirPath) {
|
77 | if (err) {
|
78 | reject(err);
|
79 | return;
|
80 | }
|
81 | me.tempPath = dirPath;
|
82 | resolve(dirPath);
|
83 | });
|
84 | });
|
85 | };
|
86 |
|
87 | Fetcher.prototype.afterLocalPath = function () {
|
88 | var me = this;
|
89 |
|
90 | return new Promise(function (resolve, reject) {
|
91 | mkdirp(me.localPath, function (err) {
|
92 | if (err) {
|
93 | reject(err);
|
94 | return;
|
95 | }
|
96 | resolve(me.localPath);
|
97 | });
|
98 | });
|
99 | };
|
100 |
|
101 | Fetcher.prototype.readFile = function (filePath) {
|
102 | return new Promise(function (resolve, reject) {
|
103 | fs.readFile(filePath, { encoding: 'utf8' }, function (err, contents) {
|
104 | if (err) {
|
105 | console.error(err);
|
106 | reject(err);
|
107 | return;
|
108 | }
|
109 | resolve(contents);
|
110 | });
|
111 | });
|
112 | };
|
113 |
|
114 | Fetcher.prototype.writeFile = function (filePath, contents) {
|
115 | return new Promise(function (resolve, reject) {
|
116 | fs.writeFile(filePath, contents, function (err) {
|
117 | if (err) {
|
118 | console.error(err);
|
119 | reject(err);
|
120 | return;
|
121 | }
|
122 | resolve();
|
123 | });
|
124 | });
|
125 | };
|
126 |
|
127 | Fetcher.prototype.generateAppCacheIndexShim = function () {
|
128 | var addPath = path.join(__dirname, 'lib', 'app-cache-index.js');
|
129 | var filePath = path.join(this.localPath, 'appCacheIndex.js');
|
130 | var options = {
|
131 | paths: [ this.localPath ],
|
132 | standalone: 'appCacheIndex'
|
133 | };
|
134 |
|
135 | return doBrowserify(addPath, filePath, options);
|
136 | };
|
137 |
|
138 | Fetcher.prototype.generateRequireShim = function () {
|
139 | var addPath = path.join(__dirname, 'lib', 'require.load.js');
|
140 | var filePath = path.join(this.localPath, 'require.load.js');
|
141 |
|
142 | return doBrowserify(addPath, filePath, {});
|
143 | };
|
144 |
|
145 | Fetcher.prototype.download = function (remoteUrl, localPath) {
|
146 | var me = this;
|
147 | var filePath;
|
148 | var filename;
|
149 |
|
150 | if (Array.isArray(remoteUrl)) {
|
151 | return Promise.all(remoteUrl.map(function (r) {
|
152 | return me.download(r, localPath);
|
153 | }));
|
154 | }
|
155 |
|
156 | console.log('download:', remoteUrl);
|
157 |
|
158 | filename = me.generateLocalFilePath(remoteUrl);
|
159 | filePath = path.join(localPath, filename);
|
160 |
|
161 | return new Promise(function (resolve, reject) {
|
162 | var reader, writer;
|
163 |
|
164 | reader = request(remoteUrl)
|
165 | .on('error', function (err) {
|
166 | console.error(err);
|
167 | reject(err);
|
168 | })
|
169 | .on('response', function (res) {
|
170 | var errorMsg;
|
171 | if (res.statusCode !== 200) {
|
172 | errorMsg = remoteUrl + ' : ' + res.statusCode;
|
173 | console.error(errorMsg);
|
174 | reject(new Error(errorMsg));
|
175 | return;
|
176 | }
|
177 | me.index.set(remoteUrl, filename);
|
178 | });
|
179 |
|
180 | writer = fs.createWriteStream(filePath)
|
181 | .on('error', function (err) {
|
182 | console.error(err);
|
183 | reject(err);
|
184 | })
|
185 | .on('finish', function () {
|
186 | resolve();
|
187 | });
|
188 |
|
189 | reader.pipe(writer);
|
190 | });
|
191 | };
|
192 |
|
193 | Fetcher.prototype.generateLocalFilePath = function (remoteUrl) {
|
194 | var parsed;
|
195 | var hash;
|
196 | if (remoteUrl === this.remoteUrl) {
|
197 | return 'index.html';
|
198 | }
|
199 | if (remoteUrl === this.manifestUrl) {
|
200 | return 'appcache.manifest';
|
201 | }
|
202 | parsed = url.parse(remoteUrl, true, true);
|
203 | hash = crypto.createHash('sha1');
|
204 | hash.update(parsed.pathname + parsed.search);
|
205 | return hash.digest('hex') + path.extname(parsed.pathname);
|
206 | };
|
207 |
|
208 | Fetcher.prototype.persistFilesIndex = function () {
|
209 | var content = JSON.stringify(this.index, null, 2);
|
210 | var filePath = path.join(this.localPath, 'index.json');
|
211 |
|
212 | return this.writeFile(filePath, content);
|
213 | };
|
214 |
|
215 | Fetcher.prototype.getManifestURL = function () {
|
216 | var me = this;
|
217 | var filePath = path.join(this.localPath, 'index.html');
|
218 | var extractors = this.extractors.manifestUrl;
|
219 |
|
220 | if (!Array.isArray(extractors) || !extractors.length) {
|
221 | return Promise.reject(new Error('no manifestUrl extractors'));
|
222 | }
|
223 |
|
224 | return me.readFile(filePath)
|
225 | .then(function (contents) {
|
226 | var manifestUrl;
|
227 | var e, eLength, extractor;
|
228 | eLength = extractors.length;
|
229 | for (e = 0; e < eLength; e++) {
|
230 | extractor = extractors[e];
|
231 | manifestUrl = extractor({
|
232 | contents: contents,
|
233 | remoteUrl: me.remoteUrl
|
234 | });
|
235 | if (manifestUrl) {
|
236 | return Promise.resolve(manifestUrl);
|
237 | }
|
238 | }
|
239 | return Promise.resolve('');
|
240 | });
|
241 | };
|
242 |
|
243 | Fetcher.prototype.downloadAppCacheEntries = function () {
|
244 | var me = this;
|
245 | var appCache;
|
246 | var remoteUrls;
|
247 |
|
248 | delete require.cache[path.join(me.localPath, 'appcache.json')];
|
249 | appCache = require(path.join(me.localPath, 'appcache.json'));
|
250 |
|
251 | remoteUrls = appCache.cache.map(function (entry) {
|
252 | return url.resolve(me.remoteUrl, entry.replace(/^\/\//, 'https://'));
|
253 | });
|
254 |
|
255 | return this.download(remoteUrls, me.localPath);
|
256 | };
|
257 |
|
258 | Fetcher.prototype.postProcessFile = function (filePath) {
|
259 | var me = this;
|
260 | var ext = path.extname(filePath).toLowerCase().replace('.', '');
|
261 | var transforms = me.transforms[ext];
|
262 | if (!Array.isArray(transforms) || !transforms.length) {
|
263 | return Promise.resolve();
|
264 | }
|
265 | console.log('postProcessFile:', filePath.replace(process.cwd(), ''));
|
266 | return this.readFile(filePath)
|
267 | .then(function (contents) {
|
268 | var transformedContents = contents;
|
269 | var t, tLength, transform;
|
270 | tLength = transforms.length;
|
271 | for (t = 0; t < tLength; t++) {
|
272 | transform = transforms[t];
|
273 | transformedContents = transform({
|
274 | contents: transformedContents,
|
275 | filePath: filePath,
|
276 | index: me.index
|
277 | });
|
278 | }
|
279 | return Promise.resolve(transformedContents);
|
280 | })
|
281 | .then(function (contents) {
|
282 | return me.writeFile(filePath, contents);
|
283 | });
|
284 | };
|
285 |
|
286 | Fetcher.prototype.postProcessDownloads = function () {
|
287 | var me = this;
|
288 |
|
289 | return new Promise(function (resolve, reject) {
|
290 | fs.readdir(me.localPath, function (err, files) {
|
291 | if (err) {
|
292 | console.error(err);
|
293 | reject(err);
|
294 | return;
|
295 | }
|
296 | Promise.all(files.map(function (file) {
|
297 | return me.postProcessFile(path.join(me.localPath, file));
|
298 | })).then(resolve, reject);
|
299 | });
|
300 | });
|
301 | };
|
302 |
|
303 | Fetcher.prototype.go = function () {
|
304 | var me = this;
|
305 |
|
306 | return Promise.all([
|
307 | this.afterTempPath(),
|
308 | this.afterLocalPath()
|
309 | ])
|
310 | .then(function () {
|
311 | return me.download(me.remoteUrl, me.localPath);
|
312 | })
|
313 | .then(function () {
|
314 | return me.getManifestURL();
|
315 | })
|
316 | .then(function (manifestUrl) {
|
317 | me.manifestUrl = manifestUrl;
|
318 | return me.download(me.manifestUrl, me.localPath);
|
319 | })
|
320 | .then(function () {
|
321 | return me.readFile(path.join(me.localPath, 'appcache.manifest'));
|
322 | })
|
323 | .then(function (contents) {
|
324 | var appCache = AppCache.parse(contents);
|
325 | return me.writeFile(
|
326 | path.join(me.localPath, 'appcache.json'),
|
327 | JSON.stringify(appCache, null, 2)
|
328 | );
|
329 | })
|
330 | .then(function () {
|
331 | return me.downloadAppCacheEntries();
|
332 | })
|
333 | .then(function () {
|
334 | return me.persistFilesIndex();
|
335 | })
|
336 | .then(function () {
|
337 | return me.postProcessDownloads();
|
338 | })
|
339 | .then(function () {
|
340 | return Promise.all([
|
341 | me.generateAppCacheIndexShim(),
|
342 | me.generateRequireShim()
|
343 | ]);
|
344 | })
|
345 | .then(null, function (err) {
|
346 | console.error(err);
|
347 | });
|
348 | };
|
349 |
|
350 | Fetcher.getURLVariationsOnQuery = urlVars.getURLVariationsOnQuery;
|
351 | Fetcher.getURLVariationsOnScheme = urlVars.getURLVariationsOnScheme;
|
352 | Fetcher.getURLVariations = urlVars.getURLVariations;
|
353 |
|
354 | module.exports = Fetcher;
|