You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
268 lines
6.7 KiB
268 lines
6.7 KiB
2 years ago
|
'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('picocolors');
|
||
|
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;
|