file.js

import Promise from 'bluebird';
import path from 'path';
import fileGlob from 'minimatch';

import fsp from './fsp';
import filelock from './lock';

function joinWith(dir) {
  return (file) => {
    return path.join(dir, file);
  };
}

/**
 * @class
 */
class File {
  constructor(pathname) {
    this._dir = process.cwd();
    this._pathname = pathname;
  }

  _getStatsSync() {
    return fsp.statSync(this._pathname);
  }

  _getStats() {
    return fsp.statAsync(this._pathname);
  }

  _isHiddenFile() {
    return (/^\./).test(path.basename(this._pathname));
  }

  _isHiddenDirectory() {
    return (/(^|\/)\.[^\/\.]/g).test(this._pathname);
  }

  _depth(pathname) {
    return pathname.split(path.sep).length - 1;
  }

  _access(permission) {
    let hasPermission = true;

    return fsp.accessAsync(this._pathname, permission)
      .catch(() => hasPermission = false)
      .then(() => hasPermission);
  }

  _checkAsyncStats(type) {
    return this._getStats().then((stats) => stats[type]());
  }

  /**
   * Synchronously determine if pathname is a directory
   *
   * @instance
   * @memberOf File
   * @method
   * isDirectorySync
   * @return boolean
   * @example
   * import File from 'file-js';
   *
   * const file = File.create('myDirectory');
   * if (file.isDirectorySync()) {
   *    console.log('processing directory');
   * }
   */
  isDirectorySync() {
    return this._getStatsSync().isDirectory();
  }

  /**
   * Synchronously determine if pathname is a socket
   *
   * @instance
   * @memberOf File
   * @method
   * isSocketSync
   * @return boolean
   * @example
   * import File from 'file-js';
   *
   * const file = File.create('mysocket');
   * if (file.isSocketSync()) {
   *    console.log('processing socket');
   * }
   */
  isSocketSync() {
    return this._getStatsSync().isSocket();
  }

  /**
   * Synchronously determine if pathname is a file
   *
   * @instance
   * @memberOf File
   * @method
   * isFileSync
   * @return boolean
   * @example
   * import File from 'file-js';
   *
   * const file = File.create('myDirectory');
   * if (file.isFileSync()) {
   *    console.log('processing file');
   * }
   */
  isFileSync() {
    return this._getStatsSync().isFile();
  }

  /**
   * Determine if pathname is a directory
   *
   * @instance
   * @memberOf File
   * @method
   * isDirectory
   * @return If the Promise fulfils, the fulfilment value is
   * a boolean indicating if the pathname is a directory
   * @example
   * import File from 'file-js';
   *
   * const file = File.create('myDirectory');
   * file.isDirectory((isDirectory) => {
   *   console.log(isDirectory);
   * });
   *
   */
  isDirectory() {
    return this._checkAsyncStats('isDirectory');
  }

  /**
   * Determine if pathname is a Socket
   *
   * @instance
   * @memberOf File
   * @method
   * isSocket
   * @return If the Promise fulfils, the fulfilment value is
   * a boolean indicating if the pathname is a Socket
   * @example
   * import File from 'file-js';
   *
   * const file = File.create('mySocket');
   * file.isSocket((isSocket) => {
   *   console.log(isSocket);
   * });
   *
   */
  isSocket() {
    return this._checkAsyncStats('isSocket');
  }

  /**
   * Determine if pathname is a file
   *
   * @instance
   * @memberOf File
   * @method
   * isDirectory
   * @return If the Promise fulfils, the fulfilment value is
   * a boolean indicating if the pathname is a file
   * @example
   * import File from 'file-js';
   *
   * const file = File.create('myDirectory');
   * file.isFile((isFile) => {
   *   console.log(isFile);
   * });
   */
  isFile() {
    return this._checkAsyncStats('isFile');
  }

