index.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. 'use strict';
  2. const pathFn = require('path');
  3. const Promise = require('bluebird');
  4. const File = require('./file');
  5. const util = require('hexo-util');
  6. const fs = require('hexo-fs');
  7. const chalk = require('chalk');
  8. const EventEmitter = require('events').EventEmitter;
  9. const minimatch = require('minimatch');
  10. const Pattern = util.Pattern;
  11. const join = pathFn.join;
  12. const sep = pathFn.sep;
  13. const defaultPattern = new Pattern(() => ({}));
  14. function Box(ctx, base, options) {
  15. EventEmitter.call(this);
  16. this.options = Object.assign({
  17. persistent: true
  18. }, options);
  19. if (!base.endsWith(sep)) {
  20. base += sep;
  21. }
  22. this.context = ctx;
  23. this.base = base;
  24. this.processors = [];
  25. this._processingFiles = {};
  26. this.watcher = null;
  27. this.Cache = ctx.model('Cache');
  28. this.File = this._createFileClass();
  29. this.ignore = ctx.config.ignore;
  30. if (!Array.isArray(this.ignore)) {
  31. this.ignore = [this.ignore];
  32. }
  33. }
  34. require('util').inherits(Box, EventEmitter);
  35. function escapeBackslash(path) {
  36. // Replace backslashes on Windows
  37. return path.replace(/\\/g, '/');
  38. }
  39. function getHash(path) {
  40. return new Promise((resolve, reject) => {
  41. const src = fs.createReadStream(path);
  42. const hasher = new util.HashStream();
  43. src.pipe(hasher)
  44. .on('finish', () => {
  45. resolve(hasher.read().toString('hex'));
  46. })
  47. .on('error', reject);
  48. });
  49. }
  50. Box.prototype._createFileClass = function() {
  51. const ctx = this.context;
  52. const _File = function(data) {
  53. File.call(this, data);
  54. };
  55. require('util').inherits(_File, File);
  56. _File.prototype.box = this;
  57. _File.prototype.render = function(options, callback) {
  58. if (!callback && typeof options === 'function') {
  59. callback = options;
  60. options = {};
  61. }
  62. return ctx.render.render({
  63. path: this.source
  64. }, options).asCallback(callback);
  65. };
  66. _File.prototype.renderSync = function(options) {
  67. return ctx.render.renderSync({
  68. path: this.source
  69. }, options);
  70. };
  71. return _File;
  72. };
  73. Box.prototype.addProcessor = function(pattern, fn) {
  74. if (!fn && typeof pattern === 'function') {
  75. fn = pattern;
  76. pattern = defaultPattern;
  77. }
  78. if (typeof fn !== 'function') throw new TypeError('fn must be a function');
  79. if (!(pattern instanceof Pattern)) pattern = new Pattern(pattern);
  80. this.processors.push({
  81. pattern,
  82. process: fn
  83. });
  84. };
  85. Box.prototype._readDir = function(base, fn, prefix = '') {
  86. const self = this;
  87. const ignore = self.ignore;
  88. if (base && ignore && ignore.length) {
  89. for (let i = 0, len = ignore.length; i < len; i++) {
  90. if (minimatch(base, ignore[i])) {
  91. return Promise.resolve('Ignoring dir.');
  92. }
  93. }
  94. }
  95. return fs.readdir(base).map(path => fs.stat(join(base, path)).then(stats => {
  96. if (stats.isDirectory()) {
  97. return self._readDir(join(base, path), fn, `${prefix + path}/`);
  98. }
  99. return self._checkFileStatus(prefix + path).then(file => fn(file).thenReturn(file));
  100. })).catch(err => {
  101. if (err.cause && err.cause.code === 'ENOENT') return;
  102. throw err;
  103. }).reduce((files, item) => files.concat(item), []);
  104. };
  105. Box.prototype._checkFileStatus = function(path) {
  106. const Cache = this.Cache;
  107. const src = join(this.base, path);
  108. const ctx = this.context;
  109. return Cache.compareFile(
  110. escapeBackslash(src.substring(ctx.base_dir.length)),
  111. () => getHash(src),
  112. () => fs.stat(src)
  113. ).then(result => ({
  114. type: result.type,
  115. path
  116. }));
  117. };
  118. Box.prototype.process = function(callback) {
  119. const self = this;
  120. const base = this.base;
  121. const Cache = this.Cache;
  122. const ctx = this.context;
  123. return fs.stat(base).then(stats => {
  124. if (!stats.isDirectory()) return;
  125. // Check existing files in cache
  126. const relativeBase = escapeBackslash(base.substring(ctx.base_dir.length));
  127. const cacheFiles = Cache.filter(item => item._id.startsWith(relativeBase)).map(item => item._id.substring(relativeBase.length));
  128. // Read files from directory
  129. return self._readDir(base, file => self._processFile(file.type, file.path)).map(file => file.path).then(files => // Handle deleted files
  130. Promise.filter(cacheFiles, path => !files.includes(path)).map(path => self._processFile(File.TYPE_DELETE, path)));
  131. }).catch(err => {
  132. if (err.cause && err.cause.code !== 'ENOENT') throw err;
  133. }).asCallback(callback);
  134. };
  135. Box.prototype.load = Box.prototype.process;
  136. Box.prototype._processFile = function(type, path) {
  137. if (this._processingFiles[path]) {
  138. return Promise.resolve();
  139. }
  140. this._processingFiles[path] = true;
  141. const File = this.File;
  142. const base = this.base;
  143. const ctx = this.context;
  144. const self = this;
  145. this.emit('processBefore', {
  146. type,
  147. path
  148. });
  149. return Promise.reduce(this.processors, (count, processor) => {
  150. const params = processor.pattern.match(path);
  151. if (!params) return count;
  152. const file = new File({
  153. source: join(base, path),
  154. path,
  155. params,
  156. type
  157. });
  158. return Promise.method(processor.process).call(ctx, file)
  159. .thenReturn(count + 1);
  160. }, 0).then(count => {
  161. if (count) {
  162. ctx.log.debug('Processed: %s', chalk.magenta(path));
  163. }
  164. self.emit('processAfter', {
  165. type,
  166. path
  167. });
  168. }).catch(err => {
  169. ctx.log.error({err}, 'Process failed: %s', chalk.magenta(path));
  170. }).finally(() => {
  171. self._processingFiles[path] = false;
  172. }).thenReturn(path);
  173. };
  174. Box.prototype.watch = function(callback) {
  175. if (this.isWatching()) {
  176. return Promise.reject(new Error('Watcher has already started.')).asCallback(callback);
  177. }
  178. const base = this.base;
  179. const self = this;
  180. function getPath(path) {
  181. return escapeBackslash(path.substring(base.length));
  182. }
  183. return this.process().then(() => fs.watch(base, self.options)).then(watcher => {
  184. self.watcher = watcher;
  185. watcher.on('add', path => {
  186. self._processFile(File.TYPE_CREATE, getPath(path));
  187. });
  188. watcher.on('change', path => {
  189. self._processFile(File.TYPE_UPDATE, getPath(path));
  190. });
  191. watcher.on('unlink', path => {
  192. self._processFile(File.TYPE_DELETE, getPath(path));
  193. });
  194. watcher.on('addDir', path => {
  195. let prefix = getPath(path);
  196. if (prefix) prefix += '/';
  197. self._readDir(path, file => self._processFile(file.type, file.path), prefix);
  198. });
  199. }).asCallback(callback);
  200. };
  201. Box.prototype.unwatch = function() {
  202. if (!this.isWatching()) return;
  203. this.watcher.close();
  204. this.watcher = null;
  205. };
  206. Box.prototype.isWatching = function() {
  207. return Boolean(this.watcher);
  208. };
  209. module.exports = Box;