UNPKG

phylo

Version:
1,612 lines (1,378 loc) 109 kB
'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();