  /**
   * Synchronously determine if pathname is a hidden file
   *
   * @instance
   * @memberOf File
   * @method
   * isHiddenSync
   * @return boolean
   * @example
   * import File from 'file-js';
   *
   * const file = File.create('./myHiddenFile');
   * if (file.isHiddenSync()) {
   *    console.log('processing hidden file');
   * }
   */
  isHiddenSync() {
    if (!this.isDirectorySync()) {
      return this._isHiddenFile();
    }
    return this._isHiddenDirectory();
  }

  /**
   * Determine if pathname is a file
   *
   * @instance
   * @memberOf File
   * @method
   * isDirectory
   * @return If the Promise fulfils, the fulfilment value is
   * a boolean indicating if the pathname is a file
   * @example
   * import File from 'file-js';
   *
   * const file = File.create('myDirectory');
   * file.isFile((isFile) => {
   *   console.log(isFile);
   * });
   */
  isHidden() {
    this.isDirectory()
      .then((isDirectory) => {
        if (!isDirectory) {
          return this._isHiddenFile();
        }
        return this._isHiddenDirectory();
      });
  }

  /**
   *  Renames the abstract pathname
   *
   * @instance
   * @memberOf File
   * @param {string|File} pathname - pathname either as a string or File instance
   * @method
   * rename
   * @return If the Promise fulfils, the fulfilment value is undefined
   * @example
   * import File from 'file-js';
   *
   * const original = File.create('fileA');
   * const renameTo = File.create('fileB');
   * original
   *  .rename(renameTo)
   *  .then(() => {
   *     console.log(original.getName()) // prints fileA
   *  });
   */
  rename(pathname) {
    const newname = pathname instanceof File ? pathname.getName() : pathname;

    return fsp
      .renameAsync(this._pathname, newname)
      .then(() => {
        this._pathname = newname;
      });
  }

  /**
   * Synchronously get list of files, if pathname is a directory
   *
   * @instance
   * @memberOf File
   * @method
   * getListSync
   * @return array of files
   * @example
   * import File from 'file-js';
   *
   * const file = File.create('./myHiddenFile');
   * const files = file.getListSync();
   * console.log(files);
   */
  getListSync() {
    if (this.isDirectorySync()) {
      return fsp.readdirSync(this._pathname).map((file) => {
        return path.join(this._pathname, file);
      });
    }
    return null;
  }

  /**
   * Get list of file objects, if pathname is a directory
   *
   * @instance
   * @memberOf File
   * @method
   * getList
   * @param {string=} glob - file glob
   * @return a promise. If the Promise fulfils, the fulfilment value is
   * a list of pathnames
   * @example
   * import File from 'file-js';
   *
   * // get all json files
   * const file = File.create('./myDirectory');
   * file.getFiles('*.json')
   * .then((jsonFiles) => {
   *    console.log(jsonFiles);
   * });
   */
  getList(glob) {
    return this.getFiles(glob)
      .then((list) => {
        if (!list) return [];

        return list.map((pathname) => pathname.getName());
      });
  }

  /**
   * Get list of file objects, if pathname is a directory
   *
   * @instance
   * @memberOf File
   * @param {string=} glob - file glob
   * @method
   * getFiles
   * @return a promise. If the Promise fulfils, the fulfilment value is
   * a list of File objects
   * @example
   * import File from 'file-js';
   *
   * // get last modified time of all json files
   * const file = File.create('./myDirectory');
   * file.getFiles('*.json')
   * .then((jsonFiles) => {
   *    console.log(jsonFiles.map(file => file.lastModifiedSync()));
   * });
   */
  getFiles(glob) {
    if (!this.isDirectory()) return Promise.resolve(null);

    const results = fsp
      .readdirAsync(this._pathname)
      .map(joinWith(this._pathname))
      .then((list) => {
        if (!list) return Promise.resolve(null);

        return list.map((pathname) => File.create(pathname));
      });

    if (glob) return results.filter((file) => file.isMatch(glob));

    return results;
  }

