phylo
Version:
File operations class
1,612 lines (1,378 loc) • 109 kB
JavaScript
'use strict';
// Use $ wrapper for imports to avoid name collision with locals and parameters
// (esp bad here is 'path' module):
const $ = {
fs: require('fs'),
json5: require('json5'),
mkdirp: require('mkdirp'),
os: require('os'),
path: require('path'),
rimraf: require('rimraf'),
tmp: require('tmp'),
which: require('which')
};
const platform = $.os.platform();
const isWin = /^win\d\d$/i.test(platform);
const isMac = /^darwin$/i.test(platform);
const driveLetterRe = /^[A-Z]:[\\]?$/i;
//================================================================================
/**
* This class wraps a path to a file or directory and provides methods to ease processing
* and operating on that path.
*
* ## Naming Conventions
*
* Any method that ends with 'Path' returns a string, while all other methods return a
* `File` (where appropriate). Methods often come in pairs: one that returns the result
* as a String and one that returns a `File`. Since it is best to stay in the realm of
* `File` objects, their names are the more concise.
*
* let absFile = file.absolutify(); // a File object
*
* let absPath = file.absolutePath(); // a string
*
* ### Synchronous vs Asynchronous
*
* All async methods return promises and have names that begin with 'async' (for example,
* `asyncFoo()`). Consider the `stat` method. It is synchronous while the asynchronous
* version is`asyncStat`.
*
* let st = file.stat(); // sync
*
* file.asyncStat().then(st => {
* // async
* });
*/
class File {
//noinspection JSUnusedGlobalSymbols
/**
* Returns the `File.Access` object describing the access modes available for the
* specified file. If an error is encountered determining the access (for example,
* the file does not exist), the `error` property of the returned object will be
* set accordingly.
*
* @param {String/File} filePath The `File` instance of path as a string.
* @return {File.Access} The `File.Access` descriptor.
*/
static access (filePath) {
if (!filePath) {
return this.Access.getError('ENOENT');
}
return this.from(filePath).access();
}
/**
* Creates a temporary directory and returns a Promise to its path as a `File`.
*
* When no arguments are passed the first result is cached (since one temp dir is
* often sufficient for a process).
*
* let temp;
* let temp2;
* let temp3;
*
* File.asyncTemp().then(t => temp = t); // generates temp dir
* File.asyncTemp().then(t => temp2 = t); // same dir (temp2 === temp)
*
* File.asyncTemp(null).then(t => temp3 = t); // new call to tmp.dir()
*
* Because only the first call to `temp()` does any real work, it is generally safe
* to use `temp()` (instead of `asyncTemp()`) when no options are passed and the one
* temporary folder is sufficient.
*
* @param {Object} [options] Options for `dir()` from the `tmp` module.
* @return {Promise<File>}
*/
static asyncTemp (options) {
let cached = this.hasOwnProperty('_temp');
let useCache = (options === undefined);
if (cached && useCache) {
return Promise.resolve(this._temp);
}
return new Promise((resolve, reject) => {
this.$tmp.dir(options, (err, name) => {
if (err) {
reject(err);
}
else {
let f = this.from(name);
if (useCache) {
// If we are after the shared temp, make sure it wasn't
// created during our async trip... If not, store this
// as the cached temp.
f = this._temp || (this._temp = f);
}
resolve(f);
}
});
});
}
/**
* Attempts to find the given program by its `name` in the system **PATH**. If it is
* found, a `File` instance is resolved. If not, `null` is resolved. If an error
* occurs, the promise will reject accordingly.
*
* @param {String} name The name of the program to find.
* @param {Object/String/String[]} options Options to control the process or a
* replacement value for the PATH as a string or array of strings.
* @param {String/String[]/File[]} options.path A replacement PATH
* @param {String} options.pathExt On Windows, this overrides the **PATHEXT**
* environment variable (normally, '.EXE;.CMD;.BAT;.COM').
* @return {Promise<File>}
*/
static asyncWhich (name, options) {
let opts = this._whichOptions(options);
return new Promise((resolve, reject) => {
$.which(name, opts, (err, result) => {
if (err) {
if (err.code === 'ENOENT') {
resolve(null);
}
else {
reject(err);
}
}
else {
resolve(this.from(result));
}
});
});
}
/**
* Returns the `process.cwd()` as a `File` instance.
* @return {File} The `process.cwd()` as a `File` instance.
*/
static cwd () {
return new this(process.cwd());
}
/**
* Returns `true` if the specified file exists, `false` if not.
* @param {String/File} filePath The `File` or path to test for existence.
* @return {Boolean} `true` if the file exists.
*/
static exists (filePath) {
let st = this.stat(filePath);
return !st.error;
}
/**
* Returns a `File` for the specified path (if it is not already a `File`).
* @param {String/File} filePath The `File` or path to convert to a `File`.
* @return {File} The `File` instance.
*/
static from (filePath) {
let file = filePath || null;
if (file && !file.$isFile) {
file = new this(filePath);
}
return file;
}
/**
* Returns the path as a string given a `File` or string.
* @param {String/File} filePath
* @return {String} The path.
*/
static fspath (filePath) {
return ((filePath && filePath.$isFile) ? filePath.fspath : filePath) || '';
}
/**
* Converts a file-system "glob" pattern into a `RegExp` instance.
*
* For example:
*
* glob('*.txt')
* glob('** /*.txt')
*
* See `File.Globber` for more details on `options`.
*
* @param {String} pattern The glob pattern to convert.
* @param {String} [options=null] Pass `'E'` to enable "extended" globs like
* in Bash. Pass 'S' to treat '*' as simple (shell-like) wildcards. This will which
* matches `'/'` characters with a `'*'`. By default, only `'**'` matches `'/'`.
* Other options are passed along a `RegExp` flags (e.g., 'i' and 'g').
* @return {RegExp}
*/
static glob (pattern, options) {
return this.Globber.get(options || '').compile(pattern);
}
/**
* Returns the `os.homedir()` as a `File` instance. On Windows, this is something
* like `'C:\Users\Name'`.
*
* @return {File} The `os.homedir()` as a `File` instance.
*/
static home () {
return new this(this.$os.homedir());
}
/**
* Returns `true` if the specified path is a directory, `false` if not.
* @param {String/File} filePath The `File` or path to test.
* @return {Boolean} Whether the file is a directory or not.
*/
static isDir (filePath) {
if (!filePath) {
return false;
}
return this.from(filePath).isDir();
}
/**
* Returns `true` if the specified path is a file, `false` if not.
* @param {String/File} filePath The `File` or path to test.
* @return {Boolean} Whether the file is a file or not (opposite of isDir).
*/
static isFile (filePath) {
if (!filePath) {
return false;
}
return this.from(filePath).isFile();
}
/**
* This method is the same as `join()` in the `path` module except that the items
* can be `File` instances or `String` and a `File` instance is returned.
* @param {File.../String...} parts Name fragments to join using `path.join()`.
* @return {File} The `File` instance from the resulting path.
*/
static join (...parts) {
let f = this.joinPath(...parts);
return new this(f);
}
/**
* This method is the same as `join()` in the `path` module except that the items
* can be `File` instances or `String`.
* @param {File.../String...} parts Name fragments to join using `path.join()`.
* @return {String} The resulting path.
*/
static joinPath (...parts) {
let n = parts && parts.length || 0;
for (let i = 0; i < n; ++i) {
let p = parts[i];
if (p.$isFile) {
parts[i] = p.path;
}
}
let ret = (n === 1) ? parts[0] : (n && this.$path.join(...parts));
return ret || '';
}
/**
* Returns the path as a string given a `File` or string.
* @param {String/File} filePath
* @return {String} The path.
*/
static path (filePath) {
return ((filePath && filePath.$isFile) ? filePath.path : filePath) || '';
}
/**
* Returns the folder into which applications should save data for their users. For
* example, on Windows this would be `'C:\Users\Name\AppData\Roaming\Company'` where
* "Name" is the user's name and "Company" is the owner of the data (typically the
* name of the company producing the application).
*
* This location is platform-specific:
*
* - Windows: C:\Users\Name\AppData\Roaming\Company
* - Mac OS X: /Users/Name/Library/Application Support/Company
* - Linux: /home/name/.local/share/data/company
* - Default: /home/name/.company
*
* The set of recognized platforms for profile locations is found in `profilers`.
*
* @param {String} company The name of the application's producer.
* @return {File} The `File` instance.
*/
static profile (company) {
company = company || this.COMPANY;
if (!company) {
throw new Error('Must provide company name to isolate profile data');
}
let fn = this.profilers.default;
return fn.call(this, this.home(), company);
}
/**
* This method is the same as `resolve()` in the `path` module except that the items
* can be `File` instances or `String` and a `File` instance is returned.
* @param {File.../String...} parts Name fragments to resolve using `path.resolve()`.
* @return {File} The `File` instance.
*/
static resolve (...parts) {
let f = this.resolvePath(...parts);
return new this(f);
}
/**
* This method is the same as `resolve()` in the `path` module except that the items
* can be `File` instances or `String`.
* @param {File.../String...} parts Name fragments to resolve using `path.resolve()`.
* @return {String} The resulting path.
*/
static resolvePath (...parts) {
for (let i = 0, n = parts.length; i < n; ++i) {
let p = parts[i];
if (p.$isFile) {
p = p.path;
}
parts[i] = this._detildify(p);
}
return (parts && parts.length && this.$path.resolve(...parts)) || '';
}
/**
* Splits the given `File` or path into an array of parts.
* @param {String/File} filePath
* @return {String[]} The path parts.
*/
static split (filePath) {
let path = this.path(filePath);
return path.split(this.re.split);
}
/**
* Compares two files using the `File` instances' `compare('d')` method to sort
* folder before files (each group being sorted by name).
* @param filePath1 A `File` instance or string path.
* @param filePath2 A `File` instance or string path.
* @return {Number}
*/
static sorter (filePath1, filePath2) {
let a = this.from(filePath1);
return a.compare(filePath2, 'd');
}
/**
* Compares two files using the `File` instances' `compare('f')` method to sort
* files before folders (each group being sorted by name).
* @param filePath1 A `File` instance or string path.
* @param filePath2 A `File` instance or string path.
* @return {Number}
*/
static sorterFilesFirst (filePath1, filePath2) {
let a = this.from(filePath1);
return a.compare(filePath2, 'f');
}
/**
* Compares two files using the `File` instances' `compare(false)` method to sort
* files and folder together by name.
* @param filePath1 A `File` instance or string path.
* @param filePath2 A `File` instance or string path.
* @return {Number}
*/
static sorterByPath (filePath1, filePath2) {
let a = this.from(filePath1);
return a.compare(filePath2, false);
}
/**
* Returns the `fs.Stats` for the specified `File` or path. If the file does not
* exist, or an error is encountered determining the stats, the `error` property
* will be set accordingly.
*
* @param {String/File} filePath
* @return {fs.Stats}
*/
static stat (filePath) {
let f = this.from(filePath);
if (!f) {
return this.Stat.getError('ENOENT');
}
return f.stat();
}
/**
* Creates a temporary directory and returns its path as a `File`.
*
* When no arguments are passed the first result is cached (since one temp dir is
* often sufficient for a process).
*
* let temp = File.temp(); // generates temp dir
*
* let temp2 = File.temp(); // === temp
*
* let temp3 = File.temp(null); // new call to tmp.dirSync()
*
* @param {Object} [options] Options for `dirSync()` from the `tmp` module.
* @return {File}
*/
static temp (options) {
let cached = this.hasOwnProperty('_temp');
let useCache = (options === undefined);
if (cached && useCache) {
return this._temp;
}
let result = this.$tmp.dirSync(options);
result = this.from(result.name);
if (useCache) {
this._temp = result;
}
return result;
}
/**
* Attempts to find the given program by its `name` in the system **PATH**. If it is
* found, a `File` instance is returned. If not, `null` is returns. If an error
* occurs, an `Error` is thrown accordingly.
*
* @param {String} name The name of the program to find.
* @param {Object/String/String[]} options Options to control the process or a
* replacement value for the PATH as a string or array of strings.
* @param {String/String[]/File[]} options.path A replacement PATH
* @param {String} options.pathExt On Windows, this overrides the **PATHEXT**
* environment variable (normally, '.EXE;.CMD;.BAT;.COM').
* @return {File}
*/
static which (name, options) {
let opts = this._whichOptions(options);
try {
// throws on not found...
return this.from($.which.sync(name, opts));
}
catch (e) {
if (e.code === 'ENOENT') {
return null;
}
throw e;
}
}
//-----------------------------------------------------------------
/**
* Initialize an instance by joining the given path fragments.
* @param {File/String...} parts The path fragments.
*/
constructor (...parts) {
this.path = this.constructor.joinPath(...parts);
}
//----------------------------
// Properties
/**
* @property {String} name
* @readonly
* The name of the file at the end of the path. For example, given '/foo/bar/baz',
* the `name` is 'baz'.
*
* Paths that end with a separator (e.g., '/foo/bar/') are treated as if the trailing
* separator were not present. That is, 'bar' would be the `name` of '/foo/bar/'. This
* is to be consistent with this behavior:
*
* File.from('/foo/bar/').equals('/foo/bar'); // === true
*
* Typically known as `basename` on Linux-like systems.
*/
get name () {
let name = this._name;
if (name === undefined) {
let index = this.lastSeparator();
let path = this.path;
let end = path.length;
if (index === path.length - 1) {
// e.g. 'foo/bar/'
index = this.lastSeparator((end = index) - 1);
}
// even if index = -1, index+1=0 which is what we want...
this._name = name = path.substring(index + 1, end) || '';
}
return name;
}
/**
* @property {File} parent
* @readonly
* The parent directory of this file. For example, for '/foo/bar/baz' the `parent` is
* '/foo/bar'. This is `null` for the file system root.
*
* Paths that end with a separator (e.g., '/foo/bar/') are treated as if the trailing
* separator were not present. That is, '/foo' would be the `parent` of '/foo/bar/'.
* This is to be consistent with this behavior:
*
* File.from('/foo/bar/').equals('/foo/bar'); // === true
*
* Typically known as `dirname` on Linux-like systems.
*/
get parent () {
let parent = this._parent;
if (parent === undefined) {
let path = this.path;
let sep = this.lastSeparator();
let ret;
if (sep > -1) {
if (sep === path.length - 1) {
// e.g. 'foo/bar/'
sep = this.lastSeparator(sep - 1);
}
ret = path.substr(0, sep);
if (sep === 2 && driveLetterRe.test(ret)) {
ret += '\\';
if (ret === path) {
ret = null;
}
}
}
if (!ret) {
let abs = this.absolutePath();
ret = this.$path.resolve(abs, '..');
if (abs === ret) {
ret = null;
}
}
this._parent = parent = this.constructor.from(ret);
}
return parent;
}
/**
* @property {String} extent
* @readonly
* The type of the file at the end of the path. For example, given '/foo/bar/baz.js',
* the `extent` is 'js'. Returns `''` for files with no extension (e.g. README).
*/
get extent () {
let ext = this._extent;
if (ext === undefined) {
let name = this.name;
let index = name.lastIndexOf('.');
this._extent = ext = ((index > -1) && name.substr(index + 1)) || '';
}
return ext;
}
/**
* @property {String} fspath
* @readonly
* The same as `path` property except resolved for `'~'` pseudo-roots and hence
* useful for `fs` module calls.
*/
get fspath () {
return this.constructor._detildify(this.path);
}
//-----------------------------------------------------------------
// Path calculation
/**
* Return absolute path to this file.
* @return {String}
*/
absolutePath () {
return this.constructor.resolvePath(this.path);
}
//noinspection JSUnusedGlobalSymbols
/**
* Returns a `File` instance created from the `absolutePath`.
* @return {File}
*/
absolutify () {
return this.constructor.from(this.absolutePath()); // null/blank handling
}
asyncCanonicalPath () {
let path = this.absolutePath();
return new Promise(resolve => {
this.$fs.realpath(path, (err, result) => {
if (err) {
resolve(null);
}
else {
resolve(result);
}
});
})
}
asyncCanonicalize () {
return this.asyncCanonicalPath().then(path => {
return this.constructor.from(path);
});
}
/**
* Returns the canonical path to this file.
* @return {String} The canonical path of this file or `null` if no file exists.
*/
canonicalPath () {
try {
return this.$fs.realpathSync(this.absolutePath());
} catch (e) {
return null;
}
}
/**
* Returns a `File` instance created from the canonical path
* @return {File} The `File` with the canonical path or `null` if no file exists.
*/
canonicalize () {
return this.constructor.from(this.canonicalPath()); // null/blank handling
}
joinPath (...parts) {
return this.constructor.joinPath(this, ...parts);
}
join (...parts) {
return this.constructor.join(this, ...parts);
}
lastSeparator (start) {
let path = this.path,
i = path.lastIndexOf('/', start);
if (this.constructor.WIN) {
// Windows respects both / and \ as path separators
i = Math.max(i, path.lastIndexOf('\\'));
}
return i;
}
nativePath (separator) {
let p = this.path;
return p && p.replace(this.re.split, separator || this.constructor.separator);
}
nativize (separator) {
return this.constructor.from(this.nativePath(separator));
}
normalize () {
return this.constructor.from(this.normalizedPath());
}
normalizedPath () {
let p = this.path;
return p && this.$path.normalize(p);
}
/**
* Returns the relative path of this file in relation to the `from` file or path.
* @param {String/File} from The base location from which this file is relative.
* @return {String}
*/
relativePath (from) {
if (from.$isFile) {
from = from.absolutePath();
}
let p = this.absolutePath();
return p && from && this.$path.relative(from, p);
}
/**
* Returns a `File` object containing the relative path of this file in relation to
* the `from` file or path.
* @param {String/File} from The base location from which this file is relative.
* @return {File}
*/
relativize (from) {
return this.constructor.from(this.relativePath(from));
}
resolvePath (...parts) {
return this.constructor.resolvePath(this, ...parts);
}
resolve (...parts) {
return this.constructor.resolve(this, ...parts);
}
slashifiedPath () {
return this.path.replace(this.re.backslash, '/');
}
/**
* Replace forward/backward slashes with forward slashes.
* @return {String}
*/
slashify () {
return this.constructor.from(this.slashifiedPath());
}
split () {
return this.constructor.split(this);
}
toString () {
return this.path;
}
terminatedPath (separator, match) {
if (separator == null || separator === true) {
separator = this.constructor.separator;
}
match = match || this.re.slash;
let p = this.path;
if (p && p.length) {
let n = p.length - 1;
let c = p[n];
if (separator) {
if (!match.test(c)) {
p += separator;
}
}
else {
while (n >= 0 && match.test(c)) {
p = p.substr(0, n--);
c = p[n];
}
}
}
return p || '';
}
terminate (separator, match) {
return this.constructor.from(this.terminatedPath(separator, match));
}
unterminatedPath (match) {
return this.terminatedPath(false, match);
}
unterminate (match) {
return this.constructor.from(this.unterminatedPath(match));
}
//-----------------------------------------------------------------
// Path checks
/**
* Compare this `File` to the other `File` or path and return -1, 0 or 1 if this
* file is less-then, equal to or great then the `other`.
* @param {File/String} other The file or path to which to compare this `File`.
* @param {'d'/'f'/false} [first='d'] Pass `'d'` to group directories before files,
* `'f'` to group files before directories or `false` to sort only by path.
* @return {Number} -1, 0 or 1 if this file is, respectively, less-than, equal to
* or great-than the `other`.
*/
compare (other, first) {
other = this.constructor.from(other);
if (!other) {
return 1;
}
if (this._stat && other._stat) {
let p = this.parent;
first = (first === false) ? 0 : (first || 'd');
if (first && p && p.equals(other.parent)) {
// Two files in the same parent folder both w/stats
let d1 = this._stat.isDirectory();
let d2 = other._stat.isDirectory();
if (d1 !== d2) {
let c = d1 ? -1 : 1;
if (first === 'f') {
c = -c;
}
return c;
}
}
}
// Treat '/foo/bar' and '/foo/bar/' as equal (by stripping trailing delimiters)
let a = this.unterminatedPath();
let b = other.unterminatedPath();
// If the platform has case-insensitive file names, ignore case...
if (this.constructor.NOCASE) {
a = a.toLocaleLowerCase();
b = b.toLocaleLowerCase();
}
return (a < b) ? -1 : ((b < a) ? 1 : 0);
}
equals (other) {
let c = this.compare(other);
return c === 0;
}
isAbsolute () {
let p = this.path;
return p ? this.re.abs.test(p) || this.$path.isAbsolute(p) : false;
}
isRelative () {
return this.path ? !this.isAbsolute() : false;
}
prefixes (subPath) {
subPath = this.constructor.from(subPath);
if (subPath) {
// Ensure we don't have trailing slashes ('/foo/bar/' => '/foo/bar')
let a = this.slashify().unterminatedPath();
let b = subPath.slashifiedPath();
if (this.constructor.NOCASE) {
a = a.toLocaleLowerCase();
b = b.toLocaleLowerCase();
}
if (a === b) {
return true;
}
if (b.startsWith(a)) {
// a = '/foo/bar'
// b = '/foo/bar/zip' ==> true
// b = '/foo/barf' ==> false
return b[a.length] === '/';
}
}
return false;
}
//-----------------------------------------------------------------
// File system checks
/**
* Returns a `File.Access` object describing the access available for this file. If
* the file does not exist, or some other error is encountered, the `error` property
* will be set.
*
* let acc = File.from(s).access();
*
* if (acc.rw) {
* // file at location s has R and W permission
* }
* else if (acc.error === 'ENOENT') {
* // no file ...
* }
* else if (acc.error) {
* // some other error
* }
*
* Alternatively:
*
* if (File.from(s).can('rw')) {
* // file at location s has R and W permission
* }
*
* @return {File.Access}
*/
access () {
let st = this.stat();
let Access = this.constructor.Access;
if (st.error) {
return Access.getError(st.error);
}
return Access[st.mode & Access.rwx.mask];
}
/**
* Returns `true` if the desired access is available for this file.
* @param {'r'/'rw'/'rx'/'rwx'/'w'/'wx'/'x'} mode
* @return {Boolean}
*/
can (mode) {
let acc = this.access();
return acc[mode];
}
/**
* Returns `true` if this file exists, `false` if not.
* @return {Boolean}
*/
exists () {
let st = this.stat();
return !st.error;
}
/**
* Returns `true` if the specified path exists relative to this path.
* @param {String} rel A path relative to this path.
* @return {Boolean}
*/
has (rel) {
let f = this.resolve(rel);
return f.exists();
}
/**
* Returns `true` if the specified directory exists relative to this path.
* @param {String} rel A path relative to this path.
* @return {Boolean}
*/
hasDir (rel) {
let f = this.resolve(rel);
return f.isDir();
}
/**
* Returns `true` if the specified file exists relative to this path.
* @param {String} rel A path relative to this path.
* @return {Boolean}
*/
hasFile (rel) {
let f = this.join(rel);
return f.isFile();
}
/**
* Returns `true` if this file is a hidden file.
* @param {Boolean} [asNative] Pass `true` to match native Explorer/Finder meaning
* of hidden state.
* @return {Boolean}
*/
isHidden (asNative) {
const Win = this.constructor.Win;
if (!Win || !asNative) {
let name = this.name || '';
if (name[0] === '.') {
return true;
}
}
if (Win) {
let st = this.stat();
return st.attrib.H; // if we got an error, H will be false
}
return false;
}
/**
* Returns the `[fs.Stats](https://nodejs.org/api/fs.html#fs_class_fs_stats)` but
* ensures a fresh copy of the stats are fetched from the file-system.
*
* @return {fs.Stats}
*/
restat () {
this._stat = null;
return this.stat();
}
/**
* Return the `[fs.Stats](https://nodejs.org/api/fs.html#fs_class_fs_stats)` for a
* (potentially) symbolic link but ensures a fresh copy of the stats are fetched
* from the file-system.
*
* @return {fs.Stats}
*/
restatLink () {
this._lstat = null;
return this.statLink();
}
/**
* Return the `[fs.Stats](https://nodejs.org/api/fs.html#fs_class_fs_stats)`.
*
* let st = File.from(s).stat();
*
* if (st) {
* // file exists...
* }
*
* If the file does not exist, or some other error is encountered determining the
* stats, the `error` property is set accordingly.
*
* The stat object is cached on this instance. Use `restat` to ensure a fresh stat
* from the file-system. This cached stat object is shared with `asyncStat()` and
* `asyncRestat()` methods. The `statLink()` family uses a separately cached object.
*
* @return {fs.Stats} The stats or `null` if the file does not exist.
*/
stat () {
let st = this._stat;
if (!st) {
let path = this.fspath;
try {
st = this.$fs.statSync(path);
let Win = this.constructor.Win;
st.attrib = Win ? Win.attrib(path) : this.constructor.Attribute.NULL;
}
catch (e) {
st = this.constructor.Stat.getError(e);
}
this._stat = st;
}
return st;
}
/**
* Return the `[fs.Stats](https://nodejs.org/api/fs.html#fs_class_fs_stats)` for a
* (potentially) symbolic link.
*
* If the file does not exist, or some other error is encountered determining the
* stats, the `error` property is set accordingly.
*
* The stat object is cached on this instance. Use `restatLink` to ensure a fresh stat
* from the file-system. This cached stat object is shared with `asyncStatLink()` and
* `asyncRestatLink()` methods. The `stat()` family uses a separately cached object.
*
* @return {fs.Stats} The stats or `null` if the file does not exist.
*/
statLink () {
let st = this._lstat;
if (!st) {
let path = this.fspath;
try {
st = this.$fs.lstatSync(path);
let Win = this.constructor.Win;
st.attrib = Win ? Win.attrib(path) : this.constructor.Attribute.NULL;
}
catch (e) {
st = this.constructor.Stat.getError(e);
}
this._lstat = st;
}
return st;
}
/**
* Starting at this location, searches upwards for a location that passes the provided
* `test` function. If `test` is a string, it will match any item (file or folder).
*
* // climb until a folder has a '.git' item (file or folder)
* f = file.up('.git');
*
* // f references the folder that contains the '.git' folder.
*
* // Climb until a folder has a '.git' sub-folder.
* f = file.up(p => p.join('.git').isDirectory());
*
* The above is equivalent to:
*
* f = file.upDir('.git');
*
* // f references the folder that contains the '.git' folder.
*
* @param {String/Function} test If a string is passed, the string is passed to the
* `has` method. Otherwise, the `test` function is called with the candidate and
* should return `true` to indicate a match.
* @return {File}
*/
up (test) {
let fn = (typeof test === 'string') ? (p => p.has(test)) : test;
for (let parent = this; parent; parent = parent.parent) {
if (fn(parent)) {
return parent;
}
}
return null;
}
/**
* Searches upwards for a folder that has the specified sub-directory.
*
* f = file.upDir('.git');
*
* // f references the folder that contains the '.git' folder.
*
* @param {String} dir The sub-directory that the desired parent must contain.
* @return {File}
*/
upDir (dir) {
return this.up(parent => parent.hasDir(dir));
}
/**
* Searches upwards for a folder that has the specified file.
*
* f = file.upFile('package.json');
*
* // f references the folder that contains the 'package.json' file.
*
* @param {String} file The file that the desired parent must contain.
* @return {File}
*/
upFile (file) {
return this.up(parent => parent.hasFile(file));
}
/**
* Starting at this location, searches upwards for a location that contains the given
* item and returns a `File` describing the item.
*
* // climb until a folder has a '.git' item (file or folder)
* f = file.upTo('.git');
*
* // f references the '.git' folder.
*
* The above is equivalent to:
*
* f = file.upToDir('.git');
*
* // f references the '.git' folder.
*
* @param {String} name A name passed to the `has` method.
* @return {File}
*/
upTo (name) {
let ret = this.up(name);
if (ret) {
ret = ret.join(name);
}
return ret;
}
/**
* Searches upwards for a folder that has the specified sub-directory and returns a
* `File` describing the sub-directory.
*
* f = file.upToDir('.git');
*
* // f references the '.git' folder.
*
* @param {String} dir The sub-directory that the desired parent must contain.
* @return {File}
*/
upToDir (dir) {
let ret = this.upDir(dir);
if (ret) {
ret = ret.join(dir);
}
return ret;
}
/**
* Searches upwards for a folder that has the specified file.
*
* f = file.upToFile('package.json');
*
* // f references the '.git' folder.
*
* @param {String} file The file that the desired parent must contain.
* @return {File}
*/
upToFile (file) {
let ret = this.upFile(file);
if (ret) {
ret = ret.join(file);
}
return ret;
}
//------------------------------------------------------------------
// File system checks (async)
/**
* Returns a `File.Access` object describing the access available for this file. If
* the file does not exist, or some other error is encountered, the `error` property
* is set accordingly.
*
* File.from(s).asyncAccess().then(acc => {
* if (acc.rw) {
* // file at location s has R and W permission
* }
* else if (acc.error === 'ENOENT') {
* // no file ...
* }
* else if (acc.error) {
* // some other error
* }
* });
*
* Alternatively:
*
* File.from(s).asyncCan('rw').then(can => {
* if (can) {
* // file at location s has R and W permission
* }
* });
*
* @return {Promise}
*/
asyncAccess () {
const Access = this.constructor.Access;
return this.asyncStat().then(st => {
if (st.error) {
return Access.getError(st.error);
}
return Access[st.mode & Access.rwx.mask];
});
}
asyncCan (mode) {
return this.asyncAccess().then(acc => {
return acc[mode];
});
}
asyncExists () {
return this.asyncStat().then(st => {
return !st.error;
});
}
/**
* Returns a Promise that resolves to `true` if this file is a hidden file.
* @param {Boolean} [asNative] Pass `true` to match native Explorer/Finder meaning
* of hidden state.
* @return {Promise<Boolean>}
*/
asyncIsHidden (asNative) {
const Win = this.constructor.Win;
if (!Win || !asNative) {
let name = this.name || '';
if (name[0] === '.') {
return Promise.resolve(true);
}
}
if (Win) {
return this.asyncStat().then(st => {
return st.attrib.H; // if we got an error, H will be false
});
}
return Promise.resolve(false);
}
/**
* Returns the `[fs.Stats](https://nodejs.org/api/fs.html#fs_class_fs_stats)` but
* ensures a fresh copy of the stats are fetched from the file-system.
*
* @return {Promise<fs.Stats>} The stats or `null` if the file does not exist.
*/
asyncRestat () {
this._stat = null;
return this.asyncStat();
}
/**
* Return the `[fs.Stats](https://nodejs.org/api/fs.html#fs_class_fs_stats)` for a
* (potentially) symbolic link but ensures a fresh copy of the stats are fetched
* from the file-system.
*
* @return {Promise<fs.Stats>} The stats or `null` if the file does not exist.
*/
asyncRestatLink () {
this._lstat = null;
return this.asyncStatLink();
}
/**
* Return the `[fs.Stats](https://nodejs.org/api/fs.html#fs_class_fs_stats)`.
*
* If the file does not exist, or some other error is encountered determining the
* stats, the `error` property is set accordingly.
*
* File.from(s).asyncStat().then(st => {
* if (!st.error) {
* // file exists...
* }
* });
*
* The stat object is cached on this instance. Use `asyncRestat` to ensure a fresh
* stat from the file-system. This cached stat object is shared with `stat()` and
* `restat()` methods. The `statLink` family uses a separately cached object.
*
* @return {Promise<fs.Stats>} The stats or `null` if the file does not exist.
*/
asyncStat () {
if (this._stat) {
return Promise.resolve(this._stat);
}
const F = this.constructor;
const path = this.fspath;
const Win = F.Win;
return this._async('_asyncStat', () => {
return new Promise(resolve => {
this.$fs.stat(path, (err, st) => {
if (err) {
resolve(F.Stat.getError(err));
}
else {
st.attrib = F.Attribute.NULL;
this._stat = st;
if (Win) {
Win.asyncAttrib(path).then(attr => {
st.attrib = attr;
resolve(st);
},
e => {
resolve(st);
});
}
else {
resolve(st);
}
}
});
});
});
}
/**
* Return the `[fs.Stats](https://nodejs.org/api/fs.html#fs_class_fs_stats)` for a
* (potentially) symbolic link.
*
* If the file does not exist, or some other error is encountered determining the
* stats, the `error` property is set accordingly.
*
* File.from(s).asyncStatLink().then(st => {
* if (!st.error) {
* // file exists...
* }
* });
*
* The stat object is cached on this instance. Use `asyncRestatLink` to ensure a fresh
* stat from the file-system. This cached stat object is shared with `statLink()` and
* `restatLink()` methods. The `stat()` family uses a separately cached object.
*
* @return {Promise<fs.Stats>} The stats or `null` if the file does not exist.
*/
asyncStatLink () {
if (this._lstat) {
return Promise.resolve(this._lstat);
}
const F = this.constructor;
const path = this.fspath;
const Win = F.Win;
return this._async('_asyncStatLink', () => {
return new Promise(resolve => {
this.$fs.lstat(path, (err, st) => {
if (err) {
resolve(F.Stat.getError(err));
}
else {
st.attrib = F.Attribute.NULL;
this._lstat = st;
if (Win) {
Win.asyncAttrib(path).then(attr => {
st.attrib = attr;
resolve(st);
},
e => {
resolve(st);
});
}
else {
resolve(st);
}
}
});
});
});
}
//-----------------------------------------------------------------
// Directory Operations
/**
* This is the asynchronous version of the `list` method.
*
* @param {String} [mode] A string containing the mode characters described above.
* @param {String/RegExp/Function} matcher Either a wildcard/glob (e.g., '*.txt'),
* a `RegExp` or a function that accepts two arguments (the file name (a String) and
* the `File` instance) and returns `true` to include the file.
* @param {String} matcher.name The name of the file.
* @param {File} matcher.file The `File` instance.
* @return {Promise<File[]>}
*/
asyncList (mode, matcher) {
if (typeof mode !== 'string') {
matcher = mode;
mode = '';
}
const F = this.constructor;
let listMode = F.ListMode.get(mode);
// If matcher is a String, we'll get a default globber compile. If it is a
// RegExp or a Function, those things are already baked in. In all cases, we
// have null or a function that takes a File.
let test = F.Globber.from(matcher);
return new Promise((resolve, reject) => {
let fail = e => {
if (listMode.T) {
if (reject) {
reject(e);
}
}
else if (resolve) {
resolve(null);
}
reject = resolve = null;
};
let finish = () => {
if (!resolve) {
return;
}
reject = null;
let statType = listMode.l ? '_lstat' : '_stat';
if (listMode.f) {
result = result.filter(f => !f[statType].isDirectory());
}
else if (listMode.d) {
result = result.filter(f => f[statType].isDirectory());
}
if (!listMode.A) {
result = result.filter(f => {
if (listMode.hideDots && f.name[0] === '.') {
return false;
}
return !f[statType].attrib.H;
});
}
if (test) {
result = result.filter(f => test(f.name, f));
}
if (listMode.o) {
result.sort(F.sorter.bind(F));
}
resolve(result);
resolve = null;
};
let result = [];
this.$fs.readdir(this.fspath, (err, names) => {
if (err) {
if (listMode.T) {
reject(err);
}
else {
resolve(null);
}
return;
}
let promises = [];
names.forEach(name => {
let f = new F(this, name);
let promise;
f._parent = this;
if (test) {
if (listMode.l) {
promise = f.asyncStatLink();
if (listMode.s) {
promise = Promise.all([ promise, f.asyncStat() ]);
}
}
else if (listMode.s) {
promise = f.asyncStat();
}
if (promise) {
promise = promise.then(() => {
return test(name, f);
});
}
else {
promise = Promise.resolve(test(name, f));
}
promises.push(promise.then(keep => {
if (keep) {
result.push(f);
}
}));
}
else {
result.push(f);
// The user may have asked to cache both types of stats...
if (listMode.l) {
promises.push(f.asyncStatLink());
}
if (listMode.s) {
promises.push(f.asyncStat());
}
}
});
if (promises.length) {
Promise.all(promises).then(finish, fail);
}
else {
finish();