UNPKG

16.9 kBJavaScriptView Raw
1var CRYPTO = require('crypto'),
2 FS = require('fs'),
3 PATH = require('path'),
4 configs,
5 config;
6
7var minimatch = require('minimatch');
8
9if (!PATH.sep) PATH.sep = process.platform === 'win32'? '\\' : '/';
10
11/**
12 * Path separator for RegExp
13 * @type {string}
14 */
15var rePathSep = PATH.sep == '\\' ? '\\\\' : PATH.sep;
16
17/**
18 * Content type for inlined resources.
19 * @constant
20 * @type {Object}
21 */
22const contentTypes = {
23 '.css': 'text/css',
24 '.cur': 'image/x-icon',
25 '.eot': 'application/vnd.ms-fontobject',
26 '.gif': 'image/gif',
27 '.ico': 'image/x-icon',
28 '.jpg': 'image/jpeg',
29 '.jpeg': 'image/jpeg',
30 '.js': 'application/javascript',
31 // '.json': 'application/json',
32 '.otf': 'font/opentype',
33 '.png': 'image/png',
34 '.svg': 'image/svg+xml',
35 '.swf': 'application/x-shockwave-flash',
36 '.ttf': 'application/x-font-ttf',
37 '.woff': 'application/x-font-woff',
38 '.woff2': 'application/font-woff2'
39};
40
41/**
42 * Clear config cache.
43 * This function is usefull for unit-tests.
44 */
45var clearConfigCache = exports.clearConfigCache = function() {
46 config = {
47 paths: {},
48 freezeNestingLevel: {},
49 freezeWildcards: {},
50 followSymlinks: {}
51 };
52
53 configs = {
54 paths: {}
55 };
56};
57
58clearConfigCache();
59
60/**
61 * Process image.
62 *
63 * @param {String} filePath Path to image file to process.
64 * @returns {String} New path of processed image.
65 */
66exports.processPath = function(filePath) {
67 return freeze(realpathSync(filePath));
68};
69
70/**
71 * Code content by SHA1 Base64 algorithm.
72 *
73 * @param {String} content Content to code.
74 * @returns {String} Coded content.
75 */
76var sha1Base64 = exports.sha1Base64 = function(content) {
77 var sha1 = CRYPTO.createHash('sha1');
78 sha1.update(content);
79 return sha1.digest('base64');
80};
81
82/**
83 * Fix Base64 string to accomplish Borschik needs.
84 *
85 * @param {String} base64 String to fix.
86 * @returns {String} Fixed string.
87 */
88var fixBase64 = exports.fixBase64 = function(base64) {
89 return base64
90 .replace(/\+/g, '-')
91 .replace(/\//g, '_')
92 .replace(/=/g, '')
93 .replace(/^[+-]+/g, '');
94};
95
96/**
97 * Freeze image.
98 *
99 * @param {String} filePath Path to the image to freeze.
100 * @param {String} content Optional content to use.
101 * @returns {String} Path to the frozen image.
102 */
103var freeze = exports.freeze = function(filePath, content) {
104 if (filePath !== realpathSync(filePath)) throw new Error();
105
106 var _freezeDir = freezeDir(filePath);
107
108 if (_freezeDir) {
109 if (content === undefined) {
110 if (!FS.existsSync(filePath)) throw new Error("No such file or directory: " + filePath);
111 if (FS.statSync(filePath).isDirectory()) throw new Error("Is a directory (file needed): " + filePath);
112
113 content = FS.readFileSync(filePath);
114 }
115
116 if (_freezeDir === ':base64:' || _freezeDir === ':encodeURIComponent:' || _freezeDir === ':encodeURI:') {
117 var fileExtension = PATH.extname(filePath);
118 var contentType = contentTypes[fileExtension];
119 if (!contentType) {
120 throw new Error('Freeze error. Unknown Content-type for ' + fileExtension);
121 }
122
123 var inlineDecl = 'data:' + contentType;
124
125 if (_freezeDir === ':base64:') {
126 // encode file as base64
127 // data:image/svg+xml;base64,....
128 return inlineDecl + ';base64,' + new Buffer(content).toString('base64');
129 } else {
130 // encode file as encoded url
131 // data:image/svg+xml,....
132 var encodeFunc = _freezeDir === ':encodeURI:' ? encodeURI : encodeURIComponent;
133 return inlineDecl + ',' + encodeFunc(new Buffer(content).toString('utf8'))
134 .replace(/%20/g, ' ')
135 .replace(/#/g, '%23');
136 }
137
138 } else {
139 // freeze as file
140 var hash = fixBase64(sha1Base64(content));
141
142 var nestingLevel = configFreezeNestingLevel(_freezeDir);
143 var nestingPath = getFreezeNestingPath(hash, nestingLevel);
144
145 filePath = PATH.join(_freezeDir, nestingPath + PATH.extname(filePath));
146
147 if (content && !FS.existsSync(filePath)) {
148 save(filePath, content);
149 }
150 }
151 }
152
153 return filePath;
154};
155
156/**
157 * Get freeze dir for specific file path.
158 *
159 * @param {String} filePath File path to use.
160 * @returns {String} Freeze dir.
161 */
162var freezeDir = exports.freezeDir = function(filePath) {
163 filePath = PATH.normalize(filePath);
164
165 if (filePath !== realpathSync(filePath)) {
166 throw new Error();
167 }
168
169 loadConfig(filePath);
170
171 for (var wildcard in config.freezeWildcards) {
172 if (minimatch(filePath, wildcard)) {
173 return config.freezeWildcards[wildcard];
174 }
175 }
176
177 return null;
178};
179
180/**
181 * Returns nesting path.
182 * @example
183 * getFreezeNestingPath('abc', 1) -> a/bc
184 * getFreezeNestingPath('abc', 2) -> a/b/c
185 * getFreezeNestingPath('abc', 5) -> a/b/c
186 *
187 * @param {String} hash Freezed filename.
188 * @param {Number} nestingLevel
189 * @returns {String}
190 */
191var getFreezeNestingPath = function(hash, nestingLevel) {
192 // reduce nestingLevel to hash size
193 nestingLevel = Math.min(hash.length - 1, nestingLevel);
194
195 if (nestingLevel === 0) {
196 return hash;
197 }
198
199 var hashArr = hash.split('');
200 for (var i = 0; i < nestingLevel; i++) {
201 hashArr.splice(i * 2 + 1, 0, '/');
202 }
203
204 return hashArr.join('');
205};
206
207/**
208 * Recursivly freeze all files in path.
209 * @param {String} input File or directory path
210 */
211exports.freezeAll = function(input) {
212 /**
213 * Result JSON
214 * @type {Object}
215 */
216 var result = {};
217
218 var basePath = PATH.dirname(input);
219 var stat = FS.statSync(input);
220 if (stat.isFile()) {
221 freezeAllProcessFile(input, process.cwd(), result);
222
223 } else if (stat.isDirectory()) {
224 freezeAllProcessDir(input, basePath, result);
225 }
226
227 return result;
228};
229
230/**
231 * Process file: freeze and write meta-data to result JSON
232 * @param absPath
233 * @param basePath
234 * @param {Object} result Result JSON
235 */
236function freezeAllProcessFile(absPath, basePath, result) {
237 var url = absPath;
238
239 if (freezableRe.test(url) || /\.(?:css|js|swf)$/.test(url)) {
240 url = freeze(url);
241 }
242
243 var relOriginalPath = PATH.relative(basePath, absPath);
244 var resolved = resolveUrl2(url);
245 url = (resolved == url ? PATH.relative(basePath, url) : resolved);
246
247 result[relOriginalPath] = url;
248}
249
250/**
251 * Read dir recursivly and process files
252 * @param dir
253 * @param basePath
254 * @param {Object} result Result JSON
255 */
256function freezeAllProcessDir(dir, basePath, result) {
257 FS.readdirSync(dir).forEach(function(file) {
258 file = PATH.resolve(dir, file);
259 var stat = FS.statSync(file);
260 if (stat.isFile()) {
261 freezeAllProcessFile(file, basePath, result);
262
263 } else if (stat.isDirectory()) {
264 freezeAllProcessDir(file, basePath, result);
265 }
266 });
267}
268
269/**
270 * Get path from "path" config by path if any.
271 *
272 * @param {String} _path Path to search config for.
273 * @returns {String} Path.
274 */
275var path = exports.path = function(_path) {
276 loadConfig(_path);
277 return config.paths[_path];
278};
279
280/**
281 * Returns freeze nesting level for given path.
282 * @param {String} path Path to search config for.
283 * @returns {Number}
284 */
285var configFreezeNestingLevel = function(path) {
286 return config.freezeNestingLevel[path] || 0;
287};
288
289/**
290 * Get path from "follow symlinks" config by path if any.
291 *
292 * @param {String} path Path to search config for.
293 * @returns {String} Path.
294 */
295var followSymlinks = exports.followSymlinks = function(path) {
296 loadConfig(path);
297 return config.followSymlinks[path];
298};
299
300/**
301 * Load config from path.
302 *
303 * @param {String} path Path to load from.
304 */
305var loadConfig = exports.loadConfig = function(path) {
306 if (configs.paths[path] !== undefined) return;
307
308 var config_path = PATH.join(path, '.borschik');
309
310 if (FS.existsSync(config_path)) {
311 configs.paths[path] = true;
312
313 try {
314 var _config = JSON.parse(FS.readFileSync(config_path));
315 } catch (e) {
316 if (e instanceof SyntaxError) {
317 console.error('Invalid config: ' + config_path);
318 }
319 throw e;
320 }
321
322 var paths = _config.paths || _config.pathmap || {};
323 for (var dir in paths) {
324 var realpath = realpathSync(PATH.resolve(path, dir));
325 if (!config.paths[realpath]) {
326 var value = paths[dir];
327 if (value) value = value.replace(/\*/g, PATH.sep);
328 config.paths[realpath] = value;
329 }
330 }
331
332 var freezeNestingLevels = _config['freeze_nesting_levels'] || {};
333 for (var dir in freezeNestingLevels) {
334 var realpath = realpathSync(PATH.resolve(path, dir));
335 setNestingLevel(config.freezeNestingLevel, realpath, freezeNestingLevels[dir]);
336 }
337
338 var freezePaths = _config.freeze_paths || {};
339 for (var freezeConfigWildcard in freezePaths) {
340 var freezeRealPath = realpathSync(PATH.resolve(path, freezeConfigWildcard));
341 if (!config.freezeWildcards[realpath]) {
342 var freezeToPath = freezePaths[freezeConfigWildcard];
343
344 // :base64: and :encodeURIComponent: are special syntax for images inlining
345 if (freezeToPath !== ':base64:' && freezeToPath !== ':encodeURIComponent:' && freezeToPath !== ':encodeURI:') {
346 freezeToPath = realpathSync(PATH.resolve(path, freezeToPath));
347
348 // freeze nesting level
349 // 0: all files freeze to given dir
350 // 1: all files freeze to
351 // freeze-dir/a/bcde.png
352 // freeze-dir/b/acde.png
353 // freeze-dir/c/abde.png
354 setNestingLevel(config.freezeNestingLevel, freezeToPath, _config['freeze_nesting_level']);
355 }
356 config.freezeWildcards[freezeRealPath] = freezeToPath;
357 }
358 }
359
360 var _followSymlinks = _config.follow_symlinks || {};
361 for (var dir in _followSymlinks) {
362 var realpath = realpathSync(PATH.resolve(path, dir));
363 if (!config.followSymlinks[realpath]) {
364 config.followSymlinks[realpath] = _followSymlinks[dir];
365 }
366 }
367 } else {
368 configs.paths[path] = false;
369 }
370
371};
372
373/**
374 * Resolve URL.
375 *
376 * @param {String} filePath File path to resolve.
377 * @param {String} base Base to use if any.
378 * @returns {String} Resolved URL.
379 */
380var resolveUrl2 = exports.resolveUrl2 = function(filePath, base) {
381 filePath = realpathSync(filePath);
382
383 var suffix = filePath,
384 prefix = '',
385 host = '',
386 hostpath = '',
387 rePrefix = new RegExp('^(' + rePathSep + '[^' + rePathSep + ']+)', 'g'),
388 matched;
389
390 while (matched = suffix.match(rePrefix)) {
391 prefix += matched[0];
392 hostpath += matched[0];
393
394 var _path = path(prefix);
395 if (_path !== undefined) {
396 host = _path;
397 hostpath = '';
398 }
399 suffix = suffix.replace(rePrefix, '');
400 }
401
402 var result;
403
404 if (host) {
405 hostpath = hostpath.replace(new RegExp('^' + rePathSep + '+'), '');
406 result = host + hostpath + suffix;
407 } else {
408 result = PATH.resolve(base, prefix + suffix);
409 }
410
411 return result;
412};
413
414/**
415 * Make dirs if not exists.
416 *
417 * @param {String} path Path to make.
418 */
419function mkpath(path) {
420 var dirs = path.split(PATH.sep),
421 _path = '',
422 winDisk = /^\w{1}:\\$/;
423
424 dirs.forEach(function(dir) {
425 dir = dir || PATH.sep;
426 if (dir) {
427 _path = PATH.join(_path, dir);
428 // fs.mkdirSync('/') raises EISDIR (invalid operation) exception on OSX 10.8.4/node 0.10
429 // fs.mkdirSync('D:\\') raises EPERM (operation not permitted) exception on Windows 7/node 0.10
430 // not tested in other configurations
431 if (_path === '/' || winDisk.test(PATH.resolve(dir))) {
432 return;
433 }
434 // @see https://github.com/veged/borschik/issues/90
435 try {
436 FS.mkdirSync(_path);
437 } catch(e) {
438 // ignore EEXIST error
439 if (e.code !== 'EEXIST') {
440 throw new Error(e)
441 }
442 }
443 }
444 });
445}
446
447/**
448 * Save file and make dirs if needed.
449 *
450 * @param {String} filePath File path to save.
451 * @param {String} content File content to save.
452 */
453function save(filePath, content) {
454 mkpath(PATH.dirname(filePath));
455 FS.writeFileSync(filePath, content);
456}
457
458/**
459 * Expands all symbolic links (if "follow symlinks" config "true") and resolves references.
460 *
461 * @param {String} path Path to make real.
462 * @param {String} base Base to use.
463 * @returns {String} Real file path.
464 */
465var realpathSync = exports.realpathSync = function(path, base) {
466 path = PATH.resolve(base? base : process.cwd(), path);
467
468 var folders = path.split(PATH.sep);
469
470 for (var i = 0; i < folders.length; ) {
471 var name = folders[i];
472
473 if (name === '') {
474 i++;
475 continue;
476 }
477
478 var prefix = subArrayJoin(folders, PATH.sep, 0, i - 1),
479 _followSymlinks = false;
480
481 for (var j = 0; j < i; j++) {
482 var followSymlinksJ = followSymlinks(subArrayJoin(folders, PATH.sep, 0, j));
483 if (followSymlinksJ !== undefined) _followSymlinks = followSymlinksJ;
484 }
485
486 var possibleSymlink = PATH.join(prefix, name);
487
488 if (_followSymlinks && isSymLink(possibleSymlink)) {
489 /*
490 For example, we process file /home/user/www/config/locales/ru.js
491 /home/user/www/config is symlink to configs/production
492
493 folders is [ 'home', 'user', 'www', 'config', 'locales', 'ru.js' ]
494 */
495
496 // Read symlink. This is relative path (configs/production)
497 var relativeLinkPath = FS.readlinkSync(possibleSymlink);
498
499 // Resolve symlink to absolute path (/home/user/www/configs/production)
500 var absoluteLinkPath = PATH.resolve(prefix, relativeLinkPath);
501
502 // Split absoulte path into parts
503 var linkParts = absoluteLinkPath.split(PATH.sep);
504
505 // now we should replace path /home/user/www/config to /home/user/www/configs/production
506 // and save suffix path locales/ru.js
507 folders = arraySplice(folders, 0, i + 1, linkParts);
508
509 // result is /home/user/www/configs/production/locales/ru.js
510 } else {
511 i++;
512 }
513 }
514
515 return folders.join(PATH.sep);
516};
517
518// Freezable test extensions
519var freezableExts = process.env.BORSCHIK_FREEZABLE_EXTS ? process.env.BORSCHIK_FREEZABLE_EXTS.split(' ') :
520 Object.keys(contentTypes).map(function(ext) {
521 return ext.slice(1);
522 }),
523 freezableRe = new RegExp('\\.(' + freezableExts.join('|') + ')$');
524
525/**
526 * Check if URL is freezable.
527 *
528 * @param {String} url URL to check.
529 * @returns {boolean} True if URL is freezable, otherwise false.
530 */
531exports.isFreezableUrl = function(url) {
532 return freezableRe.test(url);
533};
534
535/**
536 * Join part of array.
537 *
538 * @param {Array} a Array to join.
539 * @param {String} separator Separator to use in join.
540 * @param {Number} from Join start index.
541 * @param {Number} to Join finish index.
542 * @returns {String} Joined array string.
543 */
544function subArrayJoin(a, separator, from, to) {
545 return a.slice(from, to + 1).join(separator);
546}
547
548/**
549 * Splice array replacing it's elements by another array.
550 *
551 * @param {Array} a1 Array to splice.
552 * @param {Number} from Splice start index.
553 * @param {Number} to Splice finish index.
554 * @param {Array} a2 Array to inject.
555 * @returns {Array} New array.
556 */
557function arraySplice(a1, from, to, a2) {
558 var aL = a1.slice(0, from),
559 aR = a1.slice(to);
560
561 return aL.concat(a2).concat(aR);
562}
563
564/**
565 * Check if path is the path to symbolic link.
566 *
567 * @param {String} path Path to check.
568 * @returns {boolean} True if it is symbolic link, otherwise false.
569 */
570function isSymLink(path) {
571 return FS.existsSync(path) && FS.lstatSync(path).isSymbolicLink();
572}
573
574/**
575 * Parse and set nesting level in config object
576 * @param {object} config Nesting level config
577 * @param {string} dir Path to directory
578 * @param {string} rawValue Raw value from config (.borschik)
579 */
580function setNestingLevel(config, dir, rawValue) {
581 // config can have value "0", so we can't use "!config[dir]"
582 if ( !(dir in config) ) {
583 config[dir] = Math.max(parseInt(rawValue, 10) || 0, 0);
584 }
585}
586
587exports.getFreezeNestingPath = getFreezeNestingPath;