  /**
   * Synchronously get list of file objects, if pathname is a directory
   *
   * @instance
   * @memberOf File
   * @method
   * getFileSync
   * @return array of files
   * @example
   * import File from 'file-js';
   *
   * const file = File.create('./myHiddenFile');
   * const files = file.getFileSync();
   * console.log(files);
   */
  getFilesSync(glob) {
    if (this.isDirectorySync()) {
      const files = this.getListSync()
        .map((pathname) => {
          return File.create(pathname);
        });

      if (glob) return files.filter((file) => file.isMatch(glob));

      return files;
    }
    return null;
  }

  /**
   * Synchronously caculate the depth of a directory
   *
   * @instance
   * @memberOf File
   * @method
   * getDepthSync
   * @return boolean
   * @example
   * import File from 'file-js';
   *
   * const file = File.create('myDirectory');
   * console.log(file.getDepthSync());
   */
  getDepthSync() {
    if (!this.isDirectorySync()) {
      return this._depth(path.dirname(this._pathname));
    }
    return this._depth(this._pathname);
  }

  /**
   * Returns the pathname as a string
   *
   * @instance
   * @memberOf File
   * @method
   * getName
   * @return String
   * @example
   * import File from 'file-js';
   *
   * const file = File.create('myDirectory');
   * console.log(file.getName());
   */
  getName() {
    return this._pathname;
  }

  /**
   * Returns the absolutePath
   *
   * @instance
   * @memberOf File
   * @method
   * getAbsolutePath
   * @return String
   * @example
   * import File from 'file-js';
   *
   * const file = File.create('myFile');
   * console.log(file.getAbsolutePath());
   */
  getAbsolutePath() {
    if (path.isAbsolute(this._pathname)) {
      return this._pathname;
    }
    return [this._dir, this._pathname].join(path.sep);
  }

  /**
   * Returns the canonical path
   *
   * @instance
   * @memberOf File
   * @method
   * getCanonicalPath
   * @return String
   * @example
   * import File from 'file-js';
   *
   * const file = File.create('myFile');
   * console.log(file.getCanonicalPath());
   */
  getCanonicalPath() {
    return path.normalize(this.getAbsolutePath());
  }

  /**
   * Returns the file extension.
   *
   * @instance
   * @memberOf File
   * @method
   * getPathExtension
   * @return String
   * @example
   * import File from 'file-js';
   *
   * const file = File.create('./tmp.sh');
   * console.log(file.getPathExtension()); // sh
   */
  getPathExtension() {
    return path.extname(this._pathname).substring(1);
  }

  isMatch(globPattern) {
    const glob = new fileGlob.Minimatch(globPattern, {
      matchBase: true
    });
    return glob.match(this._pathname);
  }

  lastModifiedSync() {
    return this._getStatsSync()['mtime'];
  }

  lastAccessedSync() {
    return this._getStatsSync()['atime'];
  }

  lastChangedSync() {
    return this._getStatsSync()['ctime'];
  }

  sizeSync() {
    return this._getStatsSync().size;
  }

  isWritable() {
    return this._access(fsp.W_OK);
  }

  isReadable() {
    return this._access(fsp.R_OK);
  }

  isExecutable() {
    return this._access(fsp.X_OK);
  }

  delete() {
    return fsp.unlinkAsync(this._pathname);
  }

  /**
   * Locks the pathname
   *
   * @instance
   * @memberOf File
   * @method
   * withLock
   * @return returning value of function
   * @example
   * import File from 'file-js';
   *
   * const file = File.create('myFile');
   * file.with(() => {
   *   if (file.isFileSync()) {
   *     file.delete();
   *   } 
   * });
   */
  withLock(fn) {
    return filelock.lockAsync(this._pathname)
      .then(() => {
        return fn();
      })
      .finally(() => {
        filelock.unlockAsync(this._pathname);
      });
  }

  /**
   * Static factory method to create an instance of File
   *
   * @static
   * @memberOf File
   * @method
   * create
   * @return File instance
   * @example
   * import File from 'file-js';
   *
   * const file = File.create();
   */
  static create(filename) {
    return new File(filename);
  }
}

module.exports.create = File.create;