util.js

'use strict';

var minimatch = require('minimatch');
var lodash = require('lodash');
var fs = require('fs'),
  pth = require('path'),
  crypto = require('crypto'),
  Url = require('url'),
  _exists = fs.existsSync || pth.existsSync,
  toString = Object.prototype.toString,
  iconv, tar;

var IS_WIN = process.platform.indexOf('win') === 0;

/*
 * 后缀类型HASH
 * @type {Array}
 */
var TEXT_FILE_EXTS = [
    'css', 'tpl', 'js', 'php',
    'txt', 'json', 'xml', 'htm',
    'text', 'xhtml', 'html', 'md',
    'conf', 'po', 'config', 'tmpl',
    'coffee', 'less', 'sass', 'jsp',
    'scss', 'pcss', 'manifest', 'bak', 'asp',
    'tmp', 'haml', 'jade', 'aspx',
    'ashx', 'java', 'py', 'c', 'cpp',
    'h', 'cshtml', 'asax', 'master',
    'ascx', 'cs', 'ftl', 'vm', 'ejs',
    'styl', 'jsx', 'handlebars', 'shtml',
    'ts', 'tsx', 'yml', 'sh', 'es', 'es6', 'es7',
    'map'
  ],
  IMAGE_FILE_EXTS = [
    'svg', 'tif', 'tiff', 'wbmp',
    'png', 'bmp', 'fax', 'gif',
    'ico', 'jfif', 'jpe', 'jpeg',
    'jpg', 'woff', 'cur', 'webp',
    'swf', 'ttf', 'eot', 'woff2'
  ],
  MIME_MAP = {
    //text
    'css': 'text/css',
    'tpl': 'text/html',
    'js': 'text/javascript',
    'jsx': 'text/javascript',
    'ts': 'text/javascript',
    'tsx': 'text/javascript',
    'es': 'text/javascript',
    'es6': 'text/javascript',
    'es7': 'text/javascript',
    'php': 'text/html',
    'asp': 'text/html',
    'jsp': 'text/jsp',
    'txt': 'text/plain',
    'json': 'application/json',
    'xml': 'text/xml',
    'htm': 'text/html',
    'text': 'text/plain',
    'md': 'text/plain',
    'xhtml': 'text/html',
    'html': 'text/html',
    'conf': 'text/plain',
    'po': 'text/plain',
    'config': 'text/plain',
    'coffee': 'text/javascript',
    'less': 'text/css',
    'sass': 'text/css',
    'scss': 'text/css',
    'styl': 'text/css',
    'pcss': 'text/css',
    'manifest': 'text/cache-manifest',
    //image
    'svg': 'image/svg+xml',
    'tif': 'image/tiff',
    'tiff': 'image/tiff',
    'wbmp': 'image/vnd.wap.wbmp',
    'webp': 'image/webp',
    'png': 'image/png',
    'bmp': 'image/bmp',
    'fax': 'image/fax',
    'gif': 'image/gif',
    'ico': 'image/x-icon',
    'jfif': 'image/jpeg',
    'jpg': 'image/jpeg',
    'jpe': 'image/jpeg',
    'jpeg': 'image/jpeg',
    'eot': 'application/vnd.ms-fontobject',
    'woff': 'application/font-woff',
    'ttf': 'application/octet-stream',
    'cur': 'application/octet-stream'
  };

function getIconv() {
  if (!iconv) {
    iconv = require('iconv-lite');
  }
  return iconv;
}

function getTar() {
  if (!tar) {
    tar = require('tar');
  }
  return tar;
}

/**
 * fis 中工具类操作集合。{@link https://lodash.com/ lodash} 中所有方法都挂载在此名字空间下面。
 * @param  {String} path
 * @return {String}
 * @example
 *   /a/b//c\d/ -> /a/b/c/d
 * @namespace fis.util
 */
var _ = module.exports = function(path) {
  var type = typeof path;
  if (arguments.length > 1) {
    path = Array.prototype.join.call(arguments, '/');
  } else if (type === 'string') {
    //do nothing for quickly determining.
  } else if (type === 'object') {
    path = Array.prototype.join.call(path, '/');
  } else if (type === 'undefined') {
    path = '';
  }
  if (path) {
    path = pth.normalize(path.replace(/[\/\\]+/g, '/')).replace(/\\/g, '/');
    if (path !== '/') {
      path = path.replace(/\/$/, '');
    }
  }
  return path;
};

// 将lodash内部方法的引用挂载到utils上,方便使用
lodash.assign(_, lodash);

_.is = function(source, type) {
  return toString.call(source) === '[object ' + type + ']';
};

/**
 * 对象枚举元素遍历,若merge为true则进行_.assign(obj, callback),若为false则回调元素的key value index
 * @param  {Object}   obj      源对象
 * @param  {Function|Object} callback 回调函数|目标对象
 * @param  {Boolean}   merge    是否为对象赋值模式
 * @memberOf fis.util
 * @name map
 * @function
 */
_.map = function(obj, callback, merge) {
  var index = 0;
  for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
      if (merge) {
        callback[key] = obj[key];
      } else if (callback(key, obj[key], index++)) {
        break;
      }
    }
  }
};

/**
 * 固定长度字符前后缀填补方法(fillZero)
 * @param  {String} str  初始字符串
 * @param  {Number} len  固定长度
 * @param  {String} fill 填补的缀
 * @param  {Boolean} pre  前缀还是后缀
 * @return {String}      填补后的字符串
 * @memberOf fis.util
 * @name pad
 * @function
 */
