123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- 'use strict';
- const { join, sep } = require('path');
- const Promise = require('bluebird');
- const File = require('./file');
- const { Pattern, createSha1Hash } = require('hexo-util');
- const { createReadStream, readdir, stat, watch } = require('hexo-fs');
- const { magenta } = require('chalk');
- const { EventEmitter } = require('events');
- const { isMatch, makeRe } = require('micromatch');
- const defaultPattern = new Pattern(() => ({}));
- class Box extends EventEmitter {
- constructor(ctx, base, options) {
- super();
- this.options = Object.assign({
- persistent: true,
- awaitWriteFinish: {
- stabilityThreshold: 200
- }
- }, options);
- if (!base.endsWith(sep)) {
- base += sep;
- }
- this.context = ctx;
- this.base = base;
- this.processors = [];
- this._processingFiles = {};
- this.watcher = null;
- this.Cache = ctx.model('Cache');
- this.File = this._createFileClass();
- let targets = this.options.ignored || [];
- if (ctx.config.ignore && ctx.config.ignore.length) {
- targets = targets.concat(ctx.config.ignore);
- }
- this.ignore = targets;
- this.options.ignored = targets.map(s => toRegExp(ctx, s)).filter(x => x);
- }
- _createFileClass() {
- const ctx = this.context;
- class _File extends File {
- render(options) {
- return ctx.render.render({
- path: this.source
- }, options);
- }
- renderSync(options) {
- return ctx.render.renderSync({
- path: this.source
- }, options);
- }
- }
- _File.prototype.box = this;
- return _File;
- }
- addProcessor(pattern, fn) {
- if (!fn && typeof pattern === 'function') {
- fn = pattern;
- pattern = defaultPattern;
- }
- if (typeof fn !== 'function') throw new TypeError('fn must be a function');
- if (!(pattern instanceof Pattern)) pattern = new Pattern(pattern);
- this.processors.push({
- pattern,
- process: fn
- });
- }
- _readDir(base, prefix = '') {
- const results = [];
- return readDirWalker(base, results, this.ignore, prefix)
- .return(results)
- .map(path => this._checkFileStatus(path))
- .map(file => this._processFile(file.type, file.path).return(file.path));
- }
- _checkFileStatus(path) {
- const { Cache, context: ctx } = this;
- const src = join(this.base, path);
- return Cache.compareFile(
- escapeBackslash(src.substring(ctx.base_dir.length)),
- () => getHash(src),
- () => stat(src)
- ).then(result => ({
- type: result.type,
- path
- }));
- }
- process(callback) {
- const { base, Cache, context: ctx } = this;
- return stat(base).then(stats => {
- if (!stats.isDirectory()) return;
- // Check existing files in cache
- const relativeBase = escapeBackslash(base.substring(ctx.base_dir.length));
- const cacheFiles = Cache.filter(item => item._id.startsWith(relativeBase)).map(item => item._id.substring(relativeBase.length));
- // Handle deleted files
- return this._readDir(base)
- .then(files => cacheFiles.filter(path => !files.includes(path)))
- .map(path => this._processFile(File.TYPE_DELETE, path));
- }).catch(err => {
- if (err && err.code !== 'ENOENT') throw err;
- }).asCallback(callback);
- }
- _processFile(type, path) {
- if (this._processingFiles[path]) {
- return Promise.resolve();
- }
- this._processingFiles[path] = true;
- const { base, File, context: ctx } = this;
- this.emit('processBefore', {
- type,
- path
- });
- return Promise.reduce(this.processors, (count, processor) => {
- const params = processor.pattern.match(path);
- if (!params) return count;
- const file = new File({
- source: join(base, path),
- path,
- params,
- type
- });
- return Reflect.apply(Promise.method(processor.process), ctx, [file])
- .thenReturn(count + 1);
- }, 0).then(count => {
- if (count) {
- ctx.log.debug('Processed: %s', magenta(path));
- }
- this.emit('processAfter', {
- type,
- path
- });
- }).catch(err => {
- ctx.log.error({err}, 'Process failed: %s', magenta(path));
- }).finally(() => {
- this._processingFiles[path] = false;
- }).thenReturn(path);
- }
- watch(callback) {
- if (this.isWatching()) {
- return Promise.reject(new Error('Watcher has already started.')).asCallback(callback);
- }
- const { base } = this;
- function getPath(path) {
- return escapeBackslash(path.substring(base.length));
- }
- return this.process().then(() => watch(base, this.options)).then(watcher => {
- this.watcher = watcher;
- watcher.on('add', path => {
- this._processFile(File.TYPE_CREATE, getPath(path));
- });
- watcher.on('change', path => {
- this._processFile(File.TYPE_UPDATE, getPath(path));
- });
- watcher.on('unlink', path => {
- this._processFile(File.TYPE_DELETE, getPath(path));
- });
- watcher.on('addDir', path => {
- let prefix = getPath(path);
- if (prefix) prefix += '/';
- this._readDir(path, prefix);
- });
- }).asCallback(callback);
- }
- unwatch() {
- if (!this.isWatching()) return;
- this.watcher.close();
- this.watcher = null;
- }
- isWatching() {
- return Boolean(this.watcher);
- }
- }
- function escapeBackslash(path) {
- // Replace backslashes on Windows
- return path.replace(/\\/g, '/');
- }
- function getHash(path) {
- const src = createReadStream(path);
- const hasher = createSha1Hash();
- const finishedPromise = new Promise((resolve, reject) => {
- src.once('error', reject);
- src.once('end', resolve);
- });
- src.on('data', chunk => { hasher.update(chunk); });
- return finishedPromise.then(() => hasher.digest('hex'));
- }
- function toRegExp(ctx, arg) {
- if (!arg) return null;
- if (typeof arg !== 'string') {
- ctx.log.warn('A value of "ignore:" section in "_config.yml" is not invalid (not a string)');
- return null;
- }
- const result = makeRe(arg);
- if (!result) {
- ctx.log.warn('A value of "ignore:" section in "_config.yml" can not be converted to RegExp:' + arg);
- return null;
- }
- return result;
- }
- function isIgnoreMatch(path, ignore) {
- return path && ignore && ignore.length && isMatch(path, ignore);
- }
- function readDirWalker(base, results, ignore, prefix) {
- if (isIgnoreMatch(base, ignore)) return Promise.resolve();
- return Promise.map(readdir(base).catch(err => {
- if (err && err.code === 'ENOENT') return [];
- throw err;
- }), async path => {
- const fullpath = join(base, path);
- const stats = await stat(fullpath);
- const prefixdPath = `${prefix}${path}`;
- if (stats.isDirectory()) {
- return readDirWalker(fullpath, results, ignore, `${prefixdPath}/`);
- }
- if (!isIgnoreMatch(fullpath, ignore)) {
- results.push(prefixdPath);
- }
- });
- }
- module.exports = Box;
|