DirBackUpper.js

/* globals Wsh: false */

(function () {
  if (Wsh && Wsh.DirBackUpper) return;

  /**
   * The WSH (Windows Script Host) CLI that updates or mirrors directories according to the schema defined in a JSON file.
   *
   * @namespace DirBackUpper
   * @memberof Wsh
   * @requires {@link https://github.com/tuckn/WshBasicPackage|tuckn/WshBasicPackage}
   */
  Wsh.DirBackUpper = {};

  // Shorthands
  var util = Wsh.Util;
  var path = Wsh.Path;
  var os = Wsh.OS;
  var fs = Wsh.FileSystem;
  var fse = Wsh.FileSystemExtra;
  var zlib = Wsh.ZLIB;
  var logger = Wsh.Logger;

  var objAdd = Object.assign;
  var cloneDeep = util.cloneDeep;
  var insp = util.inspect;
  var obtain = util.obtainPropVal;
  var parseTmp = util.parseTemplateLiteral;
  var parseDate = util.parseDateLiteral;
  var hasContent = util.hasContent;
  var includes = util.includes;
  var isArray = util.isArray;
  var isBoolean = util.isBoolean;
  var isString = util.isString;
  var isEmpty = util.isEmpty;
  var isSolidArray = util.isSolidArray;
  var isSolidObject = util.isSolidObject;
  var isSolidString = util.isSolidString;
  var isPlainObject = util.isPlainObject;
  var isSameMeaning = util.isSameMeaning;
  var srrd = os.surroundCmdArg;

  var dirBkup = Wsh.DirBackUpper; // Shorthand

  /** @constant {string} */
  var MODULE_TITLE = 'WshDirBackUpper/DirBackUpper.js';

  var throwErrNonStr = function (functionName, errVal) {
    util.throwTypeError('string', MODULE_TITLE, functionName, errVal);
  };

  var throwErrNonObject = function (functionName, errVal) {
    util.throwTypeError('object', MODULE_TITLE, functionName, errVal);
  };

  var throwValErr = function (valName, functionName, errVal) {
    util.throwValueError(valName, MODULE_TITLE, functionName, errVal);
  };

  // dirBkup.backupDir {{{
  /**
   * Backs up the directory.
   *
   * @example
   * var dirBkup = Wsh.DirBackUpper; // Shorthand
   *
   * var srcDir = 'C:\\Users';
   * var destDir = 'D:\\BackUp\\Users\\#{yyyy}\\#{MM - 1}';
   *
   * dirBkup.backupDir(srcDir, destDir, {
   *   sync: 'MIRROR',
   *   comparison: 'CONTENT',
   *   ignoredRegExp: 'tmp$',
   *   logger: 'warn/winEvent' // See https://github.com/tuckn/WshLogger
   * });
   * @function backupDir
   * @memberof Wsh.DirBackUpper
   * @param {string} srcDir - The source directory path to back up.
   * @param {string} destDir - The destination directory path.
   * @param {object} [options] - Optional parameters.
   * @param {string} [options.syncMethod='UPDATE'] - The synchronization method. "UPDATE" or "MIRROR".
   * @param {string} [options.comparison='TIME'] - "TIME" or "CONTENT".
   * @param {boolean} [options.isRecursive=true] - Copies sub directories.
   * @param {boolean} [options.copiesEmpDir=false] - Copies empty directories.
   * @param {boolean} [options.includesSymlink=false] - Copies symbolic link.
   * @param {string|Array} [options.matchedRegExp] - e.g. "\\w+\\.txt$"
   * @param {string|Array} [options.ignoredRegExp] - e.g. "\\.tmp$"
   * @param {(Logger|string|object)} [options.logger] - The Logger instance or create options. See {@link https://tuckn.net/docs/WshLogger/Wsh.Logger.html#.create|Wsh.Logger.create}.
   * @param {boolean} [options.transportsLog=true] - Outputs Wsh.Logger logs after connecting. See {@link https://tuckn.net/docs/WshLogger/Wsh.Logger.html#.this.transport|Wsh.Logger.transport}.
   * @param {boolean} [options.isDryRun=false] - No execute, returns the string of command.
   * @param {boolean} [options.throws=false] - Throws a error.
   * @returns {void}
   */
  dirBkup.backupDir = function (srcDir, destDir, options) {
    var FN = 'dirBkup.backupDir';
    var loggerObj = obtain(options, 'logger', {});
    var lggr = logger.create(loggerObj);
    lggr.info('Start the function ' + FN);

    lggr.debug('srcDir: ' + insp(srcDir));
    lggr.debug('dest: ' + insp(destDir));
    lggr.debug('options: ' + insp(options));

    // Setting the source and destination directory
    if (!isSolidString(srcDir)) throwErrNonStr(FN, srcDir);
    if (!isSolidString(destDir)) throwErrNonStr(FN, destDir);

    var srcDirPath = path.resolve(srcDir);
    lggr.info('srcDir: "' + srcDir + '" -> "' + srcDirPath + '"');

    var destDirPath = parseDate(path.resolve(destDir));
    lggr.info('destDir: "' + destDir + '" -> "' + destDirPath + '"');

    // Setting parameters
    var syncMethod = obtain(options, 'syncMethod', 'UPDATE');
    if (!/^(update|mirror)$/i.test(syncMethod)) {
      throwValErr('options.update', FN, syncMethod);
    }
    lggr.info('syncMethod: ' + syncMethod);

    var comparison = obtain(options, 'comparison', 'TIME');
    lggr.info('comparison: ' + comparison);

    var isDryRun = obtain(options, 'isDryRun', false);
    lggr.info('isDryRun: ' + String(isDryRun));

    var throws = obtain(options, 'throws', false);
    lggr.info('throws: ' + String(throws));

    var copyDummy = function () { return; };
    var copyFunc = isDryRun ? copyDummy : fse.copySync;
    var removeDummy = function () { return; };
    var removeFunc = isDryRun ? removeDummy : fse.removeSync;

    // Setting filtering sub directories options
    var copiesEmpDir = obtain(options, 'copiesEmpDir', false);
    lggr.info('copiesEmpDir: ' + String(copiesEmpDir));

    var includesSymlink = obtain(options, 'includesSymlink', false);
    lggr.info('includesSymlink: ' + String(includesSymlink));

    var matchedRegExp = obtain(options, 'matchedRegExp', null);
    if (matchedRegExp && isArray(matchedRegExp) && matchedRegExp.length > 0) {
      matchedRegExp = '(' + matchedRegExp.join('|') + ')';
    }
    lggr.info('matchedRegExp: ' + matchedRegExp);

    var ignoredRegExp = obtain(options, 'ignoredRegExp', null);
    if (ignoredRegExp && isArray(ignoredRegExp) && ignoredRegExp.length > 0) {
      ignoredRegExp = '(' + ignoredRegExp.join('|') + ')';
    }
    lggr.info('ignoredRegExp: ' + ignoredRegExp);

    var isRecursive = obtain(options, 'isRecursive', true);
    lggr.info('isRecursive: ' + String(isRecursive));

    var readFn = isRecursive ? fse.readdirSyncRecursively : fs.readdirSync;

    // Getting files lists
    lggr.info('Reading srcDir' + (isRecursive ? ' recursively...' : '...'));

    var srcFileNames = readFn(srcDirPath, {
      withFileTypes: false,
      excludesSymlink: !includesSymlink,
      matchedRegExp: matchedRegExp,
      ignoredRegExp: ignoredRegExp
    });
    lggr.debug('srcFileNames: ' + insp(srcFileNames));

    var srcNum = srcFileNames.length;
    lggr.info('Found ' + srcNum + ' files and directories in src');

    var destFileNames = [];
    if (fs.existsSync(destDirPath)) {
      lggr.info('Reading destDir' + (isRecursive ? ' recursively...' : '...'));

      destFileNames = readFn(destDirPath, {
        withFileTypes: false,
        excludesSymlink: !includesSymlink,
        matchedRegExp: matchedRegExp,
        ignoredRegExp: ignoredRegExp
      });

      lggr.info('Found ' + destFileNames.length + ' files and directories in dest');
    } else {
      lggr.info('destDir is not existing');
    }
    lggr.debug(destFileNames);

    // Compare differences of file and copy {{{
    lggr.info('Comparing a difference of file ' + comparison);

    srcFileNames.forEach(function (srcFileName, i) {
      if (lggr.transportation === 'CONSOLE') WScript.StdOut.Write('.');

      var logHeader = '[' + (i + 1) + '/' + srcNum + '] "' + srcFileName + '"';
      lggr.debug(logHeader);

      var srcPath = path.join(srcDirPath, srcFileName);
      var destPath = path.join(destDirPath, srcFileName);
      lggr.debug('srcPath: ' + srcPath);
      lggr.debug('destPath: ' + destPath);

      try {
        if (fs.statSync(srcPath).isDirectory()) {
          if (copiesEmpDir && !isDryRun) fse.ensureDirSync(destPath);
          return;
        }

        if (!includes(destFileNames, srcFileName, 'i')) {
          lggr.info('Copied ' + logHeader + ' (New file)');
          return copyFunc(srcPath, destPath);
        }

        if (/^time$/i.test(comparison)) {
          if (fse.isTheSameFile(srcPath, destPath, 'date')) return;
          lggr.info('Copied ' + logHeader + ' (Modified date are different)');
          return copyFunc(srcPath, destPath);
        }

        if (/^content$/i.test(comparison)) {
          if (fse.isTheSameFile(srcPath, destPath, 'MD5')) return;
          lggr.info('Copied ' + logHeader + ' (MD5 values are different)');
          return copyFunc(srcPath, destPath);
        }

        throw new Error('"' + comparison + '" is undefined comparison method.');
      } catch (e) {
        if (throws) {
          throw new Error(insp(e) + '\n'
              + '  at ' + FN + ' (' + MODULE_TITLE + ')\n'
              + '  copy "' + srcPath + '" to "' + destPath);
        }

        lggr.error(logHeader + ' -> Error occured while trying to copy "' + srcPath + '" to "' + destPath + '". ' + insp(e));
      }
    }); // }}}

    if (/^mirror$/i.test(syncMethod)) {
      lggr.info('Remove none-existing files from dest');
      destFileNames.forEach(function (destFileName) {
        if (includes(srcFileNames, destFileName, 'i')) return;
        lggr.info('Remove ' + destFileName + ' in ' + destDirPath);
        return removeFunc(path.join(destDirPath, destFileName));
      });
    }

    lggr.info('Finished the function ' + FN);
    var transportsLog = obtain(options, 'transportsLog', true);
    if (transportsLog) lggr.transport();

    return;
  }; // }}}

  // dirBkup.archiveDir {{{
  /**
   * Compresses the directory into archive file (ZIP or RAR).
   *
   * @example
   * var dirBkup = Wsh.DirBackUpper; // Shorthand
   *
   * var srcDir = 'C:\\Users';
   * var dest = 'D:\\BackUp\\Users\\#{yyyy}\\#{MM - 1}';
   *
   * dirBkup.archiveDir(srcDir, dest, {
   *   archiveType: 'ZIP',
   *   ignoredRegExp": ["\\.git.*"],
   *   archiveOptions": {
   *     exe7z: exe7z
   *     dateCode: 'yyyy-MM-dd_hhmmss',
   *     compressLv: 9,
   *     password: 'This is mY&p@ss ^_<',
   *   },
   *   logger: 'warn/winEvent' // See https://github.com/tuckn/WshLogger
   * });
   * @function archiveDir
   * @memberof Wsh.DirBackUpper
   * @param {string} srcDir - The source directory path to back up.
   * @param {string} dest - The destination directory path.
   * @param {object} [options] - Optional parameters.
   * @param {string} [options.archiveType='ZIP'] - The archiving method, 'ZIP' (default) or 'RAR'
   * @param {typeDeflateZipOption|typeDeflateRarOption} [options.archiveOptions] - Optional parameters. See {@link https://tuckn.net/docs/WshZLIB/global.html#typeDeflateZipOption|Wsh.ZLIB.typeDeflateZipOption} and {@link https://tuckn.net/docs/WshZLIB/global.html#typeDeflateRarOption|Wsh.ZLIB.typeDeflateRarOption}.
   * @param {boolean} [options.forEachSubDir=true] - Compresses each sub directory in the specified source directory.
   * @param {boolean} [options.rootFilesName='RootFiles'] - When forEachSubDire option is true, root files are archived as this name. (default: 'RootFiles')
   * @param {boolean} [options.includesEmptyDir=false] - Compresses empty directories.
   * @param {boolean} [options.includesSymlink=false] - Compresses symbolic links.
   * @param {string|Array} [options.matchedRegExp] - When forEachSubDir option is true, matched RegExp only for the root directories and files in the source. e.g. "^[^.].+$"
   * @param {string|Array} [options.ignoredRegExp] - When forEachSubDir option is true, ignored RegExp only for the root directories and files in the source. e.g. "\\.git.*"
   * @param {(Logger|string|object)} [options.logger] - The Logger instance or create options. See {@link https://tuckn.net/docs/WshLogger/Wsh.Logger.html#.create|Wsh.Logger.create}.
   * @param {boolean} [options.isDryRun=false] - No execute, returns the string of command.
   * @param {boolean} [options.throws=false] - Throws a error.
   * @param {boolean} [options.transportsLog=true] - Outputs Wsh.Logger logs after connecting. See {@link https://tuckn.net/docs/WshZLIB/global.html#typeDeflateResult|Wsh.ZLIB.typeDeflateResult}.
   * @returns {typeDeflateResult|typeDeflateResult[]|string|string[]} - @see typeDeflateResult. If options.isDryRun is true, returns string.
   */
  dirBkup.archiveDir = function (srcDir, dest, options) {
    var FN = 'dirBkup.archiveDir';
    var loggerObj = obtain(options, 'logger', {});
    var lggr = logger.create(loggerObj);
    lggr.info('Start the function ' + FN);

    lggr.debug('srcDir: ' + insp(srcDir));
    lggr.debug('dest: ' + insp(dest));
    lggr.debug('options: ' + insp(options));

    var errMes = '\n'
      + '  at ' + FN + ' (' + MODULE_TITLE + ').\n'
      + '  srcDir: ' + insp(srcDir) + ',\n'
      + '  dest: ' + insp(dest) + ',\n'
      + '  options: ' + insp(options);

    // Setting the source directory and destination path
    if (!isSolidString(srcDir)) throwErrNonStr(FN, srcDir);
    if (!isSolidString(dest)) throwErrNonStr(FN, dest);

    var srcDirPath = path.resolve(srcDir);
    lggr.info('srcDir: ' + srrd(srcDir) + ' -> ' + srrd(srcDirPath));

    var destPath = parseDate(path.resolve(dest));
    lggr.info('dest: ' + srrd(dest) + ' -> ' + srrd(destPath));

    // Setting parameters
    var throws = obtain(options, 'throws', false);
    lggr.info('throws: ' + String(throws));

    var archiveType = obtain(options, 'archiveType', 'ZIP');
    lggr.info('archiveType: ' + String(archiveType));

    var archiveOptions = obtain(options, 'archiveOptions', true);
    lggr.info('archiveOptions: ' + insp(archiveOptions));

    // Overwriting isDryRUn
    var isDryRun = obtain(options, 'isDryRun', null);
    lggr.info('isDryRun: ' + String(isDryRun));

    var compOp = archiveOptions;
    if (isBoolean(isDryRun)) {
      compOp = objAdd(archiveOptions, { isDryRun: isDryRun });
    }

    var archiveFunc = zlib.deflateSync;
    if (isSameMeaning(archiveType, 'RAR')) {
      archiveFunc = zlib.deflateSyncIntoRar;
    }

    var forEachSubDir = obtain(options, 'forEachSubDir', true);
    lggr.info('forEachSubDir: ' + String(forEachSubDir));

    var rtn;

    if (!forEachSubDir) {
      try {
        rtn = archiveFunc(srcDirPath, destPath, compOp);
        lggr.info('Finished to archive process with exitCode: ' + rtn.exitCode);
        lggr.info('archivedPath: ' + rtn.archivedPath);
      } catch (e) {
        if (throws) throw new Error(insp(e) + errMes);
        lggr.error(insp(e));
      }
    } else {
      // Setting options for filtering sub directories
      var includesEmptyDir = obtain(options, 'includesEmptyDir', false);
      lggr.info('includesEmptyDir: ' + String(includesEmptyDir));

      var includesSymlink = obtain(options, 'includesSymlink', false);
      lggr.info('includesSymlink: ' + String(includesSymlink));

      var matchedRegExp = obtain(options, 'matchedRegExp', null);
      if (matchedRegExp && isArray(matchedRegExp) && matchedRegExp.length > 0) {
        matchedRegExp = '(' + matchedRegExp.join('|') + ')';
      }
      lggr.info('matchedRegExp: ' + matchedRegExp);

      var ignoredRegExp = obtain(options, 'ignoredRegExp', null);
      if (ignoredRegExp && isArray(ignoredRegExp) && ignoredRegExp.length > 0) {
        ignoredRegExp = '(' + ignoredRegExp.join('|') + ')';
      }
      lggr.info('ignoredRegExp: ' + ignoredRegExp);

      // Getting files to be archive
      var srcFileNames = fs.readdirSync(srcDirPath, {
        withFileTypes: false,
        excludesSymlink: !includesSymlink,
        matchedRegExp: matchedRegExp,
        ignoredRegExp: ignoredRegExp
      });
      lggr.debug('srcFileNames: ' + insp(srcFileNames));

      var srcNum = srcFileNames.length;
      lggr.info('Found ' + srcNum + ' files and directories in src');

      // Compresses each sub directory and makes root files
      rtn = [];
      var rootFiles = [];
      var additionalArchiveOptions = obtain(options, 'additionalArchiveOptions', null);

      srcFileNames.forEach(function (srcFileName, i) {
        if (lggr.transportation === 'CONSOLE') WScript.StdOut.Write('.');

        var logHeader = '[' + (i + 1) + '/' + srcNum + '] ' + srcFileName;
        lggr.info(logHeader);

        var srcPath = path.join(srcDirPath, srcFileName);
        lggr.info('srcPath: ' + srcPath);

        if (fs.statSync(srcPath).isDirectory()) {
          if (!includesEmptyDir) {
            var srcItems = fs.readdirSync(srcPath, { withFileTypes: false });

            if (isEmpty(srcItems)) {
              lggr.info('Skipped due to empty directory. ' + srcPath);
              return;
            }
          }

          var op = cloneDeep(compOp); // Copy the Object

          // Overwriting options with the current directory options
          if (isPlainObject(additionalArchiveOptions)) {
            var dirName = path.basename(srcPath);
            var exOp = additionalArchiveOptions[dirName];
            if (isPlainObject(exOp)) op = objAdd(op, exOp);
          }

          lggr.info('options for archiving: ' + insp(op));

          try {
            var rtnCmpDir = archiveFunc(srcPath, destPath, op);
            lggr.info('Finished to archive process with exitCode: ' + rtnCmpDir.exitCode);
            lggr.info('archivedPath: ' + rtnCmpDir.archivedPath);
            rtn.push(rtnCmpDir);
          } catch (e) {
            if (throws) throw new Error(insp(e) + errMes);
            lggr.error(insp(e));
          }
          return;
        }

        if (fs.statSync(srcPath).isFile()) {
          rootFiles.push(srcPath);
        }
      });

      // Compresses root files
      if (!isEmpty(rootFiles)) {
        var rootFilesName = obtain(options, 'rootFilesName', 'RootFiles');
        var destRootFiles = path.join(destPath, rootFilesName);

        try {
          var rtnCmpFs = archiveFunc(rootFiles, destRootFiles, compOp);
          lggr.info('Finished to archive process with exitCode: ' + rtnCmpFs.exitCode);
          lggr.info('archivedPath: ' + rtnCmpFs.archivedPath);
          rtn.push(rtnCmpFs);
        } catch (e) {
          if (throws) throw new Error(insp(e) + errMes);
          lggr.error(insp(e));
        }
      }
    }

    lggr.info('Finished the function ' + FN);
    var transportsLog = obtain(options, 'transportsLog', true);
    if (transportsLog) lggr.transport();

    return rtn;
  }; // }}}

  // dirBkup.backupDirUsingSchema {{{
  /**
   * @typedef {object} typeSchemaBackUpper
   * @property {string} [description]
   * @property {object} [components]
   * @property {...(typeSchemaBackUpperTask|typeSchemaArchiverTask)} tasks
   */

  /**
   * @typedef {object} typeSchemaBackUpperTask
   * @property {string} [description] - The task description.
   * @property {boolean} [available=true] - If specifying false, Skips the task.
   * @property {string} srcDir - The source directory path to back up.
   * @property {string} destDir - The destination directory path.
   * @property {string} [syncMethod] - The synchronization method. "UPDATE" or "MIRROR".
   * @property {string} [comparison] - "TIME" or "CONTENT".
   * @property {boolean} [isRecursive] - Copies sub directories.
   * @property {boolean} [copiesEmpDir] - Copies empty directories.
   * @property {boolean} [includesSymlink] - Copies symbolic link.
   * @property {string|Array} [matchedRegExp] - e.g. "\\w+\\.txt$"
   * @property {string|Array} [ignoredRegExp] - e.g. "\\.tmp$"
   * @property {boolean} [throws=false] - Throws a error.
   */

  /**
   * @typedef {typeDeflateZipOption|typeDeflateRarOption} typeSchemaArchiverTask - See {@link https://tuckn.net/docs/WshZLIB/global.html#typeDeflateZipOption|Wsh.ZLIB.typeDeflateZipOption} and {@link https://tuckn.net/docs/WshZLIB/global.html#typeDeflateRarOption|Wsh.ZLIB.typeDeflateRarOption}.
   * @property {string} [description] - The task description.
   * @property {boolean} [available=true] - If specifying false, Skips the task.
   * @property {string} srcDir - The source directory path to archive.
   * @property {string} dest - The destination path.
   * @property {string} archiveType - The archiving method, 'ZIP' or 'RAR'
   * @property {string} {boolean} [forEachSubDir=true] - Compresses each sub directory in the specified source directory.
   * @property {boolean} [includesEmptyDir=false] - Compresses empty directories.
   * @property {boolean} [includesSymlink] - Copies symbolic link.
   * @property {string|Array} [matchedRegExp] - e.g. "^[^.].+$"
   * @property {string|Array} [ignoredRegExp] - e.g. "\\.git.*"
   * @property {boolean} [throws=false] - Throws a error.
   */

  /**
   * Backs up the directories.
   *
   * @example
   * var dirBkup = Wsh.DirBackUpper; // Shorthand
   * var schema = {
   *   description: 'Example Schema WshDirBackUpper',
   *   components: {
   *     dest: '\\\\MyNas\\BackUp',
   *     exe7z: 'D:\\My Apps\\7-Zip\\7z.exe',
   *     anyVal1: null
   *   },
   *   tasks: {
   *     userAppData: {
   *       description: 'Example task with options',
   *       srcDir: 'C:\\Users\\Default\\AppData',
   *       destDir: '${dest}\\AppData\\#{yyyy}\\#{MM-dd}',
   *       method: 'UPDATE',
   *       options: {
   *         comparison: 'TIME',
   *         ignoredRegExp: [
   *           'Windows\\\\WebCache',
   *           'Packages\\\\.*Cache\\\\',
   *           '\\.mui$',
   *           '\\.settingcontent-ms$'
   *         ]
   *       }
   *     },
   *     'userAppData:zip': {
   *       srcDir: 'C:\\Users\\Default\\AppData',
   *       destDir: '${dest}\\AppData\\archives',
   *       method: 'ARCHIVE',
   *       options: {
   *         ignoredRegExp: ['\\.git.*'],
   *         archiveType: 'ZIP',
   *         archiveOptions: {
   *           exe7z: '${exe7z}',
   *           dateCode: 'yyyy-MM-dd',
   *           compressLv: 9,
   *           password: 'This is mY&p@ss ^_<'
   *         },
   *         additionalArchiveOptions: {
   *           'Visual Studio Code': {
   *             excludingFiles: [
   *               '*\\data\\user-data\\*Cache*\\*',
   *               '*\\data\\user-data\\logs\\*',
   *               '*\\data\\user-data\\*\\*\\LOCK'
   *             ]
   *           }
   *         }
   *       }
   *     },
   *     'appLog:current': {
   *       srcDir: 'D:\\AppLogs\\#{yyyy}\\#{MM}',
   *       destDir: '${dest}\\AppLogs\\#{yyyy}\\#{MM}',
   *       method: 'MIRROR',
   *       options: {
   *         comparison: 'CONTENT',
   *         matchedRegExp: '\\.csv$'
   *       }
   *     },
   *     'appLog:lastMonth': {
   *       available: false,
   *       srcDir: '${anyVal1}:\\AppLogs\\#{yyyy\\[MM-1]}',
   *       destDir: '${dest}\\AppLogs\\#{yyyy\\[MM-1]}',
   *       method: 'MIRROR',
   *       options: {
   *         comparison: 'TIME',
   *         matchedRegExp: '\\.csv$'
   *       }
   *     }
   *   }
   * };
   *
   * dirBkup.backupDirUsingSchema(schema, 'work:*', {
   *   logger: 'info/console',
   *   overwrites: { anyVal1: 'E' }
   * });
   * // Only process appLog:current. appLog:lastMonth is not processed because available is false.
   * @function backupDirUsingSchema
   * @memberof Wsh.DirBackUpper
   * @param {typeSchemaBackUpper} schema
   * @param {string} [taskName] - The task name to back up.
   * @param {object} [options] - Optional parameters.
   * @param {object} [options.overwrites] - Ex. { anyVal1: 'myP@ss', anyVal2: 'p_w_d' }
   * @param {(string|Object)} [options.logger] - See options of {@link Wsh.Logger.create}
   * @param {boolean} [options.isDryRun=false] - No execute, returns the string of command.
   * @returns {void}
   */
  dirBkup.backupDirUsingSchema = function (schema, taskName, options) {
    var FN = 'dirBkup.backupDirUsingSchema';
    var loggerObj = obtain(options, 'logger', {});
    var lggr = logger.create(loggerObj);
    lggr.info('Start the function ' + FN);

    lggr.debug('schema: ' + insp(schema));
    lggr.debug('taskName: ' + insp(taskName));
    lggr.debug('options: ' + insp(options));

    var transportsLog = obtain(options, 'transportsLog', true);
    lggr.info('transportsLog: ' + String(transportsLog));

    var isDryRun = obtain(options, 'isDryRun', false);
    lggr.info('isDryRun: ' + String(isDryRun));

    var throws = obtain(options, 'throws', false);
    lggr.info('throws: ' + String(throws));

    // Filtering execution tasks
    if (!isPlainObject(schema)) throwErrNonObject(FN, schema);
    if (!isSolidString(taskName)) throwErrNonStr(FN, taskName);

    lggr.info('taskName: ' + taskName);

    var scm = cloneDeep(schema); // Copy the Object
    var tasks = scm.tasks; // Shorthand
    var taskNames = Object.keys(tasks);

    var regNameMatcher;
    var baseRegStr = '^' + taskName + '$';
    if (includes(taskName, '*')) {
      regNameMatcher = new RegExp(baseRegStr.replace(/\*/g, '.*'));
    } else {
      regNameMatcher = new RegExp(baseRegStr);
    }

    var execTasks = taskNames.filter(function (taskName) {
      return regNameMatcher.test(taskName);
    });
    lggr.info('matched tasks number: ' + execTasks.length);

    // Getting component values in the schema
    var cmpVals = scm.components; // Shorthand

    // Overwriting component values in keys storing null.
    if (hasContent(options.overwrites)) {
      Object.keys(cmpVals).forEach(function (key) {
        if (cmpVals[key] !== null) return;

        Object.keys(options.overwrites).some(function (writeKey) {
          if (key === writeKey) {
            cmpVals[key] = options.overwrites[writeKey];
            return true;
          }
        });
      });
    }

    // Executing tasks
    execTasks.forEach(function (taskName) {
      var tsk = tasks[taskName]; // Shorthand

      lggr.info('Start the task: ' + taskName);

      // Skipping non available task
      if (tsk.available === false) {
        lggr.info('available: false => Skip the task: ' + taskName);
        return;
      }

      // Setting parameters

      // Parsing with the component values
      var parseComponentStr = function (obj, key) {
        var val = obj[key];

        if (isString(val)) {
          obj[key] = parseDate(parseTmp(val || '', cmpVals));
          lggr.info(key + ': ' + val + ' -> ' + obj[key]);
          return;
        }

        if (isSolidObject(val)) {
          Object.keys(val).forEach(function (keyInVal) {
            parseComponentStr(obj[key], keyInVal);
          });
        }
      };

      Object.keys(tsk).forEach(function (propName) {
        parseComponentStr(tsk, propName);
      });

      var method = obtain(tsk, 'method');
      lggr.info('method: ' + method);

      var op = objAdd(
        // The option on the schema
        tsk.options,
        // The options at this function
        {
          logger: lggr,
          transportsLog: transportsLog,
          isDryRun: isDryRun,
          throws: throws
        }
      );

      if (isSameMeaning(method, 'ARCHIVE')) {
        // Archiving
        try {
          dirBkup.archiveDir(tsk.srcDir, tsk.destDir, op);
        } catch (e) {
          if (throws) throw new Error(insp(e));
          lggr.error(insp(e));
        }
      } else {
        // Copying
        op = objAdd(op, { syncMethod: method });

        try {
          dirBkup.backupDir(tsk.srcDir, tsk.destDir, op);
        } catch (e) {
          if (throws) throw new Error(insp(e));
          lggr.error(insp(e));
        }
      }

      lggr.info('Finished the task: ' + taskName);
    });

    lggr.info('Finished the function ' + FN);
    if (transportsLog) lggr.transport();

    return;
  }; // }}}
})();

// vim:set foldmethod=marker commentstring=//%s :