_.pad = function(str, len, fill, pre) {
  if (str.length < len) {
    fill = (new Array(len)).join(fill || ' ');
    if (pre) {
      str = (fill + str).substr(-len);
    } else {
      str = (str + fill).substring(0, len);
    }
  }
  return str;
};

/**
 * 将target合并到source上,新值为undefiend一样会覆盖掉原有数据
 * @param  {Object} source 源对象
 * @param  {Object} target 目标对象
 * @return {Object}        合并后的对象
 * @memberOf fis.util
 * @name merge
 * @function
 */
_.merge = function(source, target) {
  if (_.is(source, 'Object') && _.is(target, 'Object')) {
    _.map(target, function(key, value) {
      source[key] = _.merge(source[key], value);
    });
  } else {
    source = target;
  }
  return source;
};

/**
 * clone一个变量
 * @param  {any} source 变量
 * @return {any}     clone值
 * @memberOf fis.util
 * @name clone
 * @function
 */
/*_.clone = function(source) {
  var ret;
  switch (toString.call(source)) {
    case '[object Object]':
      ret = {};
      _.map(source, function(k, v) {
        ret[k] = _.clone(v);
      });
      break;
    case '[object Array]':
      ret = [];
      source.forEach(function(ele) {
        ret.push(_.clone(ele));
      });
      break;
    default:
      ret = source;
  }
  return ret;
};*/

/**
 * 正则串编码转义
 * @param  {String} str 正则串
 * @return {String}     普通字符串
 * @memberOf fis.util
 * @name escapeReg
 * @function
 */
_.escapeReg = function(str) {
  return str.replace(/[\.\\\+\*\?\[\^\]\$\(\){}=!<>\|:\/]/g, '\\$&');
};

/**
 * shell命令编码转义
 * @param  {String}  命令
 * @memberOf fis.util
 * @name escapeShellCmd
 * @function
 */
_.escapeShellCmd = function(str) {
  return str.replace(/ /g, '"$&"');
};

/**
 * shell编码转义
 * @param  {String} 命令
 * @memberOf fis.util
 * @name escapeShellArg
 * @function
 */
_.escapeShellArg = function(cmd) {
  return '"' + cmd + '"';
};

/**
 * 提取字符串中的引号和一对引号包围的内容
 * @param  {String} str    待处理字符串
 * @param  {String} quotes 初始引号可选范围,缺省为[',"]
 * @return {Object}        {
 *                           origin: 源字符串
 *                           rest: 引号包围的文字内容
 *                           quote: 引号类型
 *                         }
 * @memberOf fis.util
 * @name stringQuote
 * @function
 */
_.stringQuote = function(str, quotes) {
  var info = {
    origin: str,
    rest: str = str.trim(),
    quote: ''
  };
  if (str) {
    quotes = quotes || '\'"';
    var strLen = str.length - 1;
    for (var i = 0, len = quotes.length; i < len; i++) {
      var c = quotes[i];
      if (str[0] === c && str[strLen] === c) {
        info.quote = c;
        info.rest = str.substring(1, strLen);
        break;
      }
    }
  }
  return info;
};

/**
 * 匹配文件后缀所属MimeType类型
 * @param  {String} ext 文件后缀
 * @return {String}     MimeType类型
 * @memberOf fis.util
 * @name getMimeType
 * @function
 */
_.getMimeType = function(ext) {
  if (ext[0] === '.') {
    ext = ext.substring(1);
  }
  return MIME_MAP[ext] || 'application/x-' + ext;
};

/**
 * 判断文件是否存在。
 * @param {String} filepath 文件路径。
 * @memberOf fis.util
 * @name exist
 * @function
 */
_.exists = _exists;
_.fs = fs;

/**
 * 返回path的绝对路径,若path不存在则返回false
 * @param  {String} path 路径
 * @return {String}      绝对路径
 * @memberOf fis.util
 * @name realpath
 * @function
 */
_.realpath = function(path) {
  if (path && _exists(path)) {
    path = fs.realpathSync(path);
    if (IS_WIN) {
      path = path.replace(/\\/g, '/');
    }
    if (path !== '/') {
      path = path.replace(/\/$/, '');
    }
    return path;
  } else {
    return false;
  }
};

/**
 * 多功能path处理
 * @param  {String} path 路径
 * @return {String}      处理后的路径
 * @memberOf fis.util
 * @name realpathSafe
 * @function
 */
_.realpathSafe = function(path) {
  return _.realpath(path) || _(path);
};

/**
 * 判断路径是否为绝对路径
 * @param  {String}  path 路径
 * @return {Boolean}      true为是
 * @memberOf fis.util
 * @name isAbsolute
 * @function
 */
// _.isAbsolute = function(path) {
//   if (!IS_WIN && path && path[0] === '~') {
//     return true;
//   }

//   return pth.isAbsolute ? pth.isAbsolute(path) : pth.resolve(path) === pth.normalize(path);
// };
_.isAbsolute = function(path) {
  if (IS_WIN) {
    return /^[a-z]:/i.test(path);
  } else {
    if (path === '/') {
      return true;
    } else {
      var split = path.split('/');
      if (split[0] === '~') {
        return true;
      } else if (split[0] === '' && split[1]) {
        return _.isDir('/' + split[1] + '/' + split[2]);
      } else {
        return false;
      }
    }
  }
};

/**
 * 是否为一个文件
 * @param  {String}  path 路径
 * @return {Boolean}      true为是
 * @memberOf fis.util
 * @name isFile
 * @function
 */
_.isFile = function(path) {
  return _exists(path) && fs.statSync(path).isFile();
};

/**
 * 是否为文件夹
 * @param  {String}  path 路径
 * @return {Boolean}      true为是
 * @memberOf fis.util
 * @name isDir
 * @function
 */
_.isDir = function(path) {
  return _exists(path) && fs.statSync(path).isDirectory();
};

/**
 * 获取路径最近修改时间
 * @param  {String} path 路径
 * @return {Date}      时间(GMT+0800)
 * @memberOf fis.util
 * @name mtime
 * @function
 */
_.mtime = function(path) {
  var time = 0;
  if (_exists(path)) {
    time = fs.statSync(path).mtime;
  }
  return time;
};

/**
 * 修改文件时间戳
 * @param  {String} path  路径
 * @param  {Date|Number} mtime 时间戳
 * @memberOf fis.util
 * @name touch
 * @function
 */
_.touch = function(path, mtime) {
  if (!_exists(path)) {
    _.write(path, '');
  }
  if (mtime instanceof Date) {
    //do nothing for quickly determining.
  } else if (typeof mtime === 'number') {
    var time = new Date();
    time.setTime(mtime);
    mtime = time;
  } else {
    fis.log.error('invalid argument [mtime]');
  }
  fs.utimesSync(path, mtime, mtime);
};

/**
 * 是否为windows系统
 * @return {Boolean}
 * @memberOf fis.util
 * @name isWin
 * @function
 */
_.isWin = function() {
  return IS_WIN;
};

/*
 * 生成对应文件类型判断的正则
 * @param  {String} type 文件类型
 * @return {RegExp}      对应判断的正则表达式
 */
function getFileTypeReg(type) {
  var map = [],
    ext = fis.config.get('project.fileType.' + type);
  if (type === 'text') {
    map = TEXT_FILE_EXTS;
  } else if (type === 'image') {
    map = IMAGE_FILE_EXTS;
  } else {
    fis.log.error('invalid file type [%s]', type);
  }
  if (ext && ext.length) {
    if (typeof ext === 'string') {
      ext = ext.split(/\s*,\s*/);
    }
    map = map.concat(ext);
  }
  map = map.join('|');
  return new RegExp('\\.(?:' + map + ')$', 'i');
}

/**
 * 是否为配置中的text文件类型
 * @param  {String}  path 路径
 * @return {Boolean}
 * @memberOf fis.util
 * @name isTextFile
 * @function
 */
_.isTextFile = function(path) {
  return getFileTypeReg('text').test(path || '');
};

/**
 * 是否为配置中的image文件类型
 * @param  {String}  path 路径
 * @return {Boolean}
 * @memberOf fis.util
 * @name isImageFile
 * @function
 */
_.isImageFile = function(path) {
  return getFileTypeReg('image').test(path || '');
};

/**
 * 按位数生成md5串
 * @param  {String|Buffer} data 数据源
 * @param  {Number} len  长度
 * @return {String}      md5串
 * @memberOf fis.util
 * @name md5
 * @function
 */
_.md5 = function(data, len) {
  var md5sum = crypto.createHash('md5'),
    encoding = typeof data === 'string' ? 'utf8' : 'binary';
  md5sum.update(data, encoding);
  len = len || fis.config.get('project.md5Length', 7);
  return md5sum.digest('hex').substring(0, len);
};

/**
 * 生成base64串
 * @param  {String|Buffer|Array} data 数据源
 * @return {String}      base64串
 * @memberOf fis.util
 * @name base64
 * @function
 */
_.base64 = function(data) {
  if (data instanceof Buffer) {
    //do nothing for quickly determining.
  } else if (data instanceof Array) {
    data = new Buffer(data);
  } else {
    //convert to string.
    data = new Buffer(String(data || ''));
  }
  return data.toString('base64');
};

/**
 * 递归创建文件夹
 * @param  {String} path 路径
 * @param  {Number} mode 创建模式
 * @memberOf fis.util
 * @name mkdir
 * @function
 */
_.mkdir = function(path, mode) {
  if (typeof mode === 'undefined') {
    //511 === 0777
    mode = 511 & (~process.umask());
  }
  if (_exists(path)) return;
  path.split('/').reduce(function(prev, next) {
    if (prev && !_exists(prev)) {
      fs.mkdirSync(prev, mode);
    }
    return prev + '/' + next;
  });
  if (!_exists(path)) {
    fs.mkdirSync(path, mode);
  }
};

/**
 * 字符串编码转换
 * @param  {String|Number|Array|Buffer} str      待处理的字符串
 * @param  {String} encoding 编码格式
 * @return {String}          编码转换后的字符串
 * @memberOf fis.util
 * @name toEncoding
 * @function
 */
_.toEncoding = function(str, encoding) {
  return getIconv().toEncoding(String(str), encoding);
};

/**
 * 判断Buffer是否为utf8
 * @param  {Buffer}  bytes 待检数据
 * @return {Boolean}       true为utf8
 * @memberOf fis.util
 * @name isUtf8
 * @function
 */
_.isUtf8 = function(bytes) {
  var i = 0;
  while (i < bytes.length) {
    if (( // ASCII
        0x00 <= bytes[i] && bytes[i] <= 0x7F
      )) {
      i += 1;
      continue;
    }

    if (( // non-overlong 2-byte
        (0xC2 <= bytes[i] && bytes[i] <= 0xDF) &&
        (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF)
      )) {
      i += 2;
      continue;
    }

    if (
      ( // excluding overlongs
        bytes[i] == 0xE0 &&
        (0xA0 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
        (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF)
      ) || ( // straight 3-byte
        ((0xE1 <= bytes[i] && bytes[i] <= 0xEC) ||
          bytes[i] == 0xEE ||
          bytes[i] == 0xEF) &&
        (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
        (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF)
      ) || ( // excluding surrogates
        bytes[i] == 0xED &&
        (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0x9F) &&
        (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF)
      )
    ) {
      i += 3;
      continue;
    }

    if (
      ( // planes 1-3
        bytes[i] == 0xF0 &&
        (0x90 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
        (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) &&
        (0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF)
      ) || ( // planes 4-15
        (0xF1 <= bytes[i] && bytes[i] <= 0xF3) &&
        (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) &&
        (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) &&
        (0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF)
      ) || ( // plane 16
        bytes[i] == 0xF4 &&
        (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0x8F) &&
        (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) &&
        (0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF)
      )
    ) {
      i += 4;
      continue;
    }
    return false;
  }
  return true;
};

/**
 * 处理Buffer编码方式
 * @param  {Buffer} buffer 待读取的Buffer
 * @return {String}        判断若为utf8可识别的编码则去掉bom返回utf8编码后的String,若不为utf8可识别编码则返回gbk编码后的String
 * @memberOf fis.util
 * @name readBuffer
 * @function
 */
_.readBuffer = function(buffer) {
  if (_.isUtf8(buffer)) {
    buffer = buffer.toString('utf8');
    if (buffer.charCodeAt(0) === 0xFEFF) {
      buffer = buffer.substring(1);
    }
  } else {
    buffer = getIconv().decode(buffer, 'gbk');
  }
  return buffer;
};

/**
 * 读取文件内容
 * @param  {String} path    路径
 * @param  {Boolean} convert 是否使用text方式转换文件内容的编码 @see readBuffer
 * @return {String}         文件内容
 * @memberOf fis.util
 * @name read
 * @function
 */
_.read = function(path, convert) {
  var content = false;
  if (_exists(path)) {
    content = fs.readFileSync(path);
    if (convert || _.isTextFile(path)) {
      content = _.readBuffer(content);
    }
  } else {
    fis.log.error('unable to read file[%s]: No such file or directory.', path);
  }
  return content;
};

/**
 * 写文件,若路径不存在则创建
 * @param  {String} path    路径
 * @param  {String} data    写入内容
 * @param  {String} charset 编码方式
 * @param  {Boolean} append  是否为追加模式
 * @memberOf fis.util
 * @name write
 * @function
 */
_.write = function(path, data, charset, append) {
  if (!_exists(path)) {
    _.mkdir(_.pathinfo(path).dirname);
  }
  if (charset) {
    data = getIconv().encode(data, charset);
  }
  if (append) {
    fs.appendFileSync(path, data, null);
  } else {
    fs.writeFileSync(path, data, null);
  }
};

/**
 * str过滤处理,判断include中匹配str为true,exclude中不匹配str为true
 * @param  {String} str     待处理的字符串
 * @param  {Array} include include匹配规则
 * @param  {Array} exclude exclude匹配规则
 * @return {Boolean}         是否匹配
 * @memberOf fis.util
 * @name filter
 * @function
 */
_.filter = function(str, include, exclude) {
// pattern处理,若不为正则则调用glob处理生成正则
  function normalize(pattern) {
    var type = toString.call(pattern);
    switch (type) {
      case '[object String]':
        return _.glob(pattern);
      case '[object RegExp]':
        return pattern;
      default:
        fis.log.error('invalid regexp [%s].', pattern);
    }
  }
// 判断str是否符合patterns中的匹配规则
  function match(str, patterns) {
    var matched = false;
    if (!_.is(patterns, 'Array')) {
      patterns = [patterns];
    }
    patterns.every(function(pattern) {
      if (!pattern) {
        return true;
      }
      matched = matched || str.search(normalize(pattern)) > -1;
      return !matched;
    });
    return matched;
  }

  var isInclude, isExclude;

  if (include) {
    isInclude = match(str, include);
  } else {
    isInclude = true;
  }

  if (exclude) {
    isExclude = match(str, exclude);
  }

  return isInclude && !isExclude;
};

/**
 * 若rPath为文件夹,夹遍历目录下符合include和exclude规则的全部文件;若rPath为文件,直接匹配该文件路径是否符合include和exclude规则
 * @param  {String} rPath   要查找的目录
 * @param  {Array} include 包含匹配正则集合,可传null
 * @param  {Array} exclude 排除匹配正则集合,可传null
 * @param  {String} root    根目录
 * @return {Array}         符合规则的文件路径的集合
 * @memberOf fis.util
 * @name find
 * @function
 */
_.find = function(rPath, include, exclude, root) {
  var list = [],
    path = _.realpath(rPath),
    filterPath = root ? path.substring(root.length) : path;
  if (path) {
    var stat = fs.statSync(path);
    if (stat.isDirectory() && (include || _.filter(filterPath, include, exclude))) {
      fs.readdirSync(path).forEach(function(p) {
        if (p[0] != '.') {
          list = list.concat(_.find(path + '/' + p, include, exclude, root));
        }
      });
    } else if (stat.isFile() && _.filter(filterPath, include, exclude)) {
      list.push(path);
    }
  } else {
    fis.log.error('unable to find [%s]: No such file or directory.', rPath);
  }
  return list.sort();
};

/**
 * 删除指定目录下面的文件。
 * @memberOf fis.util
 * @name del
 * @function
 */
_.del = function(rPath, include, exclude) {
  var removedAll = true;
  var path;
  if (rPath && _.exists(rPath)) {
    var stat = fs.lstatSync(rPath);
    var isFile = stat.isFile() || stat.isSymbolicLink();

    if (stat.isSymbolicLink()) {
      path = rPath;
    } else {
      path = _.realpath(rPath);
    }

    if (/^(?:\w:)?\/$/.test(path)) {
      fis.log.error('unable to delete directory [%s].', rPath);
    }

    if (stat.isDirectory()) {
      fs.readdirSync(path).forEach(function(name) {
        if (name != '.' && name != '..') {
          removedAll = _.del(path + '/' + name, include, exclude) && removedAll;
        }
      });
      if (removedAll) {
        fs.rmdirSync(path);
      }
    } else if (isFile && _.filter(path, include, exclude)) {
      fs.unlinkSync(path);
    } else {
      removedAll = false;
    }
  } else {
    //fis.log.error('unable to delete [' + rPath + ']: No such file or directory.');
  }
  return removedAll;
};

/**
 * 复制符合include和exclude规则的文件到目标目录,若rSource为文件夹则递归其下属每个文件
 * @param  {String} rSource 源路径
 * @param  {String} target  目标路径
 * @param  {Array} include 包含匹配规则
 * @param  {Array} exclude 排除匹配规则
 * @param  {Boolean} uncover 是否覆盖
 * @param  {Boolean} move    是否移动
 * @memberOf fis.util
 * @name copy
 * @function
 */
_.copy = function(rSource, target, include, exclude, uncover, move) {
  var removedAll = true,
    source = _.realpath(rSource);
  target = _(target);
  if (source) {
    var stat = fs.statSync(source);
    if (stat.isDirectory()) {
      fs.readdirSync(source).forEach(function(name) {
        if (name != '.' && name != '..') {
          removedAll = _.copy(
            source + '/' + name,
            target + '/' + name,
            include, exclude,
            uncover, move
          ) && removedAll;
        }
      });
      if (move && removedAll) {
        fs.rmdirSync(source);
      }
    } else if (stat.isFile() && _.filter(source, include, exclude)) {
      if (uncover && _exists(target)) {
        //uncover
        removedAll = false;
      } else {
        _.write(target, fs.readFileSync(source, null));
        if (move) {
          fs.unlinkSync(source);
        }
      }
    } else {
      removedAll = false;
    }
  } else {
    fis.log.error('unable to copy [%s]: No such file or directory.', rSource);
  }
  return removedAll;
};

/**
 * path处理
 * @param  {String} str 待处理的路径
 * @return {Object}
 * @example
 * str = /a.b.c/f.php?kw=%B2%E5%BB%AD#addfhubqwek
 *                      {
 *                         origin: '/a.b.c/f.php?kw=%B2%E5%BB%AD#addfhubqwek',
 *                         rest: '/a.b.c/f',
 *                         hash: '#addfhubqwek',
 *                         query: '?kw=%B2%E5%BB%AD',
 *                         fullname: '/a.b.c/f.php',
 *                         dirname: '/a.b.c',
 *                         ext: '.php',
 *                         filename: 'f',
 *                         basename: 'f.php'
 *                      }
 * @memberOf fis.util
 * @name ext
 * @function
 */
_.ext = function(str) {
  var info = _.query(str),
    pos;
  str = info.fullname = info.rest;
  if ((pos = str.lastIndexOf('/')) > -1) {
    if (pos === 0) {
      info.rest = info.dirname = '/';
    } else {
      info.dirname = str.substring(0, pos);
      info.rest = info.dirname + '/';
    }
    str = str.substring(pos + 1);
  } else {
    info.rest = info.dirname = '';
  }
  if ((pos = str.lastIndexOf('.')) > -1) {
    info.ext = str.substring(pos).toLowerCase();
    info.filename = str.substring(0, pos);
    info.basename = info.filename + info.ext;
  } else {
    info.basename = info.filename = str;
    info.ext = '';
  }
  info.rest += info.filename;
  return info;
};

/**
 * path处理,提取path中rest部分(?之前)、query部分(?#之间)、hash部分(#之后)
 * @param  {String} str 待处理的url
 * @return {Object}     {
 *                         origin: 原始串
 *                         rest: path部分(?之前)
 *                         query: query部分(?#之间)
 *                         hash: hash部分(#之后)
 *                      }
 * @memberOf fis.util
 * @name query
 * @function
 */
_.query = function(str) {
  var rest = str,
    pos = rest.indexOf('#'),
    hash = '',
    query = '';
  if (pos > -1) {
    hash = rest.substring(pos);
    rest = rest.substring(0, pos);
  }
  pos = rest.indexOf('?');
  if (pos > -1) {
    query = rest.substring(pos);
    rest = rest.substring(0, pos);
  }
  rest = rest.replace(/\\/g, '/');
  if (rest !== '/') {
    // 排除由于.造成路径查找时因filename为""而产生bug,以及隐藏文件问题
    rest = rest.replace(/\/\.?$/, '');
  }
  return {
    origin: str,
    rest: rest,
    hash: hash,
    query: query
  };
};

/**
 * 生成路径信息
 * @param  {String|Array} path 路径,可使用多参数传递:pathinfo('a', 'b', 'c')
 * @return {Object}      @see ext()
 * @memberOf fis.util
 * @name pathinfo
 * @function
 */
_.pathinfo = function(path) {
  //can not use _() method directly for the case _.pathinfo('a/').
  var type = typeof path;
  if (arguments.length > 1) {
    path = Array.prototype.join.call(arguments, '/');
  } else if (type === 'string') {
    //do nothing for quickly determining.
  } else if (type === 'object') {
    path = Array.prototype.join.call(path, '/');
  }
  return _.ext(path);
};

/**
 * 驼峰写法转换
 * @param  {String} str 待转换字符串
 * @return {String}     转换后的字符串
 * @memberOf fis.util
 * @name camelcase
 * @function
 */
_.camelcase = function(str) {
  var ret = '';
  if (str) {
    str.split(/[-_ ]+/).forEach(function(ele) {
      ret += ele[0].toUpperCase() + ele.substring(1);
    });
  } else {
    ret = str;
  }
  return ret;
};

/**
 * 加载处理fis模块下的全部插件,如fis3-plugin-*
 * @param  {String}   type     模块名
 * @param  {Function} callback 回调
 * @param  {Object}   def      模块获取的默认值 @see fis.config.get def
 * @memberOf fis.util
 * @name pipe
 * @function
 */
_.pipe = function(type, callback, def) {
  var processors = fis.media().get('modules.' + type, def);
  if (processors) {
// 兼容处理[]、String、'String1, String2, ...'的配置写法
    if ('string' === typeof processors) {
      processors = processors.trim().split(/\s*,\s*/);
    } else if (!Array.isArray(processors)) {
      processors = [processors];
    }

    // 过滤掉同名的插件, 没必要重复操作。
    processors = processors.filter(function(item, idx, arr) {
      item = item.__name || item;

      return idx === _.findIndex(arr, function(target) {
        target = target.__name || target;

        return target === item;
      });
    });

// 若type为多层级(ex: 'a.b'),获取fis.config中groups[defaultGroup]['modules']下一层配置项的名称
    type = type.split('.')[0];
    processors.forEach(function(obj, index) {
      var processor = obj.__name || obj;
      var key;

      if (typeof processor === 'string') {
        key = type + '.' + processor;
        processor = fis.require(type, processor);
      } else {
        key = type + '.' + index;
      }
      if (typeof processor === 'function') {
        var settings = {};

        _.assign(settings, processor.defaultOptions || processor.options || {});
        _.assign(settings, fis.media().get('settings.' + key, {}));
        typeof obj === 'object' && _.assign(settings, obj);

        // 删除隐藏配置
        delete settings.__name;
        delete settings.__plugin;
        delete settings.__pos;
        delete settings.__isPlugin;

        callback(processor, settings, key, type);
      } else {
        fis.log.warning('invalid processor [modules.' + key + ']');
      }
    });
  }
};

/**
 * url解析函数,规则类似require('url').parse
 * @param  {String} url 待解析的url
 * @param  {Object} opt 解析配置参数 { host|hostname, port, path, method, agent }
 * @return {Object}     { protocol, host, port, path, method, agent }
 * @memberOf fis.util
 * @name parseUrl
 * @function
 */
_.parseUrl = function(url, opt) {
  opt = opt || {};
  url = Url.parse(url);
  var ssl = url.protocol === 'https:';
  opt.host = opt.host || opt.hostname || ((ssl || url.protocol === 'http:') ? url.hostname : 'localhost');
  opt.port = opt.port || (url.port || (ssl ? 443 : 80));
  opt.path = opt.path || (url.pathname + (url.search ? url.search : ''));
  opt.method = opt.method || 'GET';
  opt.agent = opt.agent || false;
  return opt;
};

/**
 * 下载功能实现
 * @param  {String}   url      下载的url
 * @param  {Function} callback 回调
 * @param  {String}   extract  压缩提取路径
 * @param  {Object}   opt      配置
 * @memberOf fis.util
 * @name download
 * @function
 */
_.download = function(url, callback, extract, opt) {
  opt = _.parseUrl(url, opt || {});
  var http = opt.protocol === 'https:' ? require('https') : require('http'),
    name = _.md5(url, 8) + _.ext(url).ext,
    tmp = fis.project.getTempPath('downloads', name),
    data = opt.data;
  delete opt.data;
  _.write(tmp, '');
  var writer = fs.createWriteStream(tmp),
    http_err_handler = function(err) {
      writer.destroy();
      fs.unlinkSync(tmp);
      var msg = typeof err === 'object' ? err.message : err;
      if (callback) {
        callback(msg);
      } else {
        fis.log.error('request error [%s]', msg);
      }
    },
    req = http.request(opt, function(res) {
      var status = res.statusCode;
      res
        .on('data', function(chunk) {
          writer.write(chunk);
        })
        .on('end', function() {
          if (status >= 200 && status < 300 || status === 304) {
            if (extract) {
              fs
                .createReadStream(tmp)
                .pipe(getTar().Extract({
                  path: extract
                }))
                .on('error', function(err) {
                  if (callback) {
                    callback(err);
                  } else {
                    fis.log.error('extract tar file [%s] fail, error [%s]', tmp, err);
                  }
                })
                .on('end', function() {
                  if (callback && (typeof callback(null, tmp, res) === 'undefined')) {
                    fs.unlinkSync(tmp);
                  }
                });
            } else if (callback && (typeof callback(null, tmp, res) === 'undefined')) {
              fs.unlinkSync(tmp);
            }
          } else {
            http_err_handler(status);
          }
        })
        .on('error', http_err_handler);
    });
  req.on('error', http_err_handler);
  if (data) {
    req.write(data);
  }
  req.end();
};

/**
 * 遵从RFC规范的文件上传功能实现
 * @param  {String}   url      上传的url
 * @param  {Object}   opt      配置
 * @param  {Object}   data     要上传的formdata,可传null
 * @param  {String}   content  上传文件的内容
 * @param  {String}   subpath  上传文件的文件名
 * @param  {Function} callback 上传后的回调
 * @memberOf fis.util
 * @name upload
 * @function
 */
_.upload = function(url, opt, data, content, subpath, callback) {
  if (typeof content === 'string') {
    content = new Buffer(content, 'utf8');
  } else if (!(content instanceof Buffer)) {
    fis.log.error('unable to upload content [%s]', (typeof content));
  }
  opt = opt || {};
  data = data || {};
  var endl = '\r\n';
  var boundary = '-----np' + Math.random();
  var collect = [];
  _.map(data, function(key, value) {
    collect.push('--' + boundary + endl);
    collect.push('Content-Disposition: form-data; name="' + key + '"' + endl);
    collect.push(endl);
    collect.push(value + endl);
  });
  collect.push('--' + boundary + endl);
  collect.push('Content-Disposition: form-data; name="' + (opt.uploadField || "file") + '"; filename="' + subpath + '"' + endl);
  collect.push(endl);
  collect.push(content);
  collect.push(endl);
  collect.push('--' + boundary + '--' + endl);

  var length = 0;
  collect.forEach(function(ele) {
    if (typeof ele === 'string') {
      length += new Buffer(ele).length;
    } else {
      length += ele.length;
    }
  });

  opt.method = opt.method || 'POST';
  opt.headers = _.assign({
    'Content-Type': 'multipart/form-data; boundary=' + boundary,
    'Content-Length': length
  }, opt.headers || {});
  opt = _.parseUrl(url, opt);
  var http = opt.protocol === 'https:' ? require('https') : require('http');
  var req = http.request(opt, function(res) {
    var status = res.statusCode;
    var body = '';
    res
      .on('data', function(chunk) {
        body += chunk;
      })
      .on('end', function() {
        if (status >= 200 && status < 300 || status === 304) {
          callback(null, body);
        } else {
          callback(status);
        }
      })
      .on('error', function(err) {
        callback(err.message || err);
      });
  });
  collect.forEach(function(d) {
    req.write(d);
  });
  req.end();
};

/**
 * 读取fis组件安装
 * @param  {String} name    组件名称
 * @param  {String} version 版本标识
 * @param  {Object} opt     安装配置 { remote, extract, before, error, done,  }
 * @memberOf fis.util
 * @name install
 * @function
 */
_.install = function(name, version, opt) {
  version = version === '*' ? 'latest' : (version || 'latest');
  var remote = opt.remote.replace(/^\/$/, '');
  var url = remote + '/' + name + '/' + version + '.tar';
  var extract = opt.extract || process.cwd();
  if (opt.before) {
    opt.before(name, version);
  }
  _.download(url, function(err) {
    if (err) {
      if (opt.error) {
        opt.error(err);
      } else {
        fis.log.error('unable to download component [%s@%s] from [%s], error [%s].', name, version, url, err);
      }
    } else {
      if (opt.done) {
        opt.done(name, version);
      }
      process.stdout.write('install [' + name + '@' + version + ']\n');
      var pkg = _(extract, 'package.json');
      if (_.isFile(pkg)) {
        var info = _.readJSON(pkg);
        fs.unlinkSync(pkg);
        _.map(info.dependencies || {}, function(depName, depVersion) {
          _.install(depName, depVersion, opt);
        });
      }
    }
  }, extract);
};

/**
 * 读取JSON文件
 * @param  {String} path 路径
 * @return {Object}      JSON文件内容JSON.parse后得到的对象
 * @memberOf fis.util
 * @name readJson
 * @function
 */
_.readJSON = function(path) {
  var json = _.read(path),
    result = {};
  try {
    result = JSON.parse(json);
  } catch (e) {
    fis.log.error('parse json file[%s] fail, error [%s]', path, e.message);
  }
  return result;
};

/**
 * 模拟linux glob文法实现,但()为捕获匹配模式
 * @param  {String} pattern 符合fis匹配文法的正则串
 * @param  {String} str     待匹配的字符串
 * @param  {Object} options 匹配设置参数  @see minimatch.makeRe
 * @return {Boolean|RegExp}         若str参数为String则返回str是否可被pattern匹配
 *                                  若str参数不为String,则返回正则表达式
 * @memberOf fis.util
 * @name glob
 * @function
 */
_.glob = function(pattern, str, options) {
  var regex;

  // 推荐使用 ::text 和 ::image
  // text 和 image 后续也许不会再支持。
  if (~['::text', '::image', 'text', 'image'].indexOf(pattern)) {
    regex = getFileTypeReg(pattern.replace(/^::/, ''));

    // 当以下用法时,$0 应该是拿到文件路径的全部,而不是只有后缀,所以需要修改 regex。
    // fis.match('::image', {
    //   url: '$0'
    // })
    //
    regex = new RegExp( '^.*' + regex.source, regex.ignoreCase ? 'i' : '');
  } else {

    // 由于minimatch提供的glob支持中()语法不符合fis glob的需求,因此只针对()单独处理
    var hasBracket = ~pattern.indexOf('(');


    // 当用户配置 *.js 这种写法的时候,需要让其命中所有所有目录下面的。
    if (/^(\(*?)(?!\:|\/|\(|\*\*)(.*)$/.test(pattern)) {
      pattern = '**/' + pattern;
    }

    var special = /^(\(+?)\*\*/.test(pattern);

    // support special global star
    // 保留原来的 **/ 和 /** 用法,只扩展 **.ext 这种用法。
    pattern = pattern.replace(/\*\*(?!\/|$)/g, '\uFFF0gs\uFFF1');
    if (hasBracket) {
      if (special) {
        pattern = pattern.replace(/\(/g, '\uFFF0/').replace(/\)/g, '/\uFFF1');
      } else {
        pattern = pattern.replace(/\(/g, '\uFFF0').replace(/\)/g, '\uFFF1');
      }
    }

    regex = minimatch.makeRe(pattern, options || {
      matchBase: true,
      // nocase: true
    });

    pattern = regex.source;
    pattern = pattern.replace(/\uFFF0gs\uFFF1/g, '(?!\\.)(?=.).*');

    if (hasBracket) {
      if (special) {
        pattern = pattern.replace(/\uFFF0\\\//g, '(').replace(/\\\/\uFFF1/g, ')');
      } else {
        pattern = pattern.replace(/\uFFF0/g, '(').replace(/\uFFF1/g, ')');
      }
    }

    regex = new RegExp(pattern, regex.ignoreCase ? 'i' : '');
  }

  if (typeof str === 'string') {
    return regex.test(str);
  }

  return regex;
};

/**
 * 调起nohup命令
 * @param  {String}   cmd      执行的语句
 * @param  {Object}   options  配置参数,可传可不传 @see [child_process.exec options](https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback)
 * @param  {Function} callback nohup执行完毕的回调函数
 * @memberOf fis.util
 * @name nohup
 * @function
 */
_.nohup = function(cmd, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = null;
  }
  var exec = require('child_process').exec;
  if (IS_WIN) {
    var cmdEscape = cmd.replace(/"/g, '""'),
      file = fis.project.getTempPath('nohup-' + _.md5(cmd) + '.vbs'),
      script = '';
    script += 'Dim shell\n';
    script += 'Set shell = Wscript.CreateObject("WScript.Shell")\n';
    script += 'ret = shell.Run("cmd.exe /c start /b ' + cmdEscape + '", 0, TRUE)\n';
    script += 'WScript.StdOut.Write(ret)\n';
    script += 'Set shell = NoThing';
    _.write(file, script);
    return exec('cscript.exe /nologo "' + file + '"', options, function(error, stdout) {
      if (stdout != '0') {
        fis.log.error('exec command [%s] fail.', cmd);
      }
      fs.unlinkSync(file);
      if (typeof callback === 'function') {
        callback();
      }
    });
  } else {
    return exec('nohup ' + cmd + ' > /dev/null 2>&1 &', options, function(error, stdout) {
      if (error !== null) {
        fis.log.error('exec command [%s] fail, stdout [%s].', cmd, stdout);
      }
      if (typeof callback === 'function') {
        callback();
      }
    });
  }
};

/**
 * 判断对象是否为null,[],{},0
 * @param  {Object}  obj 待测对象
 * @return {Boolean}     是否为空
 * @memberOf fis.util
 * @name isEmpty
 * @function
 */
_.isEmpty = function(obj) {
  if (obj == null) return true;
  if (_.is(obj, 'Array')) return obj.length == 0;
  for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
      return false;
    }
  }
  return true
};

/**
 * 将 matches 规则应用到某个对象上面。
 *
 * @param {String}  path 路径。用来与 match 规则匹配
 * @param {Array} matches 规则数组
 * @param {Array} allowed 可以用来过滤掉不关心的字段。
 * @memberOf fis.util
 * @name applyMatches
 * @function
 */
_.applyMatches = function(path, matches, allowed) {
  var obj = {};

  matches.forEach(function(item, index) {
    var properties = item.properties || {};
    var keys = Object.keys(properties);

    if (!keys.length) {
      return;
    }

    var m;
    var set = item.reg;
    if (!Array.isArray(set)) {
      set = [set];
    }

    set.every(function(reg) {
      reg.lastIndex = 0; // reset
      if (m = reg.exec(path)) {
        return false;
      }

      return true;
    });

    var match = !!m;

    if (match !== item.negate) {

      // 当用 negate 模式时,排除命中特殊选择器
      if (item.negate && ~path.indexOf(':')) {
        return;
      }

      m = m || {
        0: path
      };

      keys.forEach(function(key) {

        // 如果指定了允许的属性名,走白名单规则规则。
        if (allowed && !~allowed.indexOf(key)) {
          return;
        }

        var value = typeof properties[key] === 'object' ? fis.util.cloneDeep(properties[key]) : properties[key];

        // 将 property 中 $1 $2... 替换为本来的值
        if (typeof value === 'string') {
          value = value.replace(/\$(\d+|&)/g, function(_, k) {
            k = k === '&' ? 0 : k;
            return m[k] || '';
          });
        } else if (typeof(value) === 'function' &&
          ~[
            'release',
            'url',
            'relative',
            'moduleId'
          ].indexOf(key)) {
          value = value.call(null, m, path);
        }

        // 记录是命中的 match 的位置。
        obj['__' + key + 'Index'] = index;

        // 调整 plugin 顺序
        if (value && value.__plugin && value.__pos) {
          if (!obj[key]) {
            obj[key] = value;
          } else {
            if (!Array.isArray(obj[key])) {
              obj[key] = [obj[key]];
            }

            var pos = value.__pos;
            pos = pos === 'prepend' ? 0 : (pos === 'append' ? obj[key].length : (parseInt(pos) || 0));
            obj[key].splice(pos, 0, value);
          }
        } else if (_.isPlainObject(value) && _.isPlainObject(obj[key]) && !value.__isPlugin && !obj[key].__isPlugin) {
          fis.util.assign(obj[key], value);
        } else {
          obj[key] = value;
        }
      });
    }
  });

  return obj